🪝

Webhooks

👨‍🍳 Chef

Real-time events

Webhooks allow services to notify you when something happens.


How they work

Event in external service
         ↓
POST to your endpoint
         ↓
Your code processes

Example: Stripe webhook

// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  const body = await request.text()
  const signature = request.headers.get('stripe-signature')!

  const event = stripe.webhooks.constructEvent(
    body,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET!
  )

  switch (event.type) {
    case 'payment_intent.succeeded':
      // Mark order as paid
      break
    case 'customer.subscription.deleted':
      // Cancel subscription
      break
  }

  return new Response('OK')
}

Verify signature

Always verify the webhook comes from the real service:

import crypto from 'crypto'

function verifySignature(payload: string, signature: string, secret: string) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

Services with webhooks

  • Stripe (payments)
  • GitHub (commits, PRs)
  • Slack (messages)
  • Twilio (SMS, calls)

🔐 Payment Webhooks: Critical Security

Payment webhooks are the most vulnerable point of your application. An attacker can simulate a successful payment.

Idempotency: Don't process twice

Stripe can resend the same webhook multiple times. Without idempotency, you charge/credit double.

export async function POST(request: Request) {
  const event = await verifyAndParseWebhook(request)

  // ⚠️ CRITICAL: Check if we already processed this event
  const processed = await db.query(
    'SELECT 1 FROM processed_webhooks WHERE event_id = $1',
    [event.id]
  )

  if (processed.rows.length > 0) {
    // Already processed, respond OK without doing anything
    return new Response('Already processed', { status: 200 })
  }

  // Process the event
  await processPaymentEvent(event)

  // Mark as processed AFTER successful processing
  await db.query(
    'INSERT INTO processed_webhooks (event_id, processed_at) VALUES ($1, NOW())',
    [event.id]
  )

  return new Response('OK')
}

Respond fast, process later

Stripe waits for response in 20 seconds. If your processing is slow, make it async:

export async function POST(request: Request) {
  const event = await verifyAndParseWebhook(request)

  // Respond immediately
  // Process in background (queue, worker, etc.)
  await queue.add('process-payment', { eventId: event.id })

  return new Response('OK') // < 1 second
}

Common Fintech Errors

ErrorConsequenceSolution
Not validating signatureFraud: fake paymentsconstructEvent() always
Not handling duplicatesDouble charge/creditIdempotency table
Timeout in processingStripe retries = duplicatesRespond fast, process async
Not logging eventsImpossible to debugFull log with timestamp

Practice

Webhook Receiver

Want to go further?

This is one piece. GenAI Builder is the whole map.

Ten domains, from the terminal to energy and law, five levels earned with real evidence and an AI mentor. At your own pace, in English and Spanish.

See the courseLifetime access · 30-day guarantee

© 2026 luxIA.us - All rights reserved

Created by Alann Reyes