Saltar al contenido
Tickeep
Índice de la API

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,
)
Genérala fuera del bucle de reintentos

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ónRespuesta
Clave nuevaSe ejecuta la operación y se guarda el resultado
Misma clave, mismo cuerpo, ya completadaSe devuelve la respuesta guardada, sin crear nada
Misma clave, cuerpo distinto409 idempotency_key_reuse
Misma clave, la primera sigue en vuelo409 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

POST/orders

Obligatoria. 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

POST/orders/{reference}/confirm
POST/orders/{reference}/cancel
POST/tickets/{code}/check-in

Estas 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.

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.