Saltar al contenido principal

Entrega y reintentos

Las reglas​

ParámetroValor
ÉxitoCualquier respuesta 2xx
Tiempo de espera20 segundos
Intentos6 — uno inmediato y cinco reintentos
Intervalos60s · 5min · 30min · 2h · 6h
Desactivación automáticaTras 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 wamid del 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"
}
]
}
CampoQué revela
responseStatusLo que devolvió tu servidor — null significa que ni respondió
responseBodyEl cuerpo de la respuesta, truncado a 2000 caracteres
errorFallo de transporte: DNS, TLS, tiempo de espera
durationMsCerca de 20000 indica tiempo de espera agotado
attemptEn 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.