Idempotencia: reintentar sin duplicar
Cómo funciona la cabecera Idempotency-Key, por qué es obligatoria al crear un pedido y por qué no lo es en el resto.
Última actualización: 23 de agosto de 2026
El problema, con nombres y apellidos
Tu servidor manda POST /orders para reservar dos entradas. Tickeep recibe la petición, reserva el stock, crea el pedido y empieza a devolver la respuesta.
En ese momento se cae la red. O tu proceso se reinicia. O el balanceador corta la conexión a los 30 segundos.
Tú no recibes nada. Desde tu lado, la petición ha fallado. Y lo razonable es reintentar.
Sin idempotencia, ese reintento crea un segundo pedido con su propio stock retenido. Ahora tienes cuatro entradas bloqueadas para un comprador que quería dos, media hora en la que nadie más puede comprarlas, y —si el cobro llega a completarse en los dos— un problema de verdad.
La Idempotency-Key existe para eso y solo para eso.
Cómo se usa
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
- Genera una clave por intento de compra, no por petición HTTP.
- Reutilízala en todos los reintentos de esa misma compra.
- Un UUID v4 va perfecto. Vale cualquier cadena de hasta 200 caracteres, siempre que sea única para esa operación.
// Una vez, al empezar la compra
const idempotencyKey = crypto.randomUUID()
// Y la misma en todos los reintentos
const order = await tickeep.createOrder(
{ venueId, items: [{ ticketTypeId, quantity: 2 }] },
idempotencyKey,
)
Es el fallo más común, y es silencioso: si la generas dentro del reintento, cada intento lleva una clave distinta y no estás protegido de nada. La clave tiene que nacer con el intento de compra y sobrevivir a los reintentos.
Qué hace el servidor
| Situación | Respuesta |
|---|---|
| Clave nueva | Se ejecuta la operación y se guarda el resultado |
| Misma clave, mismo cuerpo, ya completada | Se devuelve la respuesta guardada, sin crear nada |
| Misma clave, cuerpo distinto | 409 idempotency_key_reuse |
| Misma clave, la primera sigue en vuelo | 409 conflict — espera y reintenta |
Las claves se recuerdan 24 horas. Después se olvidan, así que reutilizar una al día siguiente crearía un pedido nuevo.
El ámbito es tu clave de API: dos integraciones distintas nunca colisionan aunque usen la misma cadena, y nadie puede sondear las claves de otro.
Cuerpo distinto, mismo Idempotency-Key
{
"error": "idempotency_key_reuse",
"details": { "reason": "La clave ya se usó con un cuerpo distinto." }
}
Esto no es un reintento, es un error de tu código: casi siempre, una clave reutilizada para una compra nueva. Devolverte la respuesta de la otra petición sería peor que el error —le darías al comprador B las entradas del comprador A—.
Una petición en vuelo
{ "error": "conflict", "details": { "reason": "Una petición con esta clave sigue en curso." } }
Dos reintentos han salido a la vez y el primero todavía no ha terminado. Espera un segundo y vuelve a intentarlo con la misma clave: cuando el primero acabe, recibirás su respuesta.
Los errores no se memorizan
Si la operación falla con un 5xx, la clave se libera. No te quedas atrapado 24 horas recibiendo el mismo error por algo que ya se resolvió: puedes reintentar con la misma clave y se ejecutará de nuevo.
Los sold_out tampoco se memorizan, por lo mismo: si liberas stock cancelando otro pedido, el reintento con la misma clave puede ahora funcionar.
Dónde es obligatoria
/ordersObligatoria. Sin la cabecera, la respuesta es 400 invalid_input con details.header: "Idempotency-Key".
Es el único endpoint donde un reintento a ciegas crea un recurso duplicado. Preferimos exigirla y que te encuentres el 400 mientras integras, a que la descubras el día que se caiga la red en una noche que se agota.
Dónde es opcional
/orders/{reference}/confirm/orders/{reference}/cancel/tickets/{code}/check-inEstas tres ya son idempotentes por construcción. Son transiciones de estado con una guarda atómica: la primera llamada mueve el pedido de pending a paid y la segunda se encuentra con que ya no está pending, así que no repite nada — solo informa de que no aplicaba (409 conflict, o alreadyUsed en un check-in).
Exigir la cabecera ahí solo obligaría a un torno a inventarse un UUID por cada escaneo, sin ganar nada.
Si la mandas de todas formas, se honra: recibirás la respuesta guardada tal cual. Es útil si tienes una cola de reintentos genérica y prefieres no tratar unos endpoints distinto de otros.
Y /payment-link
No lleva idempotencia porque no crea nada: genera un enlace para un pedido que ya existe. Llamarlo dos veces te devuelve un enlace válido las dos veces.
Resumen práctico
Un UUID por intento de compra
Generado antes del primer intento, guardado junto al estado de esa compra.
El mismo en cada reintento
Fallos de red, timeouts, 5xx: siempre la misma clave.
Una clave nueva para una compra nueva
Aunque sea el mismo comprador y las mismas entradas. Si no, recibirás la respuesta de la compra anterior o un idempotency_key_reuse.
Seguir por aquí
El flujo de venta en dos tiempos: reservar, cobrar y emitir. Con los cuatro endpoints que lo componen y qué hace cada uno.
Todos los códigos que devuelve la API, qué significa cada uno y cuáles tiene sentido reintentar.
El circuito completo de una venta hecha desde tu propia web: disponibilidad, reserva, cobro y confirmación por webhook.