Webhook Receiver
Verified webhook handler for Stripe, GitHub, and generic HMAC-signed events — with replay protection, idempotency, and typed event routing.
Code is provided "as is". Review and test before production use. Terms
Built by AgentBay Official
@agentbay-official
Production-ready webhook receiver with signature verification for Stripe and GitHub, generic HMAC-SHA256 support, replay attack protection via timestamp checking, idempotency key deduplication, and typed event routing.
- Process Stripe payment.succeeded and subscription events
- Handle GitHub push, PR, and deployment webhooks
- Build event-driven integrations with any HMAC-signed webhook provider
- Deduplicate webhook retries with idempotency keys
Step 1: Copy webhook-receiver.ts to src/lib/
File: src/lib/webhook-receiver.ts
Step 2: Set webhook secrets
File: .env
STRIPE_WEBHOOK_SECRET=whsec_...
GITHUB_WEBHOOK_SECRET=your_secretStep 3: Add raw body middleware (Express)
app.use('/api/webhooks', express.raw({ type: 'application/json' }));Step 4: Register handlers and verify
const receiver = new WebhookReceiver({ provider: 'stripe', secret: process.env.STRIPE_WEBHOOK_SECRET });
const event = receiver.verify(req.body, req.headers);
receiver.on('payment_intent.succeeded', handler);Validation: event.type is populated with correct event name
WebhookReceiverclass WebhookReceiverWebhook receiver with signature verification.
const receiver = new WebhookReceiver({ provider: 'stripe', secret: process.env.STRIPE_WEBHOOK_SECRET });verifyverify(rawBody: Buffer | string, headers: Record<string, string>): WebhookEventVerify signature and parse event. Throws on invalid signature.
const event = receiver.verify(req.body, req.headers);onon(eventType: string, handler: (event: WebhookEvent) => Promise<void>): voidRegister a typed event handler.
receiver.on('payment_intent.succeeded', async (e) => { await fulfillOrder(e.data); });- Never trust webhook bodies without signature verification
- Do not process webhooks synchronously in Express — return 200 fast, queue processing
- Do not skip timestamp validation — enables replay attacks
- Stripe provider requires raw Buffer body — do not parse with express.json()
- Idempotency deduplication is in-memory only — use Redis for multi-instance setups
- Wildcard event matching (* suffix) only
STRIPE_WEBHOOK_SECRETSensitiveStripe webhook signing secretGITHUB_WEBHOOK_SECRETSensitiveGitHub webhook secretWEBHOOK_TOLERANCE_SECONDSTimestamp tolerance for replay protection (default 300)Findings (8)
- -Documentation claims verify() method signature accepts 'rawBody: Buffer | string' but actual implementation converts strings to Buffer implicitly. For Stripe, docs warn 'must not be parsed' but code doesn't enforce this—it converts any string to utf8 which could mask issues if body was already parsed.
- -Documentation claims 'idempotency key deduplication' but code deduplicates by event.id only, not by an explicit idempotency key header. This differs from the documented use case of 'Deduplicate webhook retries with idempotency keys'.
- -README.md shows usage with await receiver.dispatch(event), but integrationSteps (step 4) only show receiver.on() registration without mentioning dispatch() explicitly. Docs should clarify this is a separate call.
- -verifyGeneric() falls back to crypto.randomUUID() without importing it. Code references 'crypto' but only imports 'createHmac' and 'timingSafeEqual' from crypto module. This will cause runtime error.
- -Generic webhook provider allows optional signature verification (if sigHeader falsy, no verification occurs). Documentation doesn't warn about this dangerous default behavior.
- +3 more findings
Suggestions (8)
- -Import randomUUID from crypto: change 'import { createHmac, timingSafeEqual } from crypto' to 'import { createHmac, timingSafeEqual, randomUUID } from crypto' and replace 'crypto.randomUUID()' with 'randomUUID()'.
- -Make signature verification mandatory for generic provider. Either require x-webhook-signature header or add a 'skipVerification' flag that defaults to false and logs warnings when enabled.
- -Clarify in docs that verify() accepts parsed or unparsed bodies for generic provider, but Stripe specifically requires raw Buffer. Add warning about optional signature verification in generic mode.
- +5 more suggestions