Entrega de Webhooks
Zelta Pay implementa un sistema robusto de entrega de webhooks con reintentos automáticos y una Dead Letter Queue (DLQ) para garantizar que ningún evento se pierda.
En el dashboard puedes revisar las trazas de entrega de cada evento, con su estado, código de respuesta HTTP y número de intentos:

Proceso de entrega
Cuando ocurre un evento, Zelta Pay sigue estos pasos:
- Generación -- Se genera el evento con un ID único y se firma criptográficamente
- Primer intento -- Se envía la solicitud HTTP
POSTa tu endpoint inmediatamente - Evaluación de respuesta -- Se evalúa el código de estado de la respuesta
- Reintentos -- Si la entrega falla, se reintenta automáticamente tras un breve retraso
- DLQ -- Si todos los intentos fallan, el evento se envía a la Dead Letter Queue
Cronología de entrega
- Entrega inmediata -- El primer intento se realiza en milisegundos después del evento
- Hasta 6 entregas -- 1 intento inicial + hasta 5 reintentos automáticos
- Retraso entre intentos -- En producción cada reintento espera un retraso base de aproximadamente 15 segundos antes de volver a intentar
- DLQ -- Los eventos que agotan todos los intentos se almacenan por 30 días
Calendario de reintentos
Si un intento no recibe una respuesta 2xx (o no responde a tiempo), Zelta Pay reintenta la entrega. En producción se realizan el intento inicial más hasta 5 reintentos, con un retraso base de ~15 segundos entre cada uno:
| Entrega | Descripción |
|---|---|
| Intento 1 | Envío inicial, inmediato tras el evento |
| Intentos 2–6 | Hasta 5 reintentos automáticos, cada uno tras un retraso base de ~15 s |
Puedes leer el número de intento actual en el header Zeltapay-Delivery-Attempt de cada solicitud.
Manejo de respuestas
El comportamiento de Zelta Pay depende del código de estado que tu endpoint retorne:
Éxito (2xx)
Cualquier respuesta con código 2xx se considera exitosa. El evento se marca como entregado y no se reintenta.
HTTP/1.1 200 OK
Content-Type: application/json
{"received": true}Errores reintentables
Los siguientes códigos provocan reintentos automáticos:
| Código | Descripción | Comportamiento |
|---|---|---|
408 | Request Timeout | Se reintenta |
429 | Too Many Requests | Se reintenta |
500 | Internal Server Error | Se reintenta |
502 | Bad Gateway | Se reintenta |
503 | Service Unavailable | Se reintenta |
504 | Gateway Timeout | Se reintenta |
| Timeout | Sin respuesta dentro del tiempo límite | Se reintenta |
Errores no reintentables (3xx / 4xx)
Los errores 4xx (excepto 408 y 429) se consideran permanentes y el evento se envía directamente al DLQ sin reintentos. Las respuestas de redirección (3xx) nunca se siguen —por seguridad, para evitar que un endpoint reenvíe la solicitud firmada a otro host— y también se envían directamente al DLQ:
| Código | Descripción | Comportamiento |
|---|---|---|
400 | Bad Request | Directo al DLQ |
401 | Unauthorized | Directo al DLQ |
403 | Forbidden | Directo al DLQ |
404 | Not Found | Directo al DLQ |
405 | Method Not Allowed | Directo al DLQ |
422 | Unprocessable Entity | Directo al DLQ |
Dead Letter Queue (DLQ)
¿Qué es el DLQ?
La Dead Letter Queue es un almacén donde se guardan los eventos de webhook que no pudieron ser entregados exitosamente. Te permite revisar, diagnosticar y reintentar estos eventos manualmente.
¿Cuándo un evento va al DLQ?
- Después de agotar los 6 intentos de entrega con errores reintentables
- Inmediatamente al recibir un error
4xxno reintentable
Gestión del DLQ
| Característica | Detalle |
|---|---|
| Retención | Los eventos se almacenan por 30 días |
| Acceso | Disponible desde el |
| Reintento manual | Puedes reintentar eventos individuales desde el dashboard |
| Reintento masivo | Puedes reintentar múltiples eventos a la vez |
Revisa tu DLQ regularmente
Eventos en el DLQ pueden indicar problemas con tu endpoint (URL incorrecta, errores en tu código, servidor caído). Revisa el DLQ periódicamente desde el para identificar y resolver problemas.
Headers de entrega
Headers estándar
Presentes en todas las entregas:
| Header | Descripción |
|---|---|
Content-Type | application/json |
User-Agent | zeltapay-webhook/1.0 |
Zeltapay-Event-Id | Identificador único del evento |
Zeltapay-Event-Type | Tipo del evento |
Zeltapay-Timestamp | Timestamp Unix (segundos) del evento |
Zeltapay-Signature | Firma HMAC-SHA256 con formato t=<timestamp>, v1=<hmac> |
Headers específicos de entrega
Presentes para monitorear el estado de la entrega:
| Header | Descripción | Ejemplo |
|---|---|---|
Zeltapay-Delivery-Attempt | Número de intento actual (1-6) | 3 |
Ejemplos de implementación
Handler básico
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/zelta-pay', async (req, res) => {
const deliveryAttempt = req.headers['zeltapay-delivery-attempt'];
const eventId = req.headers['zeltapay-event-id'];
console.log(`Webhook recibido - Evento: ${eventId}, Intento: ${deliveryAttempt}`);
try {
await processEvent(req.body);
res.status(200).json({ received: true });
} catch (error) {
console.error(`Error procesando evento ${eventId}:`, error);
// Responder 200 para evitar reintentos si el error es de lógica de negocio
// Responder 500 solo si quieres que se reintente
res.status(200).json({ received: true, error: error.message });
}
});Handler avanzado con idempotencia
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/zelta-pay', async (req, res) => {
const eventId = req.headers['zeltapay-event-id'];
const deliveryAttempt = req.headers['zeltapay-delivery-attempt'];
try {
// Verificar idempotencia
const existing = await db.query(
'SELECT event_id FROM webhook_events WHERE event_id = $1',
[eventId]
);
if (existing.rows.length > 0) {
console.log(`Evento duplicado: ${eventId} (intento ${deliveryAttempt})`);
return res.status(200).json({ received: true, duplicate: true });
}
// Procesar dentro de una transacción
await db.transaction(async (tx) => {
// Registrar el evento
await tx.query(
'INSERT INTO webhook_events (event_id, event_type, delivery_attempt, payload) VALUES ($1, $2, $3, $4)',
[eventId, req.body.type, deliveryAttempt, JSON.stringify(req.body)]
);
// Procesar la lógica de negocio
await processEvent(tx, req.body);
});
res.status(200).json({ received: true });
} catch (error) {
console.error(`Error procesando evento ${eventId}:`, error);
// Si es un error de base de datos, permitir reintento
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
return res.status(503).json({ error: 'Database unavailable' });
}
// Para otros errores, responder 200 para evitar reintentos
res.status(200).json({ received: true, error: error.message });
}
});Monitoreo y alertas
Métricas a rastrear
Monitorea las siguientes métricas para asegurar la salud de tu integración de webhooks:
| Métrica | Descripción |
|---|---|
| Tasa de éxito | Porcentaje de webhooks procesados exitosamente |
| Latencia de respuesta | Tiempo que tarda tu endpoint en responder |
| Eventos en DLQ | Cantidad de eventos en la Dead Letter Queue |
| Tasa de duplicados | Porcentaje de eventos duplicados detectados |
| Errores por tipo | Distribución de errores (timeout, 5xx, lógica, etc.) |
Reglas de alertas recomendadas
- Crítica: Tasa de éxito < 95% en los últimos 15 minutos
- Advertencia: Latencia de respuesta > 5 segundos
- Crítica: Más de 10 eventos en DLQ en la última hora
- Informativa: Primer evento en DLQ del día
Ejemplo de monitoreo
const metrics = {
received: 0,
processed: 0,
failed: 0,
duplicates: 0,
totalLatencyMs: 0
};
app.post('/webhooks/zelta-pay', async (req, res) => {
const startTime = Date.now();
metrics.received++;
try {
const eventId = req.headers['zeltapay-event-id'];
const isDuplicate = await checkIfProcessed(eventId);
if (isDuplicate) {
metrics.duplicates++;
return res.status(200).json({ received: true, duplicate: true });
}
await processEvent(req.body);
metrics.processed++;
res.status(200).json({ received: true });
} catch (error) {
metrics.failed++;
console.error('Error procesando webhook:', error);
res.status(200).json({ received: true, error: error.message });
} finally {
metrics.totalLatencyMs += Date.now() - startTime;
}
});
// Endpoint de métricas
app.get('/webhooks/metrics', (req, res) => {
res.json({
...metrics,
averageLatencyMs: metrics.received > 0
? Math.round(metrics.totalLatencyMs / metrics.received)
: 0,
successRate: metrics.received > 0
? ((metrics.processed / metrics.received) * 100).toFixed(2) + '%'
: 'N/A'
});
});Buenas prácticas
Responde rápido
Tu endpoint debe responder con 200 en menos de 5 segundos. Usa procesamiento asíncrono para lógica que toma más tiempo:
// Bueno: responder inmediato, procesar después
app.post('/webhooks/zelta-pay', (req, res) => {
res.status(200).json({ received: true });
processEventAsync(req.body).catch(console.error);
});Maneja errores correctamente
- Responde
200si tu servidor recibió el evento pero falló la lógica de negocio - Responde
503solo si tu servidor realmente no puede procesar (ej. base de datos caída) y quieres que se reintente - Nunca respondas
4xxa menos que el endpoint sea incorrecto
Implementa un circuit breaker
Si tu servicio downstream está caído, evita procesar webhooks que sabes que fallarán:
let circuitOpen = false;
let lastFailure = 0;
const CIRCUIT_TIMEOUT = 60000; // 1 minuto
app.post('/webhooks/zelta-pay', async (req, res) => {
// Verificar circuit breaker
if (circuitOpen && Date.now() - lastFailure < CIRCUIT_TIMEOUT) {
// Responder 503 para que se reintente después
return res.status(503).json({ error: 'Service temporarily unavailable' });
}
try {
await processEvent(req.body);
circuitOpen = false;
res.status(200).json({ received: true });
} catch (error) {
if (isDownstreamError(error)) {
circuitOpen = true;
lastFailure = Date.now();
return res.status(503).json({ error: 'Downstream service unavailable' });
}
res.status(200).json({ received: true, error: error.message });
}
});Usa colas de mensajes
Para alta confiabilidad, encola los eventos y procésalos de forma asíncrona:
app.post('/webhooks/zelta-pay', async (req, res) => {
const eventId = req.headers['zeltapay-event-id'];
// Encolar para procesamiento asíncrono
await messageQueue.send({
eventId,
payload: req.body,
receivedAt: new Date().toISOString()
});
res.status(200).json({ received: true });
});Solución de problemas
| Problema | Causa probable | Solución |
|---|---|---|
| Muchos reintentos | Tu endpoint responde lento o con errores 5xx | Optimiza tu endpoint y responde con 200 rápidamente |
| Eventos en DLQ | Errores 4xx o todos los reintentos agotados | Verifica la URL del endpoint y los logs de tu servidor |
| Eventos duplicados | Reintentos exitosos después de timeout | Implementa |
| Orden incorrecto | Los reintentos pueden alterar el orden | Usa el campo timestamp del evento para verificar el orden |
| Pérdida de eventos | Endpoint caído por período prolongado | Revisa el DLQ en el y reintenta |
Siguientes pasos
- -- Guía general de webhooks
- -- Referencia completa de eventos y payloads
- -- Evitar procesamiento duplicado
- -- Verificar autenticidad de los webhooks