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.
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:
- El servidor del cliente está caído
- Tiempo de espera de red agotado
- El cliente devuelve un error 500
- El cliente es lento (tarda 30 segundos)
- El cliente recibe el webhook pero falla antes de procesarlo
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:
- Errores de servidor 5xx (500, 502, 503, 504)
- Tiempos de espera de red
- Conexión rechazada
- Fallos de DNS
No reintentar en:
- Errores de cliente 4xx (400, 401, 404) - el cliente no los solucionará
- Éxito 2xx - ya tuvo éxito
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
- Intentos máximos: 10
- Retroceso: Exponencial (1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s)
- Ventana total de reintento: ~17 minutos
- Cola de mensajes no entregados después del número máximo de reintentos
Seguridad
- Firma HMAC-SHA256 en el encabezado
X-Webhook-Signature - Validación de marca de tiempo (rechazar webhooks de más de 5 minutos de antigüedad)
- HTTPS requerido para las URL de los webhooks
Probando Webhooks con Apidog
Apidog soporta la prueba de webhooks.
Probar entrega de Webhook
- Crear un endpoint de webhook mock en Apidog
- Registrar el endpoint con PetstoreAPI
- Desencadenar un evento (adoptar mascota)
- Verificar el webhook recibido
- 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
- Devolver un error 500 desde el endpoint mock
- Verificar los intentos de reintento con retroceso exponencial
- Comprobar la cola de mensajes no entregados después del número máximo de reintentos
Probar Idempotencia
- Recibir webhook
- Devolver 200
- Recibir el mismo webhook de nuevo (reintento simulado)
- Verificar que no haya procesamiento duplicado
Conclusión
Los webhooks confiables requieren:
- Retroceso exponencial en los reintentos (5-10 intentos)
- Claves de idempotencia para prevenir duplicados
- Verificación de firma HMAC para seguridad
- Tiempos de espera de 5 segundos
- Procesamiento asíncrono en el lado del cliente
- Cola de mensajes no entregados para webhooks fallidos
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.
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.
