Reintento e idempotencia
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 endpoint | Comportamiento |
|---|---|
2xx | Éxito — entrega cerrada. |
| Timeout (8s) o error de red | Transiente → retry con backoff. |
5xx, 429, 408 | Transiente → 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 intentos | Marcado como agotado. Tras corregir el endpoint, usa el reenvío manual. |
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.
- Reacciona al webhook apenas llegue — confirma el depósito y actualiza tu sistema. El evento
transaction.paidya trae todo lo que necesitas:transactionId,status,amountCents,netCents,paidAt,endToEndId,payer. - Mantén tu saldo localmente sumando
netCentsde cadatransaction.paid(y restando los retiros), en vez de consultarGET /v1/balanceen bucle. - Concilia ligero como red de seguridad: un barrido
GET /v1/transactionscada 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.
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
eventIddel 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
200rá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: trueen el historial.