Cómo Diseñar Webhooks Fiables y Eficaces

Ashley Innocent

Ashley Innocent

13 March 2026

Cómo Diseñar Webhooks Fiables y Eficaces

TL;DR

Diseña webhooks confiables con reintentos de retroceso exponencial (5-10 intentos), claves de idempotencia, verificación de firma HMAC y tiempos de espera de 5 segundos. Devuelve 2xx inmediatamente, procesa asincrónicamente. Modern PetstoreAPI implementa webhooks para actualizaciones de pedidos, adopciones de mascotas y notificaciones de pago con reintentos y seguridad completos.

Introducción

Envías un webhook para notificar a un cliente que su mascota fue adoptada. El servidor del cliente está caído. Tu webhook falla. ¿Lo reintentas? ¿Cuántas veces? ¿Qué pasa si el cliente recibe el webhook dos veces y cobra al cliente dos veces?

Los webhooks son devoluciones de llamada HTTP que envían eventos a las URL de los clientes. Son simples en teoría, pero complejos en la práctica. Las redes fallan, los servidores se caen y los clientes tienen errores. Los webhooks de producción necesitan lógica de reintento, idempotencia, seguridad y monitoreo.

Modern PetstoreAPI implementa webhooks listos para producción para actualizaciones de pedidos, adopciones de mascotas y notificaciones de pago. Cada webhook incluye lógica de reintento, verificación de firma e idempotencia.

💡
Si estás construyendo o probando webhooks, Apidog te ayuda a probar la entrega de webhooks, validar firmas y simular escenarios de fallo. Puedes probar la lógica de reintento y verificar el manejo de la idempotencia.
button

En esta guía, aprenderás a diseñar webhooks confiables utilizando los patrones de Modern PetstoreAPI.

Conceptos básicos de los Webhooks

Los webhooks son solicitudes HTTP POST enviadas a URL proporcionadas por el cliente cuando ocurren eventos.

Cómo funcionan los Webhooks

1. El cliente registra la URL del webhook:

POST /webhooks
{
  "url": "https://client.com/webhooks/petstore",
  "events": ["pet.adopted", "order.completed"]
}

2. Ocurre el evento (mascota adoptada)

3. El servidor envía el webhook:

POST https://client.com/webhooks/petstore
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...

{
  "event": "pet.adopted",
  "data": {
    "petId": "019b4132",
    "userId": "user-456",
    "timestamp": "2026-03-13T10:30:00Z"
  }
}

4. El cliente responde:

200 OK

El Problema de la Confiabilidad

Los webhooks pueden fallar por muchas razones:

Sin lógica de reintento, los eventos se pierden. Sin idempotencia, los webhooks duplicados causan acciones duplicadas.

Lógica de reintento con Retroceso Exponencial

Reintenta los webhooks fallidos con retrasos crecientes.

Estrategia de Retroceso Exponencial

Intento 1: Inmediato
Intento 2: 1 segundo después
Intento 3: 2 segundos después
Intento 4: 4 segundos después
Intento 5: 8 segundos después
Intento 6: 16 segundos después

¿Por qué exponencial? Si el cliente está caído, bombardearlo con reintentos no ayudará. El retroceso exponencial da tiempo para la recuperación.

Implementación

async function sendWebhook(url, payload, attempt = 1, maxAttempts = 6) {
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': generateSignature(payload)
      },
      body: JSON.stringify(payload),
      timeout: 5000 // 5 second timeout
    });

    if (response.ok) {
      return { success: true, attempt };
    }

    // Retry on 5xx errors
    if (response.status >= 500 && attempt < maxAttempts) {
      const delay = Math.pow(2, attempt - 1) * 1000;
      await sleep(delay);
      return sendWebhook(url, payload, attempt + 1, maxAttempts);
    }

    // Don't retry 4xx errors (client error)
    return { success: false, status: response.status };

  } catch (error) {
    // Network error or timeout - retry
    if (attempt < maxAttempts) {
      const delay = Math.pow(2, attempt - 1) * 1000;
      await sleep(delay);
      return sendWebhook(url, payload, attempt + 1, maxAttempts);
    }
    return { success: false, error: error.message };
  }
}

Cuándo reintentar

Reintentar en:

No reintentar en:

Cola de mensajes no entregados (Dead Letter Queue)

Después de los reintentos máximos, mueve los webhooks fallidos a una cola de mensajes no entregados para revisión manual:

if (!result.success) {
  await deadLetterQueue.add({
    url,
    payload,
    attempts: maxAttempts,
    lastError: result.error,
    timestamp: new Date()
  });
}

Idempotencia para la prevención de duplicados

Los clientes pueden recibir el mismo webhook varias veces. La idempotencia previene el procesamiento duplicado.

Claves de Idempotencia

Incluye un ID único con cada webhook:

{
  "id": "webhook_019b4132",
  "event": "pet.adopted",
  "data": {...}
}

El cliente almacena los IDs procesados:

app.post('/webhooks/petstore', async (req, res) => {
  const webhookId = req.body.id;

  // Check if already processed
  const processed = await db.webhooks.findOne({ id: webhookId });
  if (processed) {
    return res.status(200).json({ message: 'Already processed' });
  }

  // Process webhook
  await processPetAdoption(req.body.data);

  // Mark as processed
  await db.webhooks.insert({ id: webhookId, processedAt: new Date() });

  res.status(200).json({ message: 'Processed' });
});

Operaciones Idempotentes

Diseña operaciones para que sean idempotentes:

Mal (no idempotente):

// Charging twice causes double charge
await chargeCustomer(userId, amount);

Bien (idempotente):

// Charging with idempotency key prevents double charge
await chargeCustomer(userId, amount, { idempotencyKey: webhookId });

Verificación de firma para seguridad

Verifica que los webhooks provengan de tu API, no de un atacante.

Firma HMAC

Genera la firma usando un secreto compartido:

// Server generates signature
const crypto = require('crypto');

function generateSignature(payload, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return hmac.digest('hex');
}

// Include in header
headers['X-Webhook-Signature'] = `sha256=${generateSignature(payload, webhookSecret)}`;

El cliente verifica la firma:

function verifySignature(payload, signature, secret) {
  const expected = generateSignature(payload, secret);
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

app.post('/webhooks/petstore', (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook
  ...
});

Validación de marca de tiempo

Incluye la marca de tiempo para prevenir ataques de repetición:

{
  "id": "webhook_019b4132",
  "timestamp": "2026-03-13T10:30:00Z",
  "event": "pet.adopted",
  "data": {...}
}

Rechazar webhooks antiguos:

const webhookAge = Date.now() - new Date(req.body.timestamp);
if (webhookAge > 5 * 60 * 1000) { // 5 minutes
  return res.status(400).json({ error: 'Webhook too old' });
}

Manejo de tiempos de espera

Establece tiempos de espera agresivos para evitar que los clientes lentos bloqueen tu sistema.

Tiempo de espera de 5 segundos

const response = await fetch(url, {
  method: 'POST',
  body: JSON.stringify(payload),
  timeout: 5000 // 5 seconds
});

¿Por qué 5 segundos? Los webhooks deben responder inmediatamente. Si un cliente tarda más, está haciendo un procesamiento síncrono (patrón incorrecto).

Patrón de procesamiento asíncrono

Mal (síncrono):

app.post('/webhooks/petstore', async (req, res) => {
  // This takes 30 seconds - webhook will timeout
  await processOrder(req.body.data);
  await sendEmail(req.body.data);
  await updateInventory(req.body.data);

  res.status(200).json({ message: 'Processed' });
});

Bien (asíncrono):

app.post('/webhooks/petstore', async (req, res) => {
  // Return immediately
  res.status(200).json({ message: 'Received' });

  // Process asynchronously
  queue.add('process-webhook', req.body);
});

Cómo Modern PetstoreAPI implementa los webhooks

Modern PetstoreAPI implementa webhooks listos para producción.

Eventos de Webhook

pet.adopted - Mascota adoptada
pet.status_changed - Estado de la mascota cambiado
order.created - Pedido creado
order.completed - Pedido completado
payment.succeeded - Pago exitoso
payment.failed - Pago fallido

Carga útil del Webhook

{
  "id": "webhook_019b4132-70aa-764f-b315-e2803d882a24",
  "event": "pet.adopted",
  "timestamp": "2026-03-13T10:30:00Z",
  "data": {
    "petId": "019b4132-70aa-764f-b315-e2803d882a24",
    "userId": "user-456",
    "orderId": "order-789",
    "adoptionDate": "2026-03-13"
  },
  "apiVersion": "v1"
}

Configuración de reintento

Seguridad

Probando Webhooks con Apidog

Apidog soporta la prueba de webhooks.

Probar entrega de Webhook

  1. Crear un endpoint de webhook mock en Apidog
  2. Registrar el endpoint con PetstoreAPI
  3. Desencadenar un evento (adoptar mascota)
  4. Verificar el webhook recibido
  5. Comprobar el formato de la carga útil

Probar verificación de firma

// Apidog test script
const signature = pm.request.headers.get('X-Webhook-Signature');
const payload = pm.request.body.raw;
const secret = pm.environment.get('WEBHOOK_SECRET');

const expected = generateSignature(payload, secret);
pm.test('Signature valid', () => {
  pm.expect(signature).to.equal(`sha256=${expected}`);
});

Probar la lógica de reintento

  1. Devolver un error 500 desde el endpoint mock
  2. Verificar los intentos de reintento con retroceso exponencial
  3. Comprobar la cola de mensajes no entregados después del número máximo de reintentos

Probar Idempotencia

  1. Recibir webhook
  2. Devolver 200
  3. Recibir el mismo webhook de nuevo (reintento simulado)
  4. Verificar que no haya procesamiento duplicado

Conclusión

Los webhooks confiables requieren:

Modern PetstoreAPI implementa todos estos patrones. Consulta la documentación de webhooks para ver ejemplos completos.

Prueba tus webhooks con Apidog para verificar la lógica de reintento, las firmas y la idempotencia antes de ir a producción.

button

Preguntas Frecuentes

¿Cuántos intentos de reintento deben tener los webhooks?

5-10 intentos con retroceso exponencial. Esto cubre interrupciones temporales (5-17 minutos) sin sobrecargar al cliente.

¿Deben los webhooks reintentar en errores 4xx?

No. Los errores 4xx indican problemas del cliente (URL incorrecta, fallo de autenticación). Reintentar no los solucionará. Solo reintenta en errores 5xx y fallos de red.

¿Cuánto tiempo deben durar los tiempos de espera de los webhooks?

5 segundos como máximo. Los clientes deben devolver 200 inmediatamente y procesar de forma asíncrona. Tiempos de espera más largos indican que el cliente está realizando un procesamiento síncrono.

¿Qué pasa si un cliente nunca responde a los webhooks?

Después de los reintentos máximos, se traslada a la cola de mensajes no entregados. Se alerta al cliente por correo electrónico. Considera deshabilitar los webhooks para ese cliente después de fallos repetidos.

¿Deben las URL de los webhooks ser HTTPS?

Sí, siempre se debe requerir HTTPS. Los webhooks HTTP pueden ser interceptados y modificados. Modern PetstoreAPI rechaza las URL de webhook HTTP.

¿Cómo se previenen los ataques de repetición?

Incluye una marca de tiempo en la carga útil y rechaza los webhooks con más de 5 minutos de antigüedad. Combínalo con la verificación de firma.

¿Pueden los clientes solicitar la reentrega de un webhook?

Sí. Modern PetstoreAPI proporciona un endpoint para reenviar webhooks específicos: POST /webhooks/{id}/redeliver

¿Cómo se prueban los webhooks localmente?

Usa herramientas como ngrok para exponer localhost a internet, o utiliza el servidor mock de Apidog para simular endpoints de webhook durante el desarrollo.

Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs