Webhooks

Verfying Webhooks

Slate will notify you of important events via webhooks. You will be able to configure your endpoints ant the suscribed events in the dashboard.

All webhook events are delivered using Svix and each request is cryptographically signed. You must verify signatures to ensure requests are authentic. Failing to verify webhook signatures may expose your system to spoofed or replayed requests.

Overview

Each webhook call includes three headers with additional information that are used for verification:

Header Description
svix-id The unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent
svix-timestamp Unix timestamp (seconds)
svix-signature Base64 encoded list of signatures (space delimited)

Each webhook endpoint has a signing secret, which looks like: whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXX. You can get it from your webhook information on the dashboard. .

How Signatures Are Generated

Webhooks are signed using:

  • Algorithm: HMAC
  • Hash: SHA-256
  • Encoding: Base64

The signature is computed over the following string: ${svix_id}.${svix_timestamp}.${body};

The HMAC key is the Base64-decoded portion of your signing secret
(the part after the whsec_ prefix).

Signature Verification Steps

1. Read Required Headers

From the incoming request, extract:

svix-id
svix-timestamp
svix-signature

Also read the raw request body exactly as received.

⚠️ The body must not be modified (no JSON reformatting, whitespace changes, etc.).

2. Construct the Signed Content

{svix-id}.{svix-timestamp}.{raw_body}

Example:

msg_2N3R9J... . 1704839123 . {"event":"payment.completed","data":{...}}

3. Decode the Signing Secret

If your secret is:

whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw

Use only the part after whsec_ and Base64-decode it.

4. Compute the Expected Signature

Generate an HMAC-SHA256 signature and Base64-encode it.

5. Compare Against the svix-signature Header

The svix-signature header may contain multiple signatures, for example:

v1,abc123= v1,def456=

Rules:

  • Split by spaces.
  • Remove the version prefix (v1,).
  • Compare each signature.
  • If any signature matches, the webhook is valid.

6. Use Constant-Time Comparison

Always compare signatures using a constant-time comparison to prevent timing attacks.

Node.js Verification Example

import crypto from "crypto";

function verifyWebhook({
  payload,
  headers,
  signingSecret,
}) {
  const svixId = headers["svix-id"];
  const svixTimestamp = headers["svix-timestamp"];
  const svixSignature = headers["svix-signature"];

  if (!svixId || !svixTimestamp || !svixSignature) {
    throw new Error("Missing Svix headers");
  }

  const signedContent = `${svixId}.${svixTimestamp}.${payload}`;

  // Remove "whsec_" prefix and base64-decode the secret
  const secret = signingSecret.split("_")[1];
  const secretBytes = Buffer.from(secret, "base64");

  const expectedSignature = crypto
    .createHmac("sha256", secretBytes)
    .update(signedContent)
    .digest("base64");

  // svix-signature can contain multiple signatures
  const signatures = svixSignature.split(" ");

  for (const sig of signatures) {
    const [, signature] = sig.split(",");
    if (
      crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      )
    ) {
      return true;
    }
  }

  return false;
}

Resources