Reintentos e idempotencia
Internet falla. Tu app puede estar caída cuando llega un webhook. Por eso INGALCA Pay reintenta automáticamente cuando tu handler responde mal.
Política de reintentos
Si tu endpoint devuelve 5xx o no responde antes de 10 segundos, reintentamos hasta 5 veces con backoff:
| Intento | Delay después del anterior |
|---|---|
| 1 | inmediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| (final) | 12 horas |
Si después del intento #5 sigue fallando, marcamos el delivery como failed definitivo y mandamos un email al admin del tenant para que investigue.
Cuándo NO reintentamos
- Respuesta
4xx— asumimos que tu URL/handler está mal configurado. Reintentar no va a cambiar el resultado. Lo marcamosfaileddirecto. - Respuesta
2xx— éxito.
flowchart TD
Send[Mandar webhook] --> Wait[Esperar respuesta]
Wait --> Code{HTTP status?}
Code -->|2xx| Done[delivered ✓]
Code -->|4xx| Failed4xx[failed - no retry]
Code -->|5xx o timeout| Retry{Intento < 5?}
Retry -->|Sí| Backoff[Esperar backoff]
Backoff --> Send
Retry -->|No| FailedMax[failed - max attempts]
FailedMax --> Email[Email al admin]
Reenvío manual
Aún después de un failed definitivo, podés reenviar el webhook desde el dashboard: Webhooks → click en el delivery → Reenviar. Esto resetea el contador y vuelve a intentar.
Idempotencia: por qué importa
El mismo evento puede llegar a tu handler más de una vez. Posibles causas:
- Tu handler procesó OK pero tardó >10s, devolviste 200 tarde, nosotros marcamos timeout y reintentamos.
- Tu handler procesó OK pero tu LB devolvió 502 antes de que la respuesta llegue a nosotros.
- Hiciste reenvío manual desde el dashboard.
Si tu handler crea una orden por cada payment.confirmed, vas a crear duplicados. Para evitarlo, hacelo idempotente.
Cómo hacerlo idempotente
Cada evento trae un event_id único en el header X-Ingalca-Event-Id y dentro del body. Usalo como clave de idempotencia.
Patrón típico: una tabla processed_webhook_events con event_id como UNIQUE.
CREATE TABLE processed_webhook_events ( event_id VARCHAR(255) PRIMARY KEY, event_type VARCHAR(100) NOT NULL, processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);En tu handler:
async function handleWebhook(event) { try { await db.insert('processed_webhook_events', { event_id: event.event_id, event_type: event.event_type, }); } catch (e) { if (isDuplicateKeyError(e)) { // Ya lo procesamos antes. Devolvemos 200 para que no reintente. return { status: 200 }; } throw e; }
// Lógica real solo se ejecuta si el insert fue exitoso (primera vez). await processEvent(event); return { status: 200 };}Esto te garantiza at-most-once procesamiento por evento. Es más seguro que tratar de detectar duplicados por contenido.
TL;DR
- Reintentamos 5 veces con backoff cuando devolvés 5xx o timeout.
- NO reintentamos cuando devolvés 4xx (config error tuyo).
- Si después de 5 intentos seguís fallando, te mandamos email.
- Cada evento puede llegar más de una vez. Usá
event_idcomo clave de idempotencia.