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.
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
- Genera una key por factura lógica (no por intento).
- Persístela antes de la primera llamada.
- 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ón | Respuesta |
|---|---|
| Key nueva | Se emite la factura. replayed: false. |
| Misma key, mismo body | Se devuelve la factura original, con replayed: true. No se emitió una segunda. |
| Misma key, body distinto | 409 IDEMPOTENCY_KEY_REQUEST_MISMATCH. Nada se emite. |
| Sin header (o vacío) | 400 MISSING_IDEMPOTENCY_KEY. |
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 es429si 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.