EspañolWebhooksReintento e idempotencia

Reintento e idempotencia

Retry automático con backoff

Si la entrega a tu endpoint falla con un error transiente (timeout, error de red, HTTP 5xx, 429 o 408), INFI reentrega automáticamente con backoff creciente — hasta 6 intentos a lo largo de ~9 horas:

1min → 5min → 30min → 2h → 6h

Ya no pierdes un evento por una inestabilidad puntual de tu lado.

Qué se reintenta (y qué no)

Respuesta de tu endpointComportamiento
2xxÉxito — entrega cerrada.
Timeout (8s) o error de redTransiente → retry con backoff.
5xx, 429, 408Transiente → retry con backoff.
4xx (400, 401, 403, 422…)Permanente — tu endpoint rechazó el evento. No se reintenta (martillar no ayudaría). Corrige la causa (auth, validación de firma, ruta) y usa el reenvío manual.
Agotó los 6 intentosMarcado como agotado. Tras corregir el endpoint, usa el reenvío manual.
La firma cambia en cada intento

Cada reentrega usa un timestamp nuevo — y por lo tanto una firma nueva (para pasar las protecciones de replay). El eventId permanece igual. Re-valida siempre el HMAC usando el X-Infi-Timestamp recibido en esa solicitud; nunca compares la firma con una anterior. Ver Firma.

Estrategia recomendada: event-driven

Con el retry automático, el webhook es lo suficientemente confiable para ser el camino primario. No necesitas polling agresivo.

  1. Reacciona al webhook apenas llegue — confirma el depósito y actualiza tu sistema. El evento transaction.paid ya trae todo lo que necesitas: transactionId, status, amountCents, netCents, paidAt, endToEndId, payer.
  2. Mantén tu saldo localmente sumando netCents de cada transaction.paid (y restando los retiros), en vez de consultar GET /v1/balance en bucle.
  3. Concilia ligero como red de seguridad: un barrido GET /v1/transactions cada 1–2 minutos (no cada pocos segundos) cubre cualquier evento que escape — por ejemplo, si tu endpoint estuvo caído más allá de la ventana de retry.
Evita polling agresivo

Consultar GET /v1/balance o GET /v1/transactions/:id cada pocos segundos es innecesario con webhook + retry, y consume rate limit en vano. Prefiere reaccionar al webhook y conciliar de forma esparza.

Idempotencia de tu lado

INFI garantiza un eventId único por evento (p.ej. evt_1715000000000_abcdef12), entregado:

  • En el header X-Infi-Event-Id.
  • En el campo eventId del cuerpo.

El mismo eventId llega cuando:

  • La entrega es reentregada automáticamente (retry tras falla transiente).
  • Múltiples URLs del mismo comerciante hacen match al evento (fanout).
  • La entrega se reenvía manualmente desde el panel.

Por eso, deduplicar por eventId es obligatorio — almacena los procesados e ignora repeticiones:

async function handleWebhook(req) {
  const eventId = req.headers['x-infi-event-id'] || req.body.eventId
  if (await db.processedEvents.has(eventId)) return  // ya procesado
  await db.processedEvents.add(eventId, ttl: '7d')
 
  // aplica la transición
  await applyTransition(req.body.transactionId, req.body.status)
}

Como el webhook y el reconcile (polling) pueden confirmar la misma transición, trata el procesamiento de forma idempotente también por transactionId + status:

async function applyTransition(transactionId, newStatus) {
  const current = await db.orders.getByTx(transactionId)
  if (current?.status === newStatus) return  // ya aplicado
  await db.orders.setStatus(transactionId, newStatus)
  await onStatusChanged(transactionId, newStatus)
}

Cuidados

  • Timeout corto: 8s por intento. No hagas trabajo pesado en el handler — responde 200 rápido y procesa en background/cola. Un handler lento se convierte en timeout → retry innecesario.
  • Devuelve el status correcto: 2xx = recibido (cierra). 5xx/timeout = “intenta de nuevo” (se convierte en retry). 4xx = “no me mandes esto” (cierra como permanente, sin retry). Úsalo a tu favor.
  • Historial en el panel: toda entrega — incluyendo las que fallaron y cada retry automático — queda en Webhooks → Historial de entregas por 60 días, con payload exacto y timeline de intentos. Útil para debug post mortem.
  • Reenvío manual: si una entrega agotó los intentos o fue rechazada (4xx), corrige el endpoint y dispara un reenvío manual — los intentos manuales aparecen con manual: true en el historial.