Idempotencia
Un error de red o un timeout no debería crear dos facturas. La idempotencia es el mecanismo que lo garantiza.
El problema sin idempotencia
Section titled “El problema sin idempotencia”Tu app Emitia API │ POST /v1/invoices │ │────────────────────►│ │ │ factura creada ✓ │ timeout / error │ │◄ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ ¿debo reintentar? │ │ Si sí → factura duplicada. │ Si no → factura perdida.Reintenta con el mismo Idempotency-Key → Emitia detecta el duplicado y devuelve
exactamente la misma respuesta que devolvió la primera vez, sin crear un segundo
recurso.
El header Idempotency-Key
Section titled “El header Idempotency-Key”POST /v1/invoices HTTP/1.1Authorization: Bearer sk_test_…Idempotency-Key: 5ad2c4a1-49a0-46e7-95d6-f3d61f08c4abContent-Type: application/json- Obligatorio en todos los
POSTque crean o modifican recursos cobrables (facturas, notas, clientes, resolutions). - Formato: cualquier string de hasta 128 caracteres. Se recomienda UUID v4 o ULID.
- Alcance: per-API-key. Dos keys distintas pueden reutilizar el mismo valor sin conflicto.
- Caché: 24 horas desde la primera petición exitosa.
Semántica de replay
Section titled “Semántica de replay”| Escenario | Qué devuelve Emitia |
|---|---|
Primera petición → éxito 201 | Respuesta original + Emitia-Idempotency-Cached: false |
| Reintento, mismo key + mismo body → la operación ya terminó | Misma respuesta original + Emitia-Idempotency-Cached: true |
| Reintento, mismo key + mismo body → la operación está en proceso | 202 Accepted con estado actual |
| Mismo key + body diferente | 409 idempotency_conflict |
El header de respuesta Emitia-Idempotency-Cached: true te confirma que recibiste
una respuesta cacheada, no una nueva ejecución.
Cómo generar la clave
Section titled “Cómo generar la clave”Genera la clave antes de intentar el request, no después. Guárdala junto al pedido en tu base de datos local para poder reintentarla con confianza.
import { randomUUID } from 'crypto';const idempotencyKey = randomUUID(); // "5ad2c4a1-..."
// Guarda en tu DB antes de llamar a la APIawait db.orders.update({ id: orderId, emitia_idempotency_key: idempotencyKey });
// Llama a la API con esa claveconst res = await fetch('https://api.emitia.co/v1/invoices', { method: 'POST', headers: { Authorization: 'Bearer sk_test_…', 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ ... }),});# Pythonimport uuididempotency_key = str(uuid.uuid4())// PHP$idempotency_key = bin2hex(random_bytes(16));// o: $idempotency_key = (string) \Ramsey\Uuid\Uuid::uuid4();Error: 409 idempotency_conflict
Section titled “Error: 409 idempotency_conflict”Ocurre cuando reutilizas una clave con un body diferente. La respuesta es:
{ "error": { "type": "idempotency_error", "code": "idempotency_conflict", "message": "Esta Idempotency-Key ya fue usada con un body diferente. Usa una clave nueva para este request.", "doc_url": "https://docs.emitia.co/errors/idempotency_conflict", "request_id": "req_01HX…" }}Causa más común: reutilizar la misma clave para dos facturas diferentes. La solución es generar una clave nueva por cada operación lógica nueva.
Ver la página de error completa.
Patrón recomendado: retry con backoff
Section titled “Patrón recomendado: retry con backoff”async function createInvoiceWithRetry( payload: object, maxRetries = 3,): Promise<Response> { // La clave se genera UNA SOLA VEZ y se reutiliza en los reintentos const idempotencyKey = randomUUID();
for (let attempt = 0; attempt <= maxRetries; attempt++) { const res = await fetch('https://api.emitia.co/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMITIA_API_KEY}`, 'Idempotency-Key': idempotencyKey, // misma clave en cada reintento 'Content-Type': 'application/json', }, body: JSON.stringify(payload), });
if (res.ok || res.status === 409) return res; // 409 = no reintentar
if (attempt < maxRetries) { // Backoff exponencial: 1s, 2s, 4s await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); } } throw new Error('Máximo de reintentos alcanzado');}El punto clave: la idempotencyKey se genera antes del bucle y no cambia entre
reintentos. Eso es lo que hace el patrón seguro.
Próximos pasos
Section titled “Próximos pasos”- Conceptos: Webhooks — recibirás el resultado final vía webhook
- Conceptos: Ambientes — sandbox vs habilitación vs producción
- Referencia: error
idempotency_conflict