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
- [Verifying Webhooks Manually][https://docs.svix.com/receiving/verifying-payloads/how-manual]
- [How to Verify Webhooks with the Svix Libraries][https://docs.svix.com/receiving/verifying-payloads/how]