Integration & APIs/
Lesson

Webhooks sit at a tricky intersection: the boilerplateWhat is boilerplate?Repetitive, standardized code that follows a known pattern and appears in nearly every project - like setting up a server or wiring up database connections. is tedious and repetitive, but the edge cases are subtle and dangerous. That makes them a perfect candidate for AI-assisted development, as long as you know which parts to trust and which to verify by hand.

What AI does well vs. poorly

AI pitfall
AI-generated webhook handlers almost always process the payload synchronously before responding. This is the single most common mistake, if your processing takes more than a few seconds, the sender times out and retries, creating the exact duplicates your idempotency logic is supposed to prevent.
AI does wellAI does poorly
Generating route handler boilerplateIdempotency logic (race conditions, dedup windows)
Signature verification code for known providersError recovery flows (what happens when step 3 of 5 fails?)
Event type switch/case structuresDistributed lock patterns (Redis SETNX, TTL choices)
Parsing and validating webhook payloadsRetry topology (exponential backoff tuning, dead letter queues)
Generating TypeScript types from webhook docsOrdering guarantees (handling out-of-order events)

The pattern is clear: AI handles the structural parts well but struggles with the behavioral parts, the things that only matter when something goes wrong.

02

Prompt templates

1. Generate a Stripe webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. handler with signature verification

Good to know
When asking AI to generate webhook handlers, always specify the provider (Stripe, GitHub, Twilio) by name. Each provider has different signature verification methods, header names, and retry behaviors. A generic "webhook handler" prompt produces generic code that misses provider-specific details like Stripe's requirement for a raw body.

Prompt:

Generate an Express.js webhook handler for Stripe with:
- Signature verification using stripe.webhooks.constructEvent
- A switch statement handling: payment_intent.succeeded,
  payment_intent.payment_failed, customer.subscription.deleted
- Raw body parsing (required for Stripe signature verification)
- TypeScript types for the request handler

What AI typically generates (and it is usually correct):

import express, { Request, Response } from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

// Important: Stripe needs the raw body for signature verification
app.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  async (req: Request, res: Response) => {
    const sig = req.headers['stripe-signature'] as string;

    let event: Stripe.Event;
    try {
      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    } catch (err) {
      console.error('Signature verification failed:', err);
      return res.status(400).send(`Webhook Error: ${(err as Error).message}`);
    }

    switch (event.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object as Stripe.PaymentIntent;
        await handlePaymentSuccess(paymentIntent);
        break;
      case 'payment_intent.payment_failed':
        const failedPayment = event.data.object as Stripe.PaymentIntent;
        await handlePaymentFailure(failedPayment);
        break;
      case 'customer.subscription.deleted':
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionCancelled(subscription);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    res.status(200).json({ received: true });
  }
);

This is solid scaffoldingWhat is scaffolding?Auto-generating the basic file structure and starter code for a project or feature so you don't have to write it from scratch.. The signature verification is correct, the raw body parsing is there (a common gotcha), and the event routing is clean. But notice what is missing: idempotency checks, async processing, and error handling inside each case branch. That is your job.

2. Review webhook code for idempotency issues

Prompt:

Review this webhook handler for idempotency issues. List every
scenario where duplicate processing could cause data corruption
or inconsistency:

[paste your webhook code here]

For each issue found, explain the failure scenario step by step
and suggest a fix.

AI is surprisingly good at this review task. It will typically catch:

  • Missing deduplication checks on event IDs
  • Non-idempotentWhat is idempotent?An operation that produces the same result whether you perform it once or multiple times, making retries safe. database operations (INSERT without ON CONFLICT)
  • Side effects that cannot be safely repeated (sending emails, charging cards)
  • Race conditions between duplicate requests arriving simultaneously

Where it falls short: it often suggests naive fixes like "use a Set to track processed events" (which does not survive a server restart) or forgets to account for the window between checking and marking an event as processed.

3. Design a retry strategy for failed webhook processing

Prompt:

Design a retry strategy for a webhook processing system with
these requirements:
- Webhook events are queued in a database table
- Failed events should retry with exponential backoff
- Max 5 retries over 24 hours
- After max retries, move to a dead letter queue for manual review
- Include the database schema and the worker process code
Use TypeScript and PostgreSQL.

Typical AI output (simplified):

// Database schema AI generates
interface WebhookEvent {
  id: string;
  event_type: string;
  payload: Record<string, unknown>;
  status: 'pending' | 'processing' | 'completed' | 'failed' | 'dead_letter';
  attempts: number;
  next_retry_at: Date | null;
  last_error: string | null;
  created_at: Date;
}

// Retry delay calculation
function getNextRetryDelay(attempt: number): number {
  // Exponential backoff: 1min, 5min, 25min, 2hr, 10hr
  const baseDelay = 60_000;
  return baseDelay * Math.pow(5, attempt);
}

// Worker that processes the queue
async function processWebhookQueue() {
  const event = await db.query(`
    UPDATE webhook_events
    SET status = 'processing'
    WHERE id = (
      SELECT id FROM webhook_events
      WHERE status = 'pending'
        AND (next_retry_at IS NULL OR next_retry_at <= NOW())
      ORDER BY created_at ASC
      LIMIT 1
      FOR UPDATE SKIP LOCKED
    )
    RETURNING *
  `);

  if (!event) return;

  try {
    await handleWebhookEvent(event);
    await db.query(
      `UPDATE webhook_events SET status = 'completed' WHERE id = CODE_BLOCK`,
      [event.id]
    );
  } catch (error) {
    const nextAttempt = event.attempts + 1;

    if (nextAttempt >= 5) {
      await db.query(
        `UPDATE webhook_events
         SET status = 'dead_letter', last_error = $2, attempts = $3
         WHERE id = CODE_BLOCK`,
        [event.id, (error as Error).message, nextAttempt]
      );
    } else {
      const retryAt = new Date(Date.now() + getNextRetryDelay(nextAttempt));
      await db.query(
        `UPDATE webhook_events
         SET status = 'pending', attempts = $2,
             next_retry_at = $3, last_error = $4
         WHERE id = CODE_BLOCK`,
        [event.id, nextAttempt, retryAt, (error as Error).message]
      );
    }
  }
}

This is a good starting point. The FOR UPDATE SKIP LOCKED pattern is correct for preventing duplicate processing. But you should verify: what happens if the worker crashes mid-processing? The event stays in processing status forever. You need a separate cleanup job that resets stale processing events, AI rarely adds that unprompted.

03

What to verify in AI-generated webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. code

Checklist itemWhat AI typically missesRisk if not fixed
Idempotency checkOften absent or uses in-memory SetDuplicate charges, double emails
Async processingProcesses before responding 200Timeout retries create more duplicates
Signature verificationSometimes present but wrong raw body handlingAnyone can send fake events
Granular error handlingSingle try/catch around everythingPartial failures leave inconsistent state
Stale processing cleanupNever includedWorker crash leaves events stuck forever

Every time AI generates webhook code for you, run through this checklist:

1. Missing idempotency
Does the handler check if this event was already processed? If not, duplicate webhooks will cause duplicate side effects (double charges, double emails, double inventory decrements).

2. Blocking processing
Does the handler do all its work before responding? If the processing takes more than a few seconds, the webhook sender will time out and retry, creating the very duplicates you are trying to avoid.

3. No signature verification
Is the handler verifying that the request actually came from the expected sender? Without this, anyone who discovers your webhook URL can send fake events.

4. Incomplete error handling
What happens when one step in a multi-step process fails? If you fulfill an order and then the email send fails, does the whole thing roll back? AI often generates a single try/catch around everything, which is rarely the right granularity.

Edge case
AI-generated tests for webhooks often test the happy path (valid signature, first delivery) but miss the critical edge cases: What happens when two identical events arrive 50ms apart? What if the event ID format changes? What if the event payload is valid JSON but missing a required field? Always add these tests manually.
04

The hybrid workflow

Here is the workflow that gets the best results when building webhooks with AI:

Step 1, AI generates the scaffold
Prompt AI for the route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes., signature verification, and event type routing. This saves 20-30 minutes of boilerplateWhat is boilerplate?Repetitive, standardized code that follows a known pattern and appears in nearly every project - like setting up a server or wiring up database connections. typing and gets the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually.-specific details right (raw body parsing, header names, verification methods).

Step 2, You add idempotency and error handling
This is where your engineering judgment matters. Add the deduplication check, decide on your idempotency storage (Redis vs. database), set the TTLWhat is ttl?Time-to-Live - a countdown attached to cached data that automatically expires it after a set number of seconds. window, and design the error handling granularity. These decisions depend on your specific system's failure modes.

Step 3, AI generates tests
This is where AI shines again. Prompt it with your final handler code and ask for test cases covering: duplicate events, invalid signatures, out-of-order events, partial processing failures, and concurrent delivery. AI is great at generating edge case tests because it can enumerate failure scenarios systematically.

// Example test AI might generate for your handler
describe('Stripe webhook handler', () => {
  it('should reject requests with invalid signatures', async () => {
    const res = await request(app)
      .post('/webhooks/stripe')
      .set('stripe-signature', 'invalid_sig')
      .send({ id: 'evt_123', type: 'payment_intent.succeeded' });

    expect(res.status).toBe(400);
  });

  it('should process duplicate events idempotently', async () => {
    const event = createTestEvent('payment_intent.succeeded');

    // First call: processes normally
    await request(app).post('/webhooks/stripe').send(event);
    // Second call: same event, should not duplicate
    await request(app).post('/webhooks/stripe').send(event);

    const orders = await db.orders.findAll({
      where: { paymentId: event.data.object.id }
    });
    expect(orders).toHaveLength(1); // Not 2!
  });

  it('should handle processing failures gracefully', async () => {
    mockPaymentService.rejectNext();

    const res = await request(app)
      .post('/webhooks/stripe')
      .send(createTestEvent('payment_intent.succeeded'));

    expect(res.status).toBe(200); // Still acknowledge receipt
    // Event should be queued for retry
    const pending = await db.webhookEvents.findPending();
    expect(pending).toHaveLength(1);
  });
});

The takeaway: let AI handle the repetitive structural code, keep the resilience logic in your own hands, then let AI help you test the whole thing. That combination gives you speed without sacrificing reliability.