Webhook Signature Verifier
Verify HMAC-SHA256 webhook signatures with timing-safe comparison at the edge - Stripe/GitHub-style webhook auth
Accept a raw payload, secret, signature header value, and algorithm. Computes the expected HMAC (Stripe/GitHub sha256= hex style) and returns whether it matches using a timing-safe compare.
Features
- POST /verify - HMAC-SHA256 verification with clear comparison explanation
- Supports prefixed signatures (
sha256=...) and raw hex - Uses Web Crypto at the edge; no external libraries
API Reference
POST /verify
payload string (required) - Raw request body as received.
secret string (required) - Shared signing secret.
signature string (required) - Signature from the webhook header.
algorithm string (optional) - Default sha256.
Example Request
curl -X POST "https://your-worker.workers.dev/verify" \
-H "Content-Type: application/json" \
-d '{"payload":"{\"id\":1}","secret":"whsec_test","signature":"sha256=..."}'Success Response
{
"match": true,
"algorithm": "sha256",
"expectedSignature": "sha256=...",
"providedSignature": "sha256=...",
"explanation": "Timing-safe comparison of 32-byte HMAC digests returned match."
}Error Codes
400-INVALID_BODY,MISSING_FIELD,INVALID_ALGORITHM
Use Cases
- Debug Stripe, GitHub, or Shopify webhook signature mismatches
- Learn timing-safe HMAC verification patterns for Workers
- Validate signing logic before wiring production webhook handlers
Limitations
- HMAC-SHA256 only in this experiment
- Secrets are sent in the request body; use only for debugging, not production traffic
- No replay protection or timestamp validation
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/webhook-signature-verifier.
/** Allowed URL schemes for optional webhook URL validation helpers. */export const ALLOWED_SCHEMES = ["http:", "https:"] as const;/** * Validates and normalizes a URL. */export function validateUrl(input: string | undefined): string | null { if (!input || typeof input !== "string") return null; const trimmed = input.trim(); if (!trimmed) return null; try { const url = new URL(trimmed); if (!ALLOWED_SCHEMES.includes(url.protocol as (typeof ALLOWED_SCHEMES)[number])) { return null; } return url.href; } catch { return null; }}Deployment
Configure bindings
See wrangler.json and the experiment README for required bindings.
Test your deployment
See the experiment README for curl examples.
Local Development
cd apps/experiments/webhook-signature-verifier
npm install
npm run devConfiguration
No bindings required beyond the Workers runtime.