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ódigo | HTTP | Cuándo aparece |
|---|---|---|
invalid_input | 400 | El cuerpo o los parámetros están mal formados |
not_authenticated | 401 | No has mandado credencial |
invalid_credentials | 401 | La clave es incorrecta, está revocada o ha caducado |
forbidden | 403 | A la clave le falta el permiso, o el pedido no es suyo |
not_found | 404 | No existe, o está fuera del ámbito de salas de la clave |
method_not_allowed | 405 | Método HTTP que ese endpoint no admite |
conflict | 409 | Transición de estado inválida (confirmar algo ya cobrado) |
idempotency_key_reuse | 409 | Misma Idempotency-Key con un cuerpo distinto |
sold_out | 409 | Sin stock, aforo completo o código promocional agotado |
unprocessable | 422 | Sintaxis correcta pero semánticamente inválido |
rate_limited | 429 | Has superado tu límite de peticiones |
server_error | 500 | Fallo interno nuestro |
service_unavailable | 503 | Una 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 red | Sí | Con la misma Idempotency-Key si era una escritura |
| Cualquier 4xx | No | Repetirlo da el mismo resultado |
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": "…" } }
reason | Qué ha pasado |
|---|---|
soldOut | Ese tipo de entrada no tiene stock |
capacityFull | El aforo de la fecha está completo, aunque quedara stock de la tarifa |
promoCodeExhausted | El 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
itemsestá vacía.
forbidden (403)
Dos causas, y conviene distinguirlas:
- Falta el permiso. Estás llamando a
/orderscon una clave que solo tienecatalog: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')
}
@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.
Seguir por aquí
Formatos, nombres, fechas, dinero y cabeceras: lo que se repite en todas las respuestas y no vuelve a explicarse en cada endpoint.
Cómo funciona la cabecera Idempotency-Key, por qué es obligatoria al crear un pedido y por qué no lo es en el resto.
Cuántas peticiones por minuto admite una clave, cómo leer las cabeceras del contador y cómo diseñar una integración que no lo roce.