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.
Cómo funciona
Sección titulada «Cómo funciona»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"por1). - 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:201si ya estáaccepted(o encontingency),200si estárejectedofailed. Si todavía está en proceso, la repetición espera el resultado igual que la emisión (?wait=N) y responde202si 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 es409 idempotency_key_reused.
Cuándo se libera la clave
Sección titulada «Cuándo se libera la clave»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.
Después de un failed, una clave nueva
Sección titulada «Después de un failed, una clave nueva»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']}"La clave sale de la venta
Sección titulada «La clave sale de la venta»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 )Qué reintentar y cómo
Sección titulada «Qué reintentar y cómo»| 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:
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".Otras operaciones
Sección titulada «Otras operaciones»resubmit(corregir un rechazo) no llevaIdempotency-Key: reenviar algo que ya no estárejectedresponde409 invalid_state, así que un reintento no puede reenviar dos veces. Espera el resultado como la emisión (201,200o202).retry(reintentar unfailed) es idempotente por estado: sobre un documento que ya espera transmisión o se está transmitiendo devuelve el documento actual conIdempotent-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).
Cómo comprobarlo
Sección titulada «Cómo comprobarlo»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.