Skip to content

Idempotencia

Un error de red o un timeout no debería crear dos facturas. La idempotencia es el mecanismo que lo garantiza.


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.


POST /v1/invoices HTTP/1.1
Authorization: Bearer sk_test_…
Idempotency-Key: 5ad2c4a1-49a0-46e7-95d6-f3d61f08c4ab
Content-Type: application/json
  • Obligatorio en todos los POST que 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.

EscenarioQué devuelve Emitia
Primera petición → éxito 201Respuesta 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 proceso202 Accepted con estado actual
Mismo key + body diferente409 idempotency_conflict

El header de respuesta Emitia-Idempotency-Cached: true te confirma que recibiste una respuesta cacheada, no una nueva ejecución.


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.

Node.js
import { randomUUID } from 'crypto';
const idempotencyKey = randomUUID(); // "5ad2c4a1-..."
// Guarda en tu DB antes de llamar a la API
await db.orders.update({ id: orderId, emitia_idempotency_key: idempotencyKey });
// Llama a la API con esa clave
const 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({ ... }),
});
# Python
import uuid
idempotency_key = str(uuid.uuid4())
// PHP
$idempotency_key = bin2hex(random_bytes(16));
// o: $idempotency_key = (string) \Ramsey\Uuid\Uuid::uuid4();

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.


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.