Saltearse al contenido

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:

IntentoDelay después del anterior
1inmediato
21 minuto
35 minutos
430 minutos
52 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 marcamos failed directo.
  • 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

  1. Reintentamos 5 veces con backoff cuando devolvés 5xx o timeout.
  2. NO reintentamos cuando devolvés 4xx (config error tuyo).
  3. Si después de 5 intentos seguís fallando, te mandamos email.
  4. Cada evento puede llegar más de una vez. Usá event_id como clave de idempotencia.