Ir al contenido

Idempotencia y reintentos

Una llamada puede fallar sin que sepas si llegó: se corta la red, vence el tiempo, el servidor responde 503. Si reintentas sin cuidado, podrías emitir dos facturas por una venta. La cabecera Idempotency-Key lo evita.

Toda emisión exige Idempotency-Key: de 1 a 128 caracteres ASCII visibles (por ejemplo, un UUID o el número interno de tu venta). Sin ella, 400 missing_idempotency_key; con una inválida, 400 invalid_idempotency_key.

Reenvías… Resultado
La misma clave y el mismo cuerpo El mismo documento en su estado actual, con la cabecera Idempotent-Replayed: true y el mismo Location. No se emite otro.
La misma clave y un cuerpo distinto 409 idempotency_key_reused.
Una clave nueva Un documento nuevo.
  • El cuerpo se compara por su contenido: reordenar campos o cambiar los espacios no cuenta como cuerpo distinto; cambiar un valor sí (incluso "1" por 1).
  • La repetición muestra el documento en su estado actual, con el mismo id, número y enlaces, y responde con el código de ese estado: 201 si ya está accepted (o en contingency), 200 si está rejected o failed. Si todavía está en proceso, la repetición espera el resultado igual que la emisión (?wait=N) y responde 202 si no alcanza.
  • La clave es por empresa y ambiente, y no vence: queda ligada para siempre a su documento. Por eso nunca la reutilices para otra venta.
  • En el sandbox, un escenario forzado (Foliios-Sandbox-Scenario) entra en la huella: la misma clave con otro escenario es 409 idempotency_key_reused.

Si la solicitud falla antes de consumir nada (contenido inválido, resolución desconocida o vencida, número ocupado, empresa sin configurar), foliios libera la clave y puedes corregir el cuerpo y reintentar con la misma. Ante un error técnico (5xx) la clave queda: el documento pudo haberse numerado, y solo el mismo cuerpo puede reintentar.

La misma clave con el mismo cuerpo siempre devuelve el mismo documento, también cuando ese documento terminó failed: recibes otra vez el failed original, con Idempotent-Replayed: true. No se emite de nuevo.

Si el documento falló y quieres emitir uno corregido (un documento failed con invalid_document no tiene reintento), usa una Idempotency-Key nueva. Con numeración manual puedes enviar el mismo número cuando el documento que falló trae number_reusable: true: se acepta de inmediato, sin esperar nada. Ver cuándo queda libre un número.

Por eso, si derivas la clave de la venta, agrégale algo que cambie solo cuando decides emitir un documento nuevo, no en cada reintento de red:

def clave(venta):
# "intento" sube solo cuando el documento anterior terminó failed
# y vas a emitir uno nuevo; un reintento de red usa el mismo valor.
return f"venta-{venta['id']}-{venta['intento']}"

Genera la clave a partir de la venta, no en cada intento: así cualquier reintento, incluso tras reiniciar tu programa, usa la misma. La única vez que cambia es después de un failed.

from foliios_inicio import EMPRESA, llamar
def emitir(venta, cuerpo):
return llamar(
"POST",
f"/v1/companies/{EMPRESA}/documents",
cuerpo,
{"Idempotency-Key": f"venta-{venta['id']}"}, # estable entre intentos
)
Respuesta ¿Reintentar? Cómo
Error de red o tiempo agotado sin respuesta Sí Misma clave y mismo cuerpo.
202 (el documento sigue en proceso) No No es un error: consulta el documento con GET (Location) o espera el webhook.
202 con outcome_code: dian_outcome_uncertain No La DIAN puede tener el documento. Nunca lo emitas de nuevo con otra clave: consulta con GET hasta un estado final.
503 (request_timeout, temporarily_unavailable) Sí Misma clave y cuerpo; espera Retry-After.
429 (rate_limited, in_flight_limited, concurrency_limited) Sí Misma clave y cuerpo; espera Retry-After y reintenta con pausa creciente. Ver límites por empresa.
409 concurrent_modification Sí Otro proceso cambió el documento a la vez.
400, 422 No Corrige el cuerpo. La clave quedó libre: puedes usar la misma.
401, 403 No Problema de clave o permisos.
409 idempotency_key_reused No Usaste la clave con otro cuerpo: es un error de tu programa.

Los límites de frecuencia y de cuota del sandbox están en escenarios, sección «Límites».

import time
def con_reintentos(hacer, intentos=5):
espera = 1
for intento in range(intentos):
try:
estado, cuerpo, cabeceras = hacer()
except OSError: # sin respuesta: la red falló
estado, cuerpo, cabeceras = None, None, {}
if estado is not None and estado not in (429, 503, 409):
return estado, cuerpo, cabeceras
if estado == 409 and cuerpo.get("code") != "concurrent_modification":
return estado, cuerpo, cabeceras
time.sleep(int(cabeceras.get("Retry-After", espera)))
espera = min(espera * 2, 30)
raise RuntimeError("sigue fallando tras varios intentos")

Con curl, sencillamente repite el mismo comando con la misma cabecera:

Ventana de terminal
curl -s -i -X POST "https://api.foliios.com/v1/companies/$FOLIIOS_COMPANY_ID/documents" \
-H "Authorization: Bearer $FOLIIOS_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: venta-1042" -d @factura.json
# La segunda vez: 201 (el documento ya está accepted) e "Idempotent-Replayed: true", con el mismo "id".
  • resubmit (corregir un rechazo) no lleva Idempotency-Key: reenviar algo que ya no está rejected responde 409 invalid_state, así que un reintento no puede reenviar dos veces. Espera el resultado como la emisión (201, 200 o 202).
  • retry (reintentar un failed) es idempotente por estado: sobre un documento que ya espera transmisión o se está transmitiendo devuelve el documento actual con Idempotent-Replayed: true, sin encolar otro trabajo. También espera el resultado (?wait=N).
  • deliver (reenviar el correo al comprador) no es idempotente: cada llamada es un correo más.
  • La transcripción de una factura de papel no lleva Idempotency-Key: el papel es la clave (factura de papel).

Emite dos veces la misma venta con la misma clave y verifica que el segundo id es igual al primero y que llega Idempotent-Replayed: true. Esta comprobación la declara tu programa: la verificación de integración no puede comprobarla.