🪝

Webhooks

👨‍🍳 Chef

Eventos en tiempo real

Los webhooks permiten que servicios te notifiquen cuando algo pasa.


Cómo funcionan

Evento en servicio externo
         ↓
POST a tu endpoint
         ↓
Tu código procesa

Ejemplo: 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':
      // Marcar orden como pagada
      break
    case 'customer.subscription.deleted':
      // Cancelar suscripción
      break
  }

  return new Response('OK')
}

Verificar firma

Siempre verifica que el webhook viene del servicio real:

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)
  )
}

Servicios con webhooks

  • Stripe (pagos)
  • GitHub (commits, PRs)
  • Slack (mensajes)
  • Twilio (SMS, llamadas)

🔐 Webhooks de Pago: Seguridad Crítica

Los webhooks de pago son el punto más vulnerable de tu aplicación. Un atacante puede simular un pago exitoso.

Idempotencia: No proceses dos veces

Stripe puede reenviar el mismo webhook múltiples veces. Sin idempotencia, cobras/acreditas doble.

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

  // ⚠️ CRÍTICO: Verificar si ya procesamos este evento
  const processed = await db.query(
    'SELECT 1 FROM processed_webhooks WHERE event_id = $1',
    [event.id]
  )

  if (processed.rows.length > 0) {
    // Ya procesado, responder OK sin hacer nada
    return new Response('Already processed', { status: 200 })
  }

  // Procesar el evento
  await processPaymentEvent(event)

  // Marcar como procesado DESPUÉS de procesar exitosamente
  await db.query(
    'INSERT INTO processed_webhooks (event_id, processed_at) VALUES ($1, NOW())',
    [event.id]
  )

  return new Response('OK')
}

Responde rápido, procesa después

Stripe espera respuesta en 20 segundos. Si tu procesamiento es lento, hazlo async:

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

  // Responder inmediatamente
  // Procesar en background (cola, worker, etc.)
  await queue.add('process-payment', { eventId: event.id })

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

Errores comunes en Fintech

ErrorConsecuenciaSolución
No validar firmaFraude: pagos falsosconstructEvent() siempre
No manejar duplicadosCobro/acreditación dobleTabla de idempotencia
Timeout en procesamientoStripe reintenta = duplicadosResponder rápido, procesar async
No loguear eventosImposible debuggearLog completo con timestamp

Practica

Receptor de Webhooks