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 POST that Off the Hook sends to your endpoint includes a fixed set of HTTP headers. Three of these — webhook-id, webhook-timestamp, and webhook-signature — are defined by the Standard Webhooks specification and are what the Svix verification libraries expect. The remaining headers are either standard HTTP conventions or Off the Hook extensions.

Headers

HeaderExample valueDescription
content-typeapplication/jsonAlways application/json — the body is always a JSON object
webhook-idevt_8xN9kP2QbA...Unique event ID; stays the same across every retry of the same event — use it for deduplication
webhook-timestamp1730000000Unix timestamp in seconds when the webhook was created
webhook-signaturev1,<base64>HMAC-SHA256 signature; multiple space-separated values appear during secret rotation
x-oth-subscription-idsub_2QkP9aB7xN...The subscription that triggered this delivery — useful for routing in a shared endpoint
user-agentOffTheHook/0.1Identifies Off the Hook as the sender

Signature format

The webhook-signature header contains one or more space-separated signature tokens, each prefixed with the version string v1,:
webhook-signature: v1,<base64-encoded-hmac-sha256>
During a secret rotation grace period, both the new and old secrets are active simultaneously. Off the Hook signs the payload with both secrets and includes both signatures in a single header value, new secret first:
webhook-signature: v1,<new-secret-sig> v1,<old-secret-sig>
Your verification code should accept a delivery if any of the provided signatures matches your current secret. The Svix library handles this automatically. The signature covers the string ${webhook-id}.${webhook-timestamp}.${body} — the event ID, timestamp, and raw request body concatenated with literal periods and HMAC-SHA256 signed with your subscription secret.

Deduplication

Off the Hook retries deliveries when your endpoint returns 5xx, 408, 429, or the connection fails entirely. Every retry of the same event uses the same webhook-id value. Store the IDs you have already successfully processed and skip any delivery that arrives with a duplicate ID, returning 2xx so the retry stops.

Timestamp validation

Reject any delivery where the webhook-timestamp is more than 5 minutes older than the current time. This defends against replay attacks where a valid captured request is re-sent to your endpoint later. The Svix verification library enforces this window automatically when you call wh.Verify().
// Svix enforces the 5-minute window for you
const wh = new Webhook(process.env.WEBHOOK_SECRET!);
const event = wh.verify(body, headers); // throws if timestamp is stale
For full signature verification examples in TypeScript, Go, Python, and other languages, see Verify Webhook Signatures.