$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Best Practices

AI Rules for Webhook Handling

AI processes webhooks synchronously without signature verification or idempotency. Rules for signature validation, async processing, idempotent handlers, and retry handling.

7 min read·May 26, 2025

A webhook endpoint without signature verification accepts forged events from anyone

Signature verification, idempotent processing, async queuing, and retry-safe responses

How AI Handles Webhooks (Insecure, Fragile, and Slow)

Webhooks are HTTP callbacks from external services — Stripe sends payment events, GitHub sends push events, Twilio sends message events. AI generates webhook handlers with three critical problems: no signature verification (anyone can POST fake events to your endpoint), synchronous processing (the sender times out while you process), and no idempotency (duplicate deliveries create duplicate side effects — double charges, double notifications).

A webhook endpoint without signature verification is an open attack surface: an attacker sends a fake payment.succeeded event, your system grants access or ships the product without actual payment. Signature verification is not optional — it is the authentication layer for webhooks.

These rules apply to any webhook: payment providers (Stripe, PayPal), communication (Twilio, SendGrid), version control (GitHub, GitLab), and any service that sends HTTP callbacks.

Rule 1: Verify Every Webhook Signature

The rule: 'Verify the webhook signature before processing any event. Most services use HMAC-SHA256: compute HMAC of the raw request body using your webhook secret, compare to the signature in the header. For Stripe: stripe.webhooks.constructEvent(rawBody, sig, secret). For GitHub: compare X-Hub-Signature-256 header. For generic HMAC: crypto.timingSafeEqual(computedHmac, receivedHmac). Return 401 if verification fails.'

For the raw body: 'Signature verification requires the raw request body — not parsed JSON. If your framework parses the body before your handler, the signature check fails. Express: app.use("/webhooks", express.raw({ type: "application/json" })). Next.js App Router: const body = await request.text(). Always access the raw body for signature computation.'

AI generates: app.post("/webhook", (req, res) => { const event = req.body; processEvent(event); res.status(200).end(); }). No signature check — any HTTP client can send fake events. One verification step prevents the entire class of webhook forgery attacks.

  • HMAC-SHA256: hash raw body with secret, compare to signature header
  • Raw body required — not parsed JSON — configure middleware per route
  • crypto.timingSafeEqual — never === for signature comparison (timing attack)
  • Return 401 on verification failure — never process unverified events
  • Each provider has specific header names: X-Hub-Signature-256, Stripe-Signature, etc.
⚠️ Fake Events = Real Damage

Without signature verification, anyone can POST fake payment.succeeded events. Your system grants access, ships products, or credits accounts — all from forged requests. HMAC verification is webhook authentication.

Rule 2: Idempotent Event Processing

The rule: 'Webhook deliveries can be duplicated — the sender retries on timeout, network error, or non-2xx response. Your handler must be idempotent: processing the same event twice produces the same result as processing it once. Store processed event IDs: before processing, check if event.id exists in your processed_events table. If yes, return 200 (already handled). If no, process and store the ID.'

For the pattern: 'const alreadyProcessed = await db.processedEvents.findUnique({ where: { eventId: event.id } }); if (alreadyProcessed) return res.status(200).json({ status: "already_processed" }); await processEvent(event); await db.processedEvents.create({ data: { eventId: event.id, processedAt: new Date() } }); return res.status(200).end();'

AI processes every webhook delivery as if it is the first — duplicate delivery creates: double database records, double email notifications, double charges, or double inventory deductions. One table of processed event IDs prevents all duplication.

💡 One Table Prevents All Duplication

processed_events table with event.id. Check before processing, store after. Duplicate delivery? Already in the table → skip. One lookup prevents: double charges, double emails, double database records.

Rule 3: Return 200 Fast — Process Asynchronously

The rule: 'Return 200 within 5 seconds — most webhook senders timeout at 5-30 seconds and retry. Verify the signature, enqueue the event for background processing, and return 200 immediately. The background worker handles: database updates, email sending, external API calls, and any processing that takes more than 1 second. Never do heavy processing in the webhook handler.'

For the pattern: 'app.post("/webhook", async (req, res) => { const event = verifySignature(req); // <1ms, await eventQueue.add(event); // <10ms, res.status(200).json({ received: true }); // immediate }). The queue worker processes at its own pace — no timeout pressure. If the worker fails, it retries — without triggering a webhook retry (which creates duplicates).'

AI processes the entire webhook synchronously: verify → parse → query DB → update records → send email → return 200. If any step takes more than 5 seconds, the sender retries — creating a duplicate delivery that your non-idempotent handler processes again. Fast 200 + async processing eliminates both problems.

  • Return 200 within 5 seconds — sender retries on timeout
  • Verify signature → enqueue → return 200 — three fast steps
  • Background worker: DB updates, emails, API calls — no timeout pressure
  • Worker failures retry without triggering webhook retry — no duplication
  • If you cannot queue: at minimum, return 200 before doing heavy work
ℹ️ 200 in <5s

Webhook senders timeout at 5-30s and retry. Verify → enqueue → return 200. Three steps, all under 100ms. The background worker handles the real work — no timeout pressure, no retry storms.

Rule 4: Retry-Safe Error Handling

The rule: 'Return 200 for: successfully processed events AND events you choose to ignore (unrecognized event types). Return 4xx for: invalid signature (401), malformed payload (400). Return 5xx ONLY for transient errors that should trigger a retry. Most senders retry on 5xx and non-2xx — use return codes intentionally.'

For unrecognized events: 'Webhook senders add new event types over time. Your handler receives events you did not anticipate. Handle gracefully: switch (event.type) { case "payment.succeeded": ...; case "payment.failed": ...; default: return res.status(200).json({ status: "unhandled_event_type" }); }. Never return 4xx/5xx for unknown event types — it triggers retries for events you will never handle.'

For error scenarios: 'Signature invalid → 401 (do not process, do not retry). Payload malformed → 400 (sender has a bug, retrying is pointless). Your database is down → 500 (transient — sender should retry, and your idempotency check prevents duplicates). Business logic error → 200 + log (your problem, not the sender problem — do not trigger retry).'

Rule 5: Event Logging and Monitoring

The rule: 'Log every webhook event: received, verified, processed, failed. Include: event.id, event.type, source, timestamp, processing_duration_ms, and outcome (processed, duplicate, ignored, failed). Store raw events in a webhook_events table for debugging: { id, source, event_type, payload (JSON), received_at, processed_at, status }. This table is your audit trail — when something goes wrong, you can replay events.'

For monitoring: 'Alert on: webhook processing failures > threshold (your handler is broken), webhook volume drop > 50% (the sender stopped sending — integration broken), and processing latency > SLA. Monitor per source: Stripe webhook health is independent of GitHub webhook health. Dashboard: event volume, success rate, latency distribution, and failed events.'

For replay: 'Store raw payloads so you can replay events when: you fix a bug in your handler (reprocess failed events), you add handling for a new event type (reprocess previously ignored events), or you need to audit what happened (compliance, debugging). Replay from the webhook_events table — the raw payload is the source of truth.'

  • Log every event: id, type, source, outcome, duration — structured JSON
  • webhook_events table: raw payload, status, timestamps — audit trail
  • Alert: processing failures, volume drop, latency — per source
  • Replay from stored payloads: fix bugs, add handlers, audit history
  • Retention: 30 days minimum — longer for compliance-regulated events

Complete Webhook Rules Template

Consolidated rules for webhook handling.

  • Verify signature first: HMAC-SHA256 with raw body — 401 on failure — never skip
  • Idempotent: check event.id in processed_events — skip duplicates — store after processing
  • Return 200 fast: verify → enqueue → respond in <5s — heavy work in background worker
  • 200 for processed + ignored events — 4xx for invalid — 5xx only for transient (triggers retry)
  • Graceful unknown events: return 200 for unrecognized types — never 4xx/5xx
  • Log every event: id, type, outcome, duration — webhook_events table for audit trail
  • Monitor: failures, volume drop, latency — per source — alert on anomalies
  • Replay capability: stored raw payloads — reprocess on bug fix or new handler