Serverless webhook processor with retries, logs & background jobs
Your serverless webhook processor needs three things: verify the signature on the raw body, acknowledge within the provider timeout window, and continue heavy work in a background pipeline so retries never duplicate side effects. Inquir wraps all three in isolated container functions behind a gateway you control.
Last updated: 2026-04-20
- HMAC signature verification on raw request body before any parsing
- Fast 200 ACK inside Stripe, GitHub, Slack, and Shopify timeout windows
- Async handoff to pipelines or jobs for slow downstream work
- Idempotency keys and execution traces for duplicate deliveries
Answer first
Direct answer
Serverless webhook processor with retries, logs & background jobs. One function per provider keeps signature verification small, reviewable, and independent. The function verifies the HMAC on the raw body, writes an idempotency key to block duplicate processing, returns 200 immediately, and enqueues the heavy work to a pipeline.
When it fits
- You receive webhooks from Stripe, GitHub, Slack, Shopify, or any HMAC-signed provider and need verified, idempotent, async-safe processing.
- You want provider-specific functions isolated from customer-facing API routes.
Tradeoffs
- Running the full mutation synchronously before returning 200 means any downstream slowness—slow database, slow email API, network hiccup—looks like a failure to the provider and triggers a retry cascade.
- A single shared endpoint that handles Stripe, GitHub, and Slack mixes verification logic and scales dimensions you wanted separate.
Workload and what breaks
Why webhook processing is harder than it looks
SaaS providers retry aggressively. Stripe retries a failed webhook up to 72 hours; GitHub retries for 3 days; Slack expects a 3-second response or it marks the app as slow. If your handler runs a slow database mutation before returning, you will eventually see duplicated side effects—double charges, double emails, double records.
Skipping signature verification to ship faster is the most common shortcut. It feels harmless until someone replays a Stripe `payment_intent.succeeded` event against your production endpoint.
Trade-offs
Where inline webhook processing breaks
Running the full mutation synchronously before returning 200 means any downstream slowness—slow database, slow email API, network hiccup—looks like a failure to the provider and triggers a retry cascade.
A single shared endpoint that handles Stripe, GitHub, and Slack mixes verification logic and scales dimensions you wanted separate.
How Inquir helps
Verify, ACK, continue — the reliable webhook pattern
One function per provider keeps signature verification small, reviewable, and independent. The function verifies the HMAC on the raw body, writes an idempotency key to block duplicate processing, returns 200 immediately, and enqueues the heavy work to a pipeline.
Pipelines run in isolated containers with retries, step-level traces, and shared secrets — so your Stripe fulfillment logic has the same observability as your HTTP API endpoints, not a black-box background worker.
What you get
Webhook processor features
Raw body for signature verification
Inquir gateway passes body as a string so HMAC verification reads the exact bytes the provider signed — no JSON.parse drift before verification.
Isolated per-provider functions
Map each provider to its own function and route. Stripe logic never touches GitHub handlers; security reviews scope to one small file.
Async handoff to pipelines
Return 200 fast; enqueue fulfillment, email, or data sync to a pipeline step that runs outside the HTTP window without blocking retries.
Idempotency and execution traces
Write event IDs before mutations. Traces in the console show body (redacted), headers, timing, and retry count per delivery.
What to do next
How to build a serverless webhook processor on Inquir
Verify on the raw body, ACK before the timeout, apply writes idempotently.
Verify signature on raw body
Read event.body as-is before any JSON.parse. Compute expected HMAC and compare with timing-safe equality to reject forgeries.
Write idempotency key, return 200
Upsert the provider event ID before any state mutation. Return 200 within the provider window—Stripe expects < 30s, Slack < 3s.
Enqueue slow work to pipeline
For any work that outlasts the timeout window, call global.durable.startNew() and return the job reference. The orchestration retries independently of the HTTP response.
Code example
Stripe and GitHub webhook handlers
One function per provider. body comes in as a string — never parse before verifying. Use timing-safe comparison to resist timing attacks on HMAC checks.
export async function handler(event) { const rawBody = event.body ?? ''; // Stripe signs the raw bytes — never parse before verifying const sig = event.headers['stripe-signature'] ?? ''; if (!stripe.webhooks.verifySignature(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET)) { return { statusCode: 400, body: 'invalid signature' }; } const evt = JSON.parse(rawBody); // Block duplicate delivery before any side effects const isNew = await db.upsertWebhookEvent(evt.id, evt.type); if (!isNew) return { statusCode: 200, body: 'duplicate' }; // Return fast; continue in pipeline await global.durable.startNew('stripe-fulfillment', undefined, { eventId: evt.id, type: evt.type, data: evt.data.object }); return { statusCode: 200, body: 'accepted' }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sigHeader = (event.headers['x-hub-signature-256'] ?? '').replace('sha256=', ''); const expected = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(body).digest('hex'); if (!timingSafeEqual(Buffer.from(sigHeader, 'hex'), Buffer.from(expected, 'hex'))) { return { statusCode: 401, body: 'invalid signature' }; } const eventType = event.headers['x-github-event']; const payload = JSON.parse(body); if (eventType === 'push') { await global.durable.startNew('index-repo', undefined, { repo: payload.repository.full_name, sha: payload.after }); } return { statusCode: 200, body: 'accepted' }; }
When it fits
Good fit
When this works
- You receive webhooks from Stripe, GitHub, Slack, Shopify, or any HMAC-signed provider and need verified, idempotent, async-safe processing.
- You want provider-specific functions isolated from customer-facing API routes.
When to skip it
- You only consume webhooks through a SaaS iPaaS like Zapier or Workato and never touch the runtime.
FAQ
FAQ
Why must I verify before parsing?
HMAC is computed over the raw bytes as received. If you JSON.parse first and re-serialize, whitespace and key order can differ, causing verification to fail on valid events. Always read event.body as a string before calling any HMAC function.
How do I handle Slack's 3-second timeout?
Return immediately with 200 and an empty body. Enqueue the actual work to a pipeline step. If you need to post a response back to Slack, use the responseUrl from the slash-command payload inside the pipeline step.
What about replay attacks?
Combine HMAC verification, provider-supplied timestamps (check within ±5 minutes), and idempotency keys so late replays are rejected and duplicate deliveries are no-ops.
Can I test locally?
Use the Stripe CLI `stripe listen --forward-to` or similar tools to forward real webhook events to a local handler. Keep the same event.body string contract as the gateway delivers in production.