Entrega y reintentos
Las reglas
| Parámetro | Valor |
|---|---|
| Éxito | Cualquier respuesta 2xx |
| Tiempo de espera | 20 segundos |
| Intentos | 6 — uno inmediato y cinco reintentos |
| Intervalos | 60s · 5min · 30min · 2h · 6h |
| Desactivación automática | Tras 50 fallos consecutivos |
Cualquier cosa que no sea 2xx cuenta como fallo: 3xx, 4xx, 5xx, tiempo de espera
agotado, error de DNS, TLS inválido. Las redirecciones no se siguen — un 301 es un
fallo.
Agotados los seis intentos, la entrega se queda en failed y ya no se reintenta
automáticamente. El tiempo total entre el primero y el último ronda las 9 horas.
Por qué conviene responder rápido
El tiempo de espera cubre la respuesta entera, cabeceras y cuerpo. Un handler que procesa antes de responder tiene 20 segundos para todo — y un pico de tráfico se los come.
// Mal
app.post('/webhooks', async (req, res) => {
await guardarEnBaseDeDatos(req.body);
await llamarCrm(req.body);
await notificarEquipo(req.body);
res.sendStatus(200); // puede no llegar a tiempo
});
// Bien
app.post('/webhooks', (req, res) => {
res.sendStatus(200);
cola.encolar(req.body);
});
Idempotencia
Como una entrega puede repetirse, el mismo evento va a llegar más de una vez. No es la excepción: es el comportamiento normal cuando tu servidor responde lento u oscila.
Dos claves de deduplicación:
X-Joinotify-Delivery— estable entre los reintentos de esa entrega- El
wamiddel mensaje — estable incluso si reenvías manualmente
const esNuevo = await redis.set(`webhook:${deliveryId}`, '1', { NX: true, EX: 86400 });
if (!esNuevo) {
return; // repetición, ignórala
}
Desactivación automática
Cada fallo incrementa failureCount; cualquier éxito lo pone a cero. Al llegar a 50
fallos consecutivos el endpoint se desactiva: active: false, disabledAt marcado, y
las entregas siguientes se marcan como failed con "Endpoint is inactive.".
Esto protege a ambos lados — nosotros dejamos de golpear un servidor que claramente no está, y tú no recibes una avalancha cuando vuelva.
# Reactivar: el contador se pone a cero también
curl -X PATCH https://api.joinotify.com/webhook-endpoints/whe_01J8ZK... \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "active": true }'
Vigila failureCount antes de que llegue a 50. Un endpoint con 30 fallos ya tiene un
problema.
Investigar
curl 'https://api.joinotify.com/webhook-endpoints/deliveries?status=failed&limit=20' \
-H 'Authorization: Bearer sk_live_xxx'
{
"data": [
{
"id": "whd_01J8ZM...",
"endpointId": "whe_01J8ZK...",
"field": "messages",
"status": "failed",
"attempt": 6,
"responseStatus": 500,
"responseBody": "Internal Server Error",
"error": null,
"durationMs": 18412,
"nextRetryAt": null,
"createdAt": "2026-07-27T14:02:11.482Z"
}
]
}
| Campo | Qué revela |
|---|---|
responseStatus | Lo que devolvió tu servidor — null significa que ni respondió |
responseBody | El cuerpo de la respuesta, truncado a 2000 caracteres |
error | Fallo de transporte: DNS, TLS, tiempo de espera |
durationMs | Cerca de 20000 indica tiempo de espera agotado |
attempt | En cuál de los seis intentos se detuvo |
responseStatus: null con error relleno es un problema de red o certificado, no de
código. durationMs cerca de 20000 es un handler lento.
Reenviar
curl -X POST https://api.joinotify.com/webhook-endpoints/deliveries/whd_01J8ZM.../redeliver \
-H 'Authorization: Bearer sk_live_xxx'
Crea una entrega nueva en lugar de reciclar la antigua, conservando el fallo original en el historial. El ciclo de seis intentos vuelve a empezar.
Para reprocesar en masa tras un incidente, lista los fallos del período y reenvíalos uno a uno:
curl 'https://api.joinotify.com/webhook-endpoints/deliveries?status=failed&limit=200' \
-H 'Authorization: Bearer sk_live_xxx' \
| jq -r '.data[].id' \
| while read id; do
curl -sS -X POST "https://api.joinotify.com/webhook-endpoints/deliveries/$id/redeliver" \
-H 'Authorization: Bearer sk_live_xxx' > /dev/null
done
Red de seguridad
Incluso con reintentos, un evento puede perderse — un endpoint caído más de nueve horas,
por ejemplo. Para datos críticos, ejecuta una conciliación periódica contra GET /events,
que guarda el historial bruto con independencia de las entregas.