Webhook verifier
Verify webhook signatures across runtimes.
What gets signed
Every delivery carries three headers:
X-Product-Signature: t=1780301011,v1=7b11b2c1…
X-Event-Id: evt_01JYA…
X-Delivery-Id: del_01JYA…The signature is HMAC-SHA256(secret, "${timestamp}.${rawBody}"), hex-encoded.
The 5-minute timestamp window guards against replay; mismatched signatures
fail in constant time.
Verify in your handler
The SDK exports the verifier directly so framework users don't need an extra package:
import { verifySignature, SIGNATURE_HEADER } from '@stableops/api-sdk/webhooks'
export async function POST(req: Request) {
// Always verify against the *raw* body. Don't `JSON.parse` first — even
// re-stringifying the same object can re-order keys.
const rawBody = await req.text()
const result = verifySignature({
secrets: [process.env.STABLEOPS_WEBHOOK_SECRET!],
header: req.headers.get(SIGNATURE_HEADER.toLowerCase()) ?? undefined,
rawBody,
})
if (!result.ok) {
return new Response(JSON.stringify({ reason: result.reason }), {
status: 400,
})
}
const event = JSON.parse(rawBody)
// … your business logic, idempotent on event.id …
return new Response('ok')
}Rotating secrets
When you rotate, the API returns the new secret immediately and keeps the
previous one valid for 24 hours. During rollout, pass both secrets to
verifySignature so deliveries signed with either will pass:
verifySignature({
secrets: [
process.env.STABLEOPS_WEBHOOK_SECRET!,
process.env.STABLEOPS_WEBHOOK_SECRET_PREVIOUS!,
],
header,
rawBody,
})Failure reasons
verifySignature returns a discriminated union — when ok: false,
inspect reason:
| Reason | What it means |
|---|---|
missing_header | No X-Product-Signature was present. |
invalid_format | Header didn't match t=…,v1=…. |
timestamp_expired | Outside the 5-minute window. |
bad_signature | HMAC didn't match any of the provided secrets. |
Replay protection
Verify the signature, then dedupe on X-Event-Id (preferred) or your own
business key. Network retries plus the platform's retry schedule mean the
same event can land more than once — your handler must be idempotent.
Edge / serverless runtimes
verifySignature uses createHmac, timingSafeEqual, and Buffer
from node:crypto. Edge runtimes support node:crypto natively or via a
compatibility layer: Deno and Vercel work out of the box with no extra
polyfills; Cloudflare Workers requires the nodejs_compat compatibility
flag in wrangler.toml.
How is this guide?
Last updated