StableOps
SDKs

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:

ReasonWhat it means
missing_headerNo X-Product-Signature was present.
invalid_formatHeader didn't match t=…,v1=….
timestamp_expiredOutside the 5-minute window.
bad_signatureHMAC 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

On this page