Saltar al contenido principal

Idempotencia y reintentos

POST /v1/invoices exige el header Idempotency-Key. Es el único endpoint que lo pide, y lo pide por una razón concreta: una factura duplicada en Honduras consume un correlativo del rango CAI y queda registrada. No es un dato de más que puedas borrar; es un problema de cumplimiento.

nota

POST /v1/invoices/preview no lleva Idempotency-Key: no emite nada, así que no hay nada que reintentar de forma idempotente. Es el endpoint con el que pruebas.

La regla, en tres líneas

  1. Genera una key por factura lógica (no por intento).
  2. Persístela antes de la primera llamada.
  3. En cada reintento manda la misma key y el mismo body.
Idempotency-Key: order-2026-0917

No tiene que ser un UUID: tu propio número de orden sirve, siempre que sea único dentro de tu sistema. Las keys se conservan alrededor de 24 horas.

Qué pasa en cada caso

SituaciónRespuesta
Key nuevaSe emite la factura. replayed: false.
Misma key, mismo bodySe devuelve la factura original, con replayed: true. No se emitió una segunda.
Misma key, body distinto409 IDEMPOTENCY_KEY_REQUEST_MISMATCH. Nada se emite.
Sin header (o vacío)400 MISSING_IDEMPOTENCY_KEY.
consejo

replayed: true es un éxito

No lo trates como duplicado ni como error. Significa exactamente lo contrario: que tu reintento no creó una segunda factura y que estás viendo la original. Guárdala y sigue adelante.

Por qué "persiste antes de llamar"

El caso que importa es el timeout. Tu solicitud se fue, la conexión se cayó y no sabes si la factura se emitió o no.

  • Si la key estaba guardada en tu base junto con la orden: reintentas con esa misma key y obtienes la verdad. O te llega la factura original (replayed: true, sí se había emitido) o se emite ahora. Nunca dos.
  • Si la generaste al vuelo en memoria: la perdiste. Reintentar con una key nueva emite una segunda factura legal. Con eso ya no puede ayudarte el API.

Por eso el orden correcto es: guardar la orden con su idempotencyKey → hacer commit → llamar a la API.

Patrón de reintento recomendado

async function emitirConReintentos(orden, intentos = 4) {
for (let intento = 1; intento <= intentos; intento++) {
try {
const res = await postFactura(orden); // usa siempre orden.idempotencyKey
if (res.status < 500) return res; // 2xx o 4xx: decisión definitiva
} catch (e) {
// timeout / red: cae al backoff
}
await esperar(2 ** intento * 500); // 1s, 2s, 4s, 8s
}
throw new Error('Contadito no respondió; revisa manualmente la orden ' + orden.id);
}

Reglas del patrón:

  • Reintenta ante timeouts, errores de red y 5xx.
  • No reintentes ante 4xx: la solicitud está mal y el próximo intento fallará igual. La excepción es 429 si alguna vez lo ves.
  • Nunca cambies el body entre reintentos. Si cambió algo (una línea, la cantidad), eso es otra factura lógica: genera otra key.

Cuando ya se te acabaron los reintentos

Si no lograste una respuesta definitiva, no emitas una factura nueva. Marca la orden como "pendiente de confirmar" y reintenta más tarde con la misma key (dentro de las 24 h). Si pasaron más de 24 h, verifica en la app de Contadito si la factura existe antes de volver a intentar.