Webhook verifier
Verify StableOps Webhook signatures using the raw body, handle secret rotation and timestamp tolerance, and choose verification methods for your runtime.
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
Wallet SDK
Use the StableOps wallet SDK to select payment instructions, connect injected or WalletConnect wallets, send EVM, Solana, and TRON transfers, and handle errors.
Checkout
Use StableOps Checkout to create one-time sessions and reusable payment links, customize branding, analyze conversion, and fulfill finalized payments safely.