Skip to main content

Verifying Webhook Signatures

Every webhook delivery is signed, so you can confirm it actually came from CELITECH before acting on it. Never process a webhook payload without verifying its signature first.

Your Signing Secret

Each endpoint has its own signing secret, available under Developers → Webhooks in the dashboard. Treat it like any other credential — keep it out of client-side code, logs, and version control. If a secret is ever compromised, rotate it from the same page — your endpoint should always verify using its current secret.

Request Headers

Every delivery includes the following headers:

HeaderDescription
svix-idA unique identifier for this delivery. Every retry attempt of the same delivery carries this same identifier — use it to de-duplicate if you receive it more than once.
svix-timestampThe Unix timestamp (in seconds) the delivery was sent.
svix-signatureOne or more space-delimited signatures for this delivery, used to verify authenticity.

Verifying the Signature

  1. Your signing secret has a whsec_ prefix — strip it, then base64-decode the remainder to get the raw key bytes. Don't use the whsec_... string itself as the key.
  2. Concatenate the svix-id, svix-timestamp, and the raw request body, separated by periods (.).
  3. Compute an HMAC-SHA256 of that string using the decoded key from step 1, then base64-encode the result.
  4. The svix-signature header contains one or more space-delimited entries, each formatted as v1,<base64 signature> (the v1 is a version tag, and multiple entries can appear during a secret rotation). Compare your computed signature against the value after v1, in each entry, using a constant-time comparison (not a plain ===/string-equality check) to avoid leaking timing information — if any one matches, the request is authentic.

Also reject any request whose svix-timestamp is too far in the past or future (a tolerance of a few minutes is reasonable), to protect against replay attacks.

warning

Always compute the signature over the raw, unparsed request body. Re-serializing a parsed JSON object before verifying can produce a different byte sequence and cause verification to fail incorrectly.

Responding to Webhook Deliveries

  • Respond with a 2xx status code as soon as you've accepted the delivery — do the minimal work needed (verify the signature, enqueue for processing) before responding. See Delivery & Retries for what happens otherwise.
  • Process deliveries idempotently. Retries can result in the same event being delivered more than once. Use the svix-id header as a de-duplication key — if you've already processed a delivery with that identifier, skip it. See the Event Catalog for per-event delivery notes.

Example Verification Code

const crypto = require('crypto');

function verifyWebhookSignature(payload, headers, secret) {
const id = headers['svix-id'];
const timestamp = headers['svix-timestamp'];
const signatureHeader = headers['svix-signature'];

// Secrets are formatted as `whsec_<base64>` — strip the prefix and
// base64-decode the rest to get the raw HMAC key.
const secretBytes = Buffer.from(secret.split('_')[1], 'base64');

const signedContent = `${id}.${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secretBytes)
.update(signedContent)
.digest('base64');

// Header looks like "v1,<sig1> v1,<sig2>" — one or more space-delimited
// entries, each prefixed with a version tag.
const receivedSignatures = signatureHeader.split(' ').map((entry) => entry.split(',')[1]);
const expectedBuffer = Buffer.from(expectedSignature);

// Use a constant-time comparison so verification doesn't leak timing
// information about how much of the signature matched.
return receivedSignatures.some((sig) => {
const receivedBuffer = Buffer.from(sig);
return (
receivedBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
);
});
}

Testing Your Integration

Developers → Webhooks in the dashboard includes a way to send an example event to your endpoint, so you can test your signature verification and handling logic before relying on it in production.

note

Failed example-event deliveries sent this way are not retried, unlike real deliveries — see Delivery & Retries.

Send example event feature in the Webhooks dashboard