Saltearse al contenido

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.

Envía una clave única en cada POST:

Ventana de terminal
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ónQué hace Olympus
Primera vez con esa claveProcesa la petición normalmente y guarda la respuesta
Misma clave, mismo cuerpoDevuelve la respuesta guardada sin volver a emitir, con X-Idempotent-Replay: true
Misma clave, cuerpo distinto409 idempotency_conflict: esa clave ya identifica otra operación
Misma clave mientras la primera se procesa409 idempotency_in_progress: espera y consulta
Misma clave tras 24 horasLa 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.

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.

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")

Olympus protege la emisión por partida doble:

  1. Idempotency-Key: protege la petición. Misma clave y mismo cuerpo, misma respuesta.
  2. referencia_externa: protege la venta. Si ya existe una factura con esa referencia, la emisión responde 409 external_id_conflict con el id de 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()
  • 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-Key sin interferirse.
  • Solo aplica a POST. Las lecturas ya son idempotentes por naturaleza.
  • Con la clave de un agente es obligatoria al emitir: sin ella, 400 idempotency_key_required.