Every webhook delivery Off the Hook sends is signed with HMAC-SHA256. Verifying the signature before processing an event confirms the delivery came from Off the Hook and that the payload has not been tampered with in transit. Skipping this step makes your endpoint vulnerable to spoofed requests.
Each delivery includes three headers that you use together to verify the signature:
| Header | Example | Description |
|---|
webhook-id | evt_8xN9kP2QbA... | The event ID; stays the same across retries |
webhook-timestamp | 1730000000 | Unix timestamp as a string |
webhook-signature | v1,<base64-hmac-sha256> | One or more space-separated signatures |
Always verify signatures against the raw request body before parsing it as JSON. Any modification to the body — even reformatting whitespace — invalidates the signature.
Verify with the Svix library
Off the Hook implements the Standard Webhooks specification, so the Svix verification library works without modification. Your signing secret starts with whsec_ — pass it as-is; the library handles base64 decoding internally.
import { Webhook } from "svix";
const wh = new Webhook(process.env.WEBHOOK_SECRET!);
try {
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"] as string,
"webhook-timestamp": req.headers["webhook-timestamp"] as string,
"webhook-signature": req.headers["webhook-signature"] as string,
});
console.log("Verified event:", event.kind);
} catch (err) {
console.error("Invalid signature:", err);
res.status(400).send("Bad signature");
}
Svix libraries are available for Python, Ruby, PHP, Java, Rust, and more. See the full list at github.com/svix/svix-webhooks.
Secret rotation and multiple signatures
During a secret rotation grace period, the webhook-signature header contains multiple space-separated v1, values — one computed with the new secret and one with the old. The Svix library accepts any valid signature in the header automatically, so no code changes are required during a rotation.
webhook-signature: v1,<base64-new-secret> v1,<base64-old-secret>
See Rotate Webhook Signing Secrets for details on managing secret rotation.
Manual verification
If no Svix library is available for your language, you can implement verification directly:
Construct the signed content
Concatenate the three values with . as the separator:{webhook-id}.{webhook-timestamp}.{raw body}
Decode the secret
Your secret starts with whsec_. Base64-decode the part after whsec_ to get the raw key bytes.
Compute the HMAC
Compute HMAC-SHA256 over the signed content string using the raw key bytes.
Encode and compare
Base64-encode the resulting bytes. Compare the result against each v1,<value> entry in the webhook-signature header using a constant-time comparison to prevent timing attacks. The delivery is valid if any entry matches.