Skip to main content

Verifying Webhook Signatures

StudioBase signs every webhook delivery following the Standard Webhooks specification, so you can verify with an off-the-shelf Standard Webhooks library or a few lines of code. Always verify signatures before trusting a payload.

Your endpoint's signing secret (format whsec_<base64>) is shown in Settings → Integrations → Webhooks when you create the endpoint.

Delivery headers

HeaderDescription
webhook-idThe event UUID — stable across retries; deduplication key
webhook-timestampUnix timestamp (seconds) of this delivery attempt
webhook-signaturev1,<base64 HMAC-SHA256> of the signed content

The signed content is the concatenation:

{webhook-id}.{webhook-timestamp}.{raw request body}

The HMAC key is the base64-decoded portion of your secret after the whsec_ prefix.

Verification example (Node.js)

const crypto = require("crypto");

function verifyWebhook({ secret, headers, rawBody }) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"] ?? "";

  // Reject stale deliveries to prevent replay (5-minute tolerance).
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`, "utf8")
    .digest("base64");

  // The header may contain multiple space-separated signatures.
  return signatureHeader.split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    return (
      version === "v1" &&
      signature?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
  });
}

Verify against the raw request body bytes — re-serializing the parsed JSON can change key order or whitespace and break the signature.

Retries and idempotency

  • A delivery succeeds when your endpoint responds with a 2xx status within 10 seconds. Anything else (including timeouts) is retried.
  • Failed deliveries are retried up to 8 total attempts with increasing backoff: 30s, 2m, 10m, 30m, 2h, 6h, then 12h between attempts. After the final failure the delivery is dead-lettered and visible in the dashboard's delivery logs.
  • webhook-id is stable across retries of the same event. Process idempotently: record handled IDs and skip duplicates.
  • Endpoints that fail persistently may be disabled automatically; re-enable them from the dashboard once your receiver is healthy.
  • Respond quickly (enqueue work, then return 200) to stay inside the 10-second window.
Last updated June 11, 2026

Navigation