Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.offthehook.dev/llms.txt

Use this file to discover all available pages before exploring further.

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.

Signature headers

Each delivery includes three headers that you use together to verify the signature:
HeaderExampleDescription
webhook-idevt_8xN9kP2QbA...The event ID; stays the same across retries
webhook-timestamp1730000000Unix timestamp as a string
webhook-signaturev1,<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:
1

Construct the signed content

Concatenate the three values with . as the separator:
{webhook-id}.{webhook-timestamp}.{raw body}
2

Decode the secret

Your secret starts with whsec_. Base64-decode the part after whsec_ to get the raw key bytes.
3

Compute the HMAC

Compute HMAC-SHA256 over the signed content string using the raw key bytes.
4

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.