Empezar
Idempotencia
Una petición puede fallar sin que sepas si llegó a ejecutarse: se corta la red, vence el tiempo de espera, tu proceso muere justo después de enviar. En facturación eso es serio: reintentar a ciegas puede emitir dos comprobantes reales para una sola venta.
Para eso está la cabecera Idempotency-Key.
Cómo funciona
Sección titulada «Cómo funciona»Envía una clave única en cada POST:
curl -X POST https://olympus.atharix.com/api/v1/invoices/issue \ -H "X-API-Key: $OLYMPUS_API_KEY" \ -H "Idempotency-Key: venta-SO-2026-0912" \ -H "Content-Type: application/json" \ -d '{ … }'A partir de ahí:
| Situación | Qué hace Olympus |
|---|---|
| Primera vez con esa clave | Procesa la petición normalmente y guarda la respuesta |
| Misma clave, mismo cuerpo | Devuelve la respuesta guardada sin volver a emitir, con X-Idempotent-Replay: true |
| Misma clave, cuerpo distinto | 409 idempotency_conflict: esa clave ya identifica otra operación |
| Misma clave mientras la primera se procesa | 409 idempotency_in_progress: espera y consulta |
| Misma clave tras 24 horas | La clave caduca y vuelve a estar libre |
Solo se recuerdan las operaciones que salieron bien. Si tu petición falló por un error de validación, corriges el payload y reintentas con la misma clave sin problema.
Cómo elegir la clave
Sección titulada «Cómo elegir la clave»La buena clave es la que identifica la operación en tu sistema, no la petición HTTP:
# Bien: la misma venta siempre da la misma clave, aunque reintentes desde otro proceso.idempotency_key = f"venta-{venta.id}"
# Mal: cada reintento genera una clave nueva, así que la protección no sirve de nada.idempotency_key = str(uuid.uuid4())Si generas un UUID nuevo en cada intento, no estás protegido: para Olympus son operaciones distintas.
Ejemplo completo
Sección titulada «Ejemplo completo»def emitir_factura(venta): idempotency_key = f"venta-{venta.id}"
for intento in range(3): try: respuesta = client.post( "/invoices/issue", json=construir_payload(venta), headers={"Idempotency-Key": idempotency_key}, timeout=20, ) except httpx.TimeoutException: continue # No sabemos si llegó: la clave nos protege al reintentar.
if respuesta.is_success: if respuesta.headers.get("X-Idempotent-Replay") == "true": log.info("La factura ya estaba emitida; se reutiliza la respuesta.") return respuesta.json()
error = respuesta.json()["error"] if error["code"] == "idempotency_in_progress": time.sleep(2) continue raise ErrorDeFacturacion(error)
raise ErrorDeFacturacion("Se agotaron los reintentos")Dos protecciones, no una
Sección titulada «Dos protecciones, no una»Olympus protege la emisión por partida doble:
Idempotency-Key: protege la petición. Misma clave y mismo cuerpo, misma respuesta.referencia_externa: protege la venta. Si ya existe una factura con esa referencia, la emisión responde409 external_id_conflictcon elidde la que ya existe, incluso si vino de otra clave, otro día o desde otro sistema.
Usa las dos. La primera te cubre ante fallos de red; la segunda, ante duplicados en tu propia lógica:
error = respuesta.json()["error"]if error["code"] == "external_id_conflict": # No es un fallo: la venta ya estaba facturada. Recupera la factura existente. return client.get(f"/invoices/{error['details']['id']}").json()Límites
Sección titulada «Límites»- La clave admite hasta 255 caracteres.
- Se recuerda durante 24 horas. Pasado ese plazo, la misma clave vuelve a emitir.
- Cada API key tiene su propio espacio de claves: dos sistemas distintos pueden usar la misma
Idempotency-Keysin interferirse. - Solo aplica a
POST. Las lecturas ya son idempotentes por naturaleza. - Con la clave de un agente es obligatoria al emitir: sin ella,
400idempotency_key_required.