Saltar al contenido
Tickeep
Índice de la API

Errores y reintentos

Todos los códigos que devuelve la API, qué significa cada uno y cuáles tiene sentido reintentar.

Última actualización: 23 de agosto de 2026

Todos los errores tienen la misma forma, vengan de donde vengan:

{
  "error": "unprocessable",
  "details": { "reason": "soldOut", "ticketTypeId": "6a7209f2c3d4e5f60718293a" }
}

error es un código estable. Está pensado para compararlo con === en tu código, no para enseñárselo a nadie. No cambia de nombre mientras exista /v1.

details es opcional. Solo aparece cuando hay algo que te ayude a corregir la llamada: qué campo falla, qué tarifa se quedó sin stock, qué valores admite un parámetro. Nunca lleva el error interno del servidor —ese se queda en nuestros logs, con tu X-Request-Id al lado—.

Todos los códigos

CódigoHTTPCuándo aparece
invalid_input400El cuerpo o los parámetros están mal formados
not_authenticated401No has mandado credencial
invalid_credentials401La clave es incorrecta, está revocada o ha caducado
forbidden403A la clave le falta el permiso, o el pedido no es suyo
not_found404No existe, o está fuera del ámbito de salas de la clave
method_not_allowed405Método HTTP que ese endpoint no admite
conflict409Transición de estado inválida (confirmar algo ya cobrado)
idempotency_key_reuse409Misma Idempotency-Key con un cuerpo distinto
sold_out409Sin stock, aforo completo o código promocional agotado
unprocessable422Sintaxis correcta pero semánticamente inválido
rate_limited429Has superado tu límite de peticiones
server_error500Fallo interno nuestro
service_unavailable503Una pasarela o integración de la que dependemos está caída

Qué reintentar

Esta es la parte que de verdad importa:

Situación¿Reintentar?Cómo
rate_limited (429)SíEspera lo que diga Retry-After
server_error (500)SíRetroceso exponencial
service_unavailable (503)SíRetroceso exponencial
Timeout o fallo de redSíCon la misma Idempotency-Key si era una escritura
Cualquier 4xxNoRepetirlo da el mismo resultado
Un 4xx no mejora repitiéndolo

Un invalid_input seguirá siendo invalid_input la décima vez. Los 4xx significan que hay algo que arreglar en tu llamada: arréglalo, no insistas. Reintentar en bucle solo te consumirá el límite de uso.

Una excepción que confunde: un fallo de red no es un 4xx. Si la petición se cortó y no sabes si llegó, reintenta —y si era un POST /orders, hazlo con la misma Idempotency-Key, que es exactamente el escenario para el que existe—.

Los que más se ven

sold_out (409)

No quedan entradas. El details te dice por qué, que no siempre es lo mismo:

{ "error": "sold_out", "details": { "reason": "soldOut", "ticketTypeId": "…" } }
reasonQué ha pasado
soldOutEse tipo de entrada no tiene stock
capacityFullEl aforo de la fecha está completo, aunque quedara stock de la tarifa
promoCodeExhaustedEl código promocional agotó sus usos

Puedes recibirlo aunque la disponibilidad dijera que sí. No es una incoherencia: entre que consultaste y vendiste, alguien se llevó las últimas. La reserva es atómica y es la única verdad; /availability es una foto. Si dos compradores se llevan la última entrada a la vez, uno recibe su pedido y el otro este error.

conflict (409)

Has pedido una transición que el pedido no admite:

{ "error": "conflict", "details": { "reason": "notPending", "status": "pagado" } }

Casi siempre es una de dos: el pedido ya estaba pagado, o la reserva caducó y se canceló sola mientras cobrabas. Vuelve a leer el pedido con GET /orders/{reference} para ver en qué estado está y decide desde ahí. Si caducó, hay que abrir la venta otra vez.

unprocessable (422)

La petición se entiende pero no se puede ejecutar. En una venta, el details.reason suele señalar la tarifa:

  • La tarifa es de otra sala.
  • La tarifa está fuera de su ventana de venta.
  • La tarifa es solo de taquilla y estás vendiendo por el canal online (o al revés).
  • La lista de items está vacía.

forbidden (403)

Dos causas, y conviene distinguirlas:

  • Falta el permiso. Estás llamando a /orders con una clave que solo tiene catalog:read.
  • El pedido no es tuyo. Los pedidos creados desde la web de venta o desde la taquilla del panel no se confirman ni se cancelan por API: cada canal tiene su circuito de cobro y su auditoría. Lo verás como details.reason: "notAnApiOrder".

not_found (404)

Recuerda que aquí se mezclan dos cosas a propósito: puede que el recurso no exista, o puede que exista en una sala fuera del ámbito de tu clave. No distinguirlos es lo que impide usar la API para descubrir identificadores ajenos. Si estás seguro de que el id es bueno, revisa el ámbito de la clave.

Un manejador razonable

const REINTENTABLES = new Set(['rate_limited', 'server_error', 'service_unavailable'])

async function llamar<T>(peticion: () => Promise<Response>, intentos = 3): Promise<T> {
  for (let i = 0; i < intentos; i++) {
    let res: Response
    try {
      res = await peticion()
    } catch (err) {
      // Red caída o timeout: transitorio por definición
      if (i === intentos - 1) throw err
      await esperar(2 ** i * 1000)
      continue
    }

    if (res.ok) return (await res.json()) as T

    const { error, details } = await res.json()
    const requestId = res.headers.get('X-Request-Id')

    if (!REINTENTABLES.has(error) || i === intentos - 1) {
      throw new ErrorTickeep(error, details, requestId)
    }

    // En un 429 manda el servidor, no tu backoff
    const espera = error === 'rate_limited'
      ? Number(res.headers.get('Retry-After') ?? 1) * 1000
      : 2 ** i * 1000
    await esperar(espera)
  }
  throw new Error('inalcanzable')
}
El cliente oficial ya trae esto

@tickeep/client reintenta solo los fallos transitorios con retroceso exponencial y expone err.code y err.requestId. Si trabajas en TypeScript, no lo escribas otra vez: cliente de TypeScript.

Guarda el X-Request-Id de los errores

Aunque solo lo hagas para los 5xx. Es lo que convierte «a veces me falla» en un ticket que se resuelve el mismo día.