Documentación de la API
Conecte su página de venta o su agencia con Boleti: busque corridas, consulte asientos y emita boletos con una API REST sencilla. Publicamos cada sección conforme la vamos liberando.
En esta página
Introducción
La API pública de Boleti le permite vender los boletos de una empresa de transporte desde fuera de Boleti: desde su propia página de venta o desde una agencia integradora que revende a su nombre.
Es una API REST con JSON. Todas las rutas de esta documentación parten de la misma URL base:
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/ping" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"Cuando algo sale mal, la respuesta siempre trae el mismo formato: un statusCode HTTP, un code estable para que su código lo distinga, y un message en español pensado para mostrarse tal cual:
{
"statusCode": 401,
"code": "API_CREDENCIALES_INVALIDAS",
"message": "Las credenciales de la API no son válidas. Revise sus headers X-Api-Key y X-Api-Secret."
}Los montos de dinero siempre viajan como texto con dos decimales (por ejemplo "150.00"), nunca como número. Así evitamos errores de redondeo entre lenguajes:
{
"precioBase": "150.00",
"precioFinal": "135.00"
}Si usted es una integradora: además de las consultas con GET, puede configurar Webhooks para enterarse en tiempo real de las emisiones y cancelaciones de sus boletos. Para conciliar, la fuente de verdad sigue siendo la consulta con GET.
Todas las peticiones POST aceptan el header Idempotency-Key con un UUID v4. Si reintenta la misma petición con la misma llave, no se duplica nada: ni bloqueos ni boletos.
Autenticación
Cada petición se firma con dos headers: su llave pública y su secreto. Ambos se generan al crear una conexión API en app.boleti.mx.
Ingrese a app.boleti.mx y cree una conexión API para su empresa. Ahí obtendrá su llave (empieza con bk_live_) y su secreto (empieza con sk_live_).
Importante: el secreto se muestra una sola vez, justo al crear la conexión en app.boleti.mx. Consérvelo en un lugar seguro; si lo pierde, tendrá que regenerarlo. Nunca lo coloque en el navegador ni en código que viaje al cliente — solo debe residir en su servidor.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
X-Api-Key | header · string | Sí | Su llave pública. Identifica a la empresa y al tipo de conexión. |
X-Api-Secret | header · string | Sí | Su secreto. Solo debe residir en su servidor, nunca en el navegador. |
Idempotency-Key | header · UUID v4 | En POSTs | Envíe un UUID v4 nuevo por operación; si reintenta la misma operación, repita el mismo. |
Para comprobar que sus llaves funcionan, realice una petición a ping:
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/ping" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"Si todo está bien, responde 200 con los datos de su conexión:
{
"empresa": "Esmeralda Travel",
"tipo": "INTEGRADORA",
"rateLimit": 300
}| Código HTTP | Código de error | Descripción |
|---|---|---|
| 401 | API_CREDENCIALES_INVALIDAS | La llave no existe o el secreto no coincide. |
| 403 | PLAN_FEATURE_NOT_INCLUDED | La conexión está desactivada o el plan de la empresa no incluye acceso a la API. |
| 429 | API_RATE_LIMIT_EXCEDIDO | Superó su límite de peticiones por minuto. Espere el tiempo que indique el header Retry-After y reintente. |
Errores
Catálogo completo de los códigos de error de la API, con su statusCode, su code estable y ejemplos de respuesta para que su integración los maneje sin adivinar.
Cuando algo sale mal, la respuesta HTTP trae SIEMPRE el mismo formato: un statusCode (el código HTTP), un code estable en MAYÚSCULAS para que su programa distinga el caso, y un message en español (redactado en tono coloquial, dirigido al pasajero), listo para mostrarse al usuario tal cual. Ramifique su lógica por code, nunca por el texto de message (el texto puede cambiar; el code no):
{
"statusCode": 409,
"code": "PRECIO_CAMBIADO",
"message": "El precio cambió desde que lo consultó. Revise el precio vigente y vuelva a intentar."
}Aplique una regla simple: si el statusCode es 401 o 403, revise sus credenciales o su tipo de conexión; si es 404, el recurso no existe o no pertenece a su empresa; si es 409, hubo una condición de carrera (otro cliente completó la compra antes o el precio cambió) y debe volver a consultar; si es 422, envió algo que su tipo de conexión no puede hacer; y si es 429, la frecuencia de peticiones excedió el límite (ver Límites de peticiones).
Estos son los code que puede devolver la API pública. Recuerde que los montos SIEMPRE viajan como texto con dos decimales ("150.00"), nunca como número — eso también aplica a cualquier message que mencione dinero:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 401 | API_CREDENCIALES_INVALIDAS | Su llave o su secreto no son válidos, o la conexión está desactivada. Verifique que los copió completos. |
| 403 | API_CREDENCIAL_TIPO_NO_PERMITIDO | Su tipo de conexión no puede usar este endpoint (por ejemplo, una integradora intentando un endpoint exclusivo de venta web, o una conexión de venta web intentando uno exclusivo de integradora). |
| 403 | VENTA_WEB_EMISION_DESACTIVADA | La emisión por el sitio web está desactivada para esta empresa. El administrador debe activarla en Ajustes → Ventas. |
| 404 | CLIENTE_FRECUENTE_NO_ENCONTRADO | No hay una tarjeta de cliente frecuente con ese número (o no pertenece a su empresa). Mismo mensaje para toda tarjeta que no resuelve, por seguridad. |
| 404 | BOLETO_NO_ENCONTRADO | No hay un boleto con ese folio (o no pertenece a su empresa). Mismo mensaje para todo folio que no resuelve. |
| 409 | RESERVATION_RACE | Otro consumidor apartó o vendió alguno de esos asientos primero. Vuelva a consultar el mapa de asientos y elija otros. |
| 409 | PRECIO_CAMBIADO | El `precioEsperado` que envió ya no coincide con el precio vigente. Vuelva a consultar `/corridas/:id/asientos` y reintente con el nuevo precio. |
| 422 | CORTE_SOLO_INTEGRADORA | Esa operación de corte es solo para conexiones de integradora. Una conexión de venta web no puede realizarla. |
| 422 | TARIFA_TIER_INACTIVO | El tipo de pasajero (`tarifaTierId`) que envió ya no está activo para esta corrida. Vuelva a consultar los tipos de pasajero y utilice uno vigente. |
| 422 | CF_SOLO_VENTA_WEB | El `clienteFrecuenteId` solo aplica a la venta web propia de la empresa, no a integradoras. Retire ese campo. |
| 429 | API_RATE_LIMIT_EXCEDIDO | Superó su límite de peticiones por minuto. Espere el tiempo que indique el header `Retry-After` y reintente. |
Un mismo statusCode puede traer distintos code (por ejemplo 403 puede ser tipo de conexión no permitido o venta web desactivada). Por eso siempre debe ramificar por code: es la única llave estable para su integración.
Límites de peticiones
Cuántas peticiones por minuto acepta cada conexión, cómo leer el header Retry-After cuando recibe un 429 y buenas prácticas de polling para integradoras.
Cada conexión tiene un límite de peticiones por minuto. El default es 300 por minuto por conexión (puede verlo en su ping, en el campo rateLimit). Es por conexión, no por IP: si reparte su tráfico entre varios servidores con la misma llave, todos suman al mismo cupo.
Cuando lo supera, la API responde 429 con el code API_RATE_LIMIT_EXCEDIDO. No es un error de su integración: la frecuencia de peticiones excedió el límite. Reduzca la frecuencia de peticiones y reintente.
{
"statusCode": 429,
"code": "API_RATE_LIMIT_EXCEDIDO",
"message": "Su llave alcanzó su límite de peticiones por minuto. Espere unos segundos y vuelva a intentar."
}Toda respuesta 429 trae el header Retry-After con los segundos que debe esperar antes de reintentar. Respételo: si reintenta antes, seguirá recibiendo 429 y solo consumirá cupo adicional. Lo ideal es un backoff (espere Retry-After y, si el 429 se repite, aumente progresivamente el intervalo):
# El header Retry-After viene en segundos: espere ese tiempo antes de reintentar.
< HTTP/1.1 429 Too Many Requests
< Retry-After: 12
< Content-Type: application/jsonSi usted es una integradora y consulta disponibilidad por polling (por ejemplo /corridas/:id/asientos), utilice un intervalo de al menos 5 segundos por corrida. Un intervalo menor desperdicia su cupo sin entregarle datos más frescos: la disponibilidad se cachea unos segundos del lado del servidor.
El lookup de clientes frecuentes (/clientes-frecuentes/:numeroTarjeta) tiene además un límite estricto propio, más bajo que el general, para proteger los datos personales. Si realiza muchas búsquedas consecutivas recibirá 429 con code CF_LOOKUP_RATE_LIMIT aunque no haya alcanzado su límite general — aumente el intervalo entre consultas.
Idempotencia
Cómo usar el header Idempotency-Key para reintentar peticiones POST con seguridad, sin duplicar bloqueos de asientos ni boletos emitidos.
Toda petición POST de la API acepta el header Idempotency-Key con un UUID v4. Es su protección ante reintentos: si la conexión se interrumpe y no sabe si el POST se completó, reintente con LA MISMA llave y recibirá exactamente la misma respuesta que la primera vez — sin apartar dos veces, sin emitir dos veces, sin cancelar dos veces.
Aplica a TODOS los POST: bloquear, extender y liberar asientos, emitir boletos y cancelar. Envíe una llave nueva por cada operación de negocio distinta; reserve el reintento con la misma llave únicamente para reintentar ESA misma operación:
# Primer intento: se apartan los asientos 1 y 2.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/asientos/bloquear" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 3f0d5b2a-1c6e-4a8d-9b7f-2e1c0a9d8b7c" \
-H "Content-Type: application/json" \
-d '{
"corridaId": "c3333333-3333-4333-8333-333333333333",
"numeros": [1, 2],
"paradaOrigenTaquillaId": "a1111111-1111-4111-8111-111111111111",
"paradaDestinoTaquillaId": "b2222222-2222-4222-8222-222222222222"
}'
# La red se interrumpió y no supo si la petición funcionó. Reintente con LA MISMA Idempotency-Key:
# recibirá exactamente la misma respuesta (el mismo reservationToken) — NO se aparta dos veces.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/asientos/bloquear" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 3f0d5b2a-1c6e-4a8d-9b7f-2e1c0a9d8b7c" \
-H "Content-Type: application/json" \
-d '{
"corridaId": "c3333333-3333-4333-8333-333333333333",
"numeros": [1, 2],
"paradaOrigenTaquillaId": "a1111111-1111-4111-8111-111111111111",
"paradaDestinoTaquillaId": "b2222222-2222-4222-8222-222222222222"
}'La clave está en generar el UUID v4 una sola vez, ANTES del primer intento, y guardarlo junto a la operación. Todos los reintentos de esa operación (por timeout, error de red, 5xx) usan ese mismo UUID. Para una operación nueva, genere un UUID nuevo.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
Idempotency-Key | header · UUID v4 | Sí | UUID v4 que identifica la operación de negocio. Genérelo una vez por operación y repítalo en cada reintento de ESA operación. |
Una llave queda ligada al cuerpo del primer POST con el que se usó. Si reintenta la misma llave con un cuerpo distinto, se trata de un error de su integración (se mezclaron dos operaciones): genere una llave nueva para cada operación de negocio.
La idempotencia se recuerda por un tiempo acotado (del orden de horas), suficiente para cubrir reintentos y cortes de red. No la utilice como historial permanente: para consultar el estado real de un apartado o un boleto utilice los endpoints GET.
Precios y tipos de pasajero
/catalogo/tipos-pasajeroCada empresa define sus tipos de pasajero (adulto, estudiante, adulto mayor…). Aquí verá cómo leer las tarifas por tipo, qué tipos requieren credencial al abordar y cómo proteger su venta con precioEsperado.
Devuelve la lista de tipos de pasajero activos de la empresa. El orden es el canónico: primero el ADT (Adulto) y luego el resto por código. No incluye porcentajes de descuento a propósito — el precio SIEMPRE lo computa el servidor por corrida y tramo (puede verlo en /corridas y en /corridas/:id/asientos).
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/catalogo/tipos-pasajero" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"Cada tipo incluye su id (el tarifaTierId que usará al emitir), su codigo, su nombre, si requiereCredencial al abordar (por ejemplo estudiante o INAPAM), y el rango de edad opcional edadMinima/edadMaxima:
{
"tiposPasajero": [
{
"id": "7c1e5a90-2b44-4f8e-9d31-0a6b2c3d4e5f",
"codigo": "ADT",
"nombre": "Adulto",
"requiereCredencial": false,
"edadMinima": null,
"edadMaxima": null
},
{
"id": "9f2a3b10-6c55-4a7d-8e21-1b7c3d4e5f60",
"codigo": "EST",
"nombre": "Estudiante",
"requiereCredencial": true,
"edadMinima": null,
"edadMaxima": null
},
{
"id": "1a2b3c40-7d66-4b8e-9f32-2c8d4e5f6071",
"codigo": "INAPAM",
"nombre": "Adulto mayor",
"requiereCredencial": true,
"edadMinima": 60,
"edadMaxima": null
}
]
}Utilice id como el tarifaTierId de cada pasajero al emitir. El nombre es el texto que se muestra en su selector, y requiereCredencial le indica que ese pasajero deberá presentar una identificación al abordar.
El precio del tipo ADT (Adulto) para el tramo CIUDAD DE MEXICO → GUADALAJARA de este ejemplo es $500.00. Nunca calcule el precio por su cuenta: solicítelo con /corridas o /corridas/:id/asientos, que ya incluye tarifasPorTier por asiento y tramo.
Cómo renderizar el croquis
El plano del autobús viaja como un snapshot auto-descriptivo: pisos, filas, tipos de celda (asiento, pasillo, baño, escaleras…), zonas con color y regla de numeración para que renderice el mapa de asientos tal cual es.
El endpoint /corridas/:id/asientos trae dos cosas juntas: el croquis (el plano del autobús como snapshot inmutable del momento en que se creó la corrida) y asientos[] (el estado y el precio de cada asiento para el tramo consultado). Además viaja una leyenda: un objeto que explica el propio JSON para que pueda renderizar el mapa sin leer código de Boleti.
Así luce la leyenda (resumida). Cada piso es una cuadrícula {rows, cols, cells[][]} donde cells[fila][columna] es una celda tipada por type; la fila 0 es el FRENTE (chofer) y la última la parte TRASERA:
{
"descripcion": "El campo `croquis` es el plano del autobús al momento de crear la corrida (snapshot inmutable). Contiene `zonas`, `numberingRule` y uno o dos pisos (`floor1`, `floor2` opcional). Cada piso es una cuadrícula `{rows, cols, cells[][]}` donde `cells[fila][columna]` es una celda tipada por `type`.",
"grid": "Renderice cada piso como una cuadrícula CSS de `cols` columnas (siempre 5 — los buses en México usan 5 carriles) por `rows` filas. La fila 0 es el FRENTE del autobús (chofer) y la última fila es la parte TRASERA. Si existe `floor2`, muestre tabs o vistas separadas por piso.",
"tiposDeCelda": {
"seat": "Asiento vendible. Cruce `number` con el arreglo `asientos[]` de esta misma respuesta para conocer estado y precios por tipo de pasajero.",
"aisle": "Pasillo — espacio vacío transitable, sin render de objeto.",
"driver": "Cabina del chofer (típicamente fila 0).",
"bathroom": "Baño. `gender: 'mixto' | 'damas' | 'caballeros'` cuando la empresa lo distingue.",
"stairs": "Escaleras entre pisos. `direction: 'up' | 'down'` indica hacia dónde suben; `iconRotation` la orientación del ícono.",
"empty": "Celda vacía sin significado — respete el hueco en la cuadrícula. Si trae `partOf`, es la cola de una celda expandida (ver camposComunes.partOf)."
},
"camposDeAsiento": {
"number": "Número visible del asiento (string). Es la llave que conecta croquis ↔ `asientos[]` ↔ bloqueo/emisión.",
"zonaId": "Id de la zona de precio a la que pertenece el asiento (ver `zonas` del croquis) o null si no tiene zona. Asientos de la misma zona comparten precio.",
"position": "Ubicación física derivada de la columna: 'ventanilla' | 'pasillo' | 'centro'."
},
"numberingRule": "Regla con la que se numeraron los asientos (informativa — los `number` ya vienen resueltos): 'LTR_TOP_DOWN' | 'RTL_TOP_DOWN' | 'LTR_BOTTOM_UP' | 'COLUMN_FIRST_LTR' | 'MANUAL_ONLY'.",
"zonas": "Arreglo `zonas[{id, label, color}]` del croquis: zonas de precio del autobús. `color` es un hex sugerido para pintar los asientos de esa zona en el mapa; `label` es el nombre visible (ej. 'Premium').",
"estadosDeAsiento": {
"DISPONIBLE": "El asiento puede bloquearse para el tramo consultado.",
"RESERVADO": "Alguien tiene un bloqueo temporal activo que solapa el tramo consultado. Puede liberarse al expirar — vuelva a consultar.",
"VENDIDO": "Hay un boleto emitido cuyo tramo solapa el consultado. No disponible."
}
}Para renderizar el mapa: recorra cada piso como una cuadrícula, dibuje solo las celdas con significado y, para cada celda type: "seat", cruce su number con el arreglo asientos[] de la misma respuesta — de ahí obtiene el estado (DISPONIBLE/RESERVADO/VENDIDO) y las tarifasPorTier. La versión del schema viaja en croquisSchemaVersion (hoy 1); nunca cambia de forma incompatible dentro de /public/v1.
Catálogo de orígenes y destinos
/catalogo/od-pairsLas terminales de la empresa y qué destinos puede vender desde cada origen, listos para llenar sus selectores de búsqueda.
Devuelve el grafo completo de orígenes y destinos vendibles en un solo request: la forma más rápida de armar sus dos selectores de búsqueda. También existen /catalogo/origenes y /catalogo/destinos?origenTaquillaId= si prefiere cargarlos por separado.
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/catalogo/od-pairs" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"La respuesta trae origenes (solo terminales con al menos un destino vendible) y destinosPorOrigen (un mapa id de origen → [ids de destino]). En este ejemplo, desde CIUDAD DE MEXICO puede vender a GUADALAJARA:
{
"origenes": [
{
"id": "a1111111-1111-4111-8111-111111111111",
"clave": "CDMX",
"nombre": "CIUDAD DE MEXICO",
"direccion": { "calle": "Av. Central 100", "ciudad": "Ciudad de México", "estado": "CDMX", "cp": "09070" },
"geolocation": { "lat": 19.4361, "lng": -99.0719 },
"telefono": "+525555551234",
"gmapsUrl": "https://maps.google.com/?q=19.4361,-99.0719"
},
{
"id": "b2222222-2222-4222-8222-222222222222",
"clave": "GDL",
"nombre": "GUADALAJARA",
"direccion": { "calle": "Av. Juárez 200", "ciudad": "Guadalajara", "estado": "JAL", "cp": "44100" },
"geolocation": { "lat": 20.6597, "lng": -103.3496 },
"telefono": "+523333335678",
"gmapsUrl": "https://maps.google.com/?q=20.6597,-103.3496"
}
],
"destinosPorOrigen": {
"a1111111-1111-4111-8111-111111111111": ["b2222222-2222-4222-8222-222222222222"]
}
}Cada terminal trae id, clave, nombre y datos de ubicación opcionales (direccion, geolocation, telefono, gmapsUrl). Utilice el nombre para mostrar y el id para las llamadas a /corridas.
Con el id del origen y el del destino que elija el usuario, ya puede buscar salidas en /corridas con ese origenTaquillaId y destinoTaquillaId.
Buscar corridas
/corridasBusque las salidas disponibles por fecha, origen y destino, con horarios, asientos libres y tarifas por tipo de pasajero.
Busca las salidas de un día para un tramo. El par origen-destino es obligatorio: sin él no habría precios por tipo de pasajero, y la respuesta pública SIEMPRE trae tarifasPorTier. No se envía canal: el servidor lo fuerza según su tipo de conexión (venta web o integradora).
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
fecha | query · YYYY-MM-DD | Sí | Día de salida en formato `YYYY-MM-DD` (por ejemplo `2026-07-15`). |
origenTaquillaId | query · UUID | Sí | `id` de la terminal de origen, tomado de `/catalogo/od-pairs`. |
destinoTaquillaId | query · UUID | Sí | `id` de la terminal de destino alcanzable desde ese origen. |
limit | query · number (1-50) | No | Máximo de corridas a devolver (1 a 50). Si lo omite, se aplica el default del servidor. |
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/corridas?fecha=2026-07-15\
&origenTaquillaId=a1111111-1111-4111-8111-111111111111\
&destinoTaquillaId=b2222222-2222-4222-8222-222222222222" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"La respuesta trae corridas[] con horarios, asientosDisponibles, las paradas del tramo y las tarifasPorTier del par consultado. Los montos viajan como texto con 2 decimales:
{
"corridas": [
{
"id": "c3333333-3333-4333-8333-333333333333",
"codigo": "0730-0001",
"rutaId": "d4444444-4444-4444-8444-444444444444",
"rutaCodigo": "RUT-0001",
"rutaNombre": "CIUDAD DE MEXICO - GUADALAJARA",
"autobusId": "e5555555-5555-4555-8555-555555555555",
"autobusEconomico": "ECO-102",
"fechaSalida": "2026-07-15",
"horaSalida": "07:30",
"fechaLlegada": "2026-07-15",
"horaLlegada": "14:00",
"capacidadTotal": 44,
"asientosDisponibles": 39,
"paradas": [
{ "orden": 0, "taquillaId": "a1111111-1111-4111-8111-111111111111", "label": "CIUDAD DE MEXICO" },
{ "orden": 1, "taquillaId": "b2222222-2222-4222-8222-222222222222", "label": "GUADALAJARA" }
],
"tarifasPorTier": [
{
"tierId": "7c1e5a90-2b44-4f8e-9d31-0a6b2c3d4e5f",
"tierCodigo": "ADT",
"tierNombre": "Adulto",
"precioBase": "500.00",
"precioFinal": "500.00",
"breakdown": { "zonaDelta": "0.00", "globalDelta": "0.00" }
}
]
}
]
}Conserve el id de la corrida que elija el usuario: lo necesitará para consultar el mapa de asientos en /corridas/:id/asientos y para bloquear.
Asientos de una corrida
/corridas/:id/asientosConsulte el croquis y el estado de cada asiento (libre, ocupado o apartado) con su precio por tipo de pasajero — cada zona del autobús puede tener un precio distinto.
Devuelve el croquis del autobús + el estado y precio de cada asiento para el tramo consultado. El par origen-destino es obligatorio (el estado y el precio dependen del tramo). Consúltelo periódicamente (polling): la disponibilidad cambia conforme otros venden.
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/corridas/c3333333-3333-4333-8333-333333333333/asientos?\
origenTaquillaId=a1111111-1111-4111-8111-111111111111\
&destinoTaquillaId=b2222222-2222-4222-8222-222222222222" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"El estado público de cada asiento colapsa en tres valores: DISPONIBLE, RESERVADO (alguien tiene un apartado temporal que solapa el tramo) o VENDIDO. Nunca verá el token de un apartado ajeno — usted rastrea sus apartados con el token que recibe al bloquear:
{
"corrida": {
"id": "c3333333-3333-4333-8333-333333333333",
"codigo": "0730-0001",
"capacidadSnapshot": 44,
"rutaCodigoSnapshot": "RUT-0001",
"rutaNombreSnapshot": "CIUDAD DE MEXICO - GUADALAJARA",
"autobusEconomicoSnapshot": "ECO-102",
"fechaSalida": "2026-07-15",
"horaSalida": "07:30",
"horaLlegada": "14:00"
},
"croquis": { "zonas": [], "numberingRule": "LTR_TOP_DOWN", "floor1": { "rows": 12, "cols": 5, "cells": [] } },
"croquisSchemaVersion": 1,
"leyenda": { "descripcion": "...", "estadosDeAsiento": { "DISPONIBLE": "...", "RESERVADO": "...", "VENDIDO": "..." } },
"asientos": [
{
"numero": 1,
"zonaId": null,
"fila": 1,
"columna": 0,
"estado": "DISPONIBLE",
"tarifasPorTier": [
{
"tierId": "7c1e5a90-2b44-4f8e-9d31-0a6b2c3d4e5f",
"tierCodigo": "ADT",
"tierNombre": "Adulto",
"precioBase": "500.00",
"precioFinal": "500.00",
"breakdown": { "zonaDelta": "0.00", "globalDelta": "0.00" }
}
]
},
{
"numero": 2,
"zonaId": null,
"fila": 1,
"columna": 1,
"estado": "VENDIDO",
"tarifasPorTier": [
{
"tierId": "7c1e5a90-2b44-4f8e-9d31-0a6b2c3d4e5f",
"tierCodigo": "ADT",
"tierNombre": "Adulto",
"precioBase": "500.00",
"precioFinal": "500.00",
"breakdown": { "zonaDelta": "0.00", "globalDelta": "0.00" }
}
]
}
],
"asientosDisponibles": 39
}Cruce cada asiento del croquis con asientos[] por su numero. Para vender, tome el tarifaTierId del tipo de pasajero de tarifasPorTier y su precioFinal (aquí, ADT = $500.00).
La disponibilidad se cachea unos segundos para soportar el polling. La validación definitiva ocurre en el bloqueo: si dos consumidores piden el mismo asiento, el segundo recibe un 409 de conflicto.
Bloquear asientos
/asientos/bloquearAparte asientos por unos minutos mientras el pasajero termina su compra. También podrá extender el apartado o liberarlo si el pasajero desiste.
Aparte hasta 20 asientos por unos minutos mientras el pasajero termina. Envíe un Idempotency-Key (UUID v4) por operación: si reintenta la misma llamada con la misma llave, no se duplica el apartado. Opcionalmente envíe el header X-Session-Ref con una referencia opaca de la sesión del comprador para trazarla.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/asientos/bloquear" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 3f0d5b2a-1c6e-4a8d-9b7f-2e1c0a9d8b7c" \
-H "Content-Type: application/json" \
-d '{
"corridaId": "c3333333-3333-4333-8333-333333333333",
"numeros": [1, 2],
"paradaOrigenTaquillaId": "a1111111-1111-4111-8111-111111111111",
"paradaDestinoTaquillaId": "b2222222-2222-4222-8222-222222222222"
}'La respuesta trae el reservationToken (consérvelo: es su llave del apartado y la usará al emitir), la lista de numeros apartados, el expiresAt (cuándo vence) y el ttlMinutes:
{
"reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
"expiresAt": "2026-07-15T13:45:00.000Z",
"numeros": [1, 2],
"ttlMinutes": 8,
"paradaOrigenOrden": 0,
"paradaDestinoOrden": 1
}Mientras el pasajero termina, puede renovar el apartado con /asientos/extender, o liberarlo con /asientos/liberar (sin numeros se libera el token completo; con numeros se liberan solo esos asientos). Ambas requieren el mismo reservationToken y su propio Idempotency-Key:
# Renovar el TTL del apartado
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/asientos/extender" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 5c4d3e2f-1a0b-4c9d-8e7f-6a5b4c3d2e1f" \
-H "Content-Type: application/json" \
-d '{ "reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d" }'
# Liberar el apartado (sin "numeros" se libera el token completo)
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/asientos/liberar" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 6d5e4f3a-2b1c-4d0e-9f8a-7b6c5d4e3f2a" \
-H "Content-Type: application/json" \
-d '{ "reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d" }'Errores que debe manejar:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 404 | RESERVATION_NOT_FOUND | El apartado no existe, ya venció o no pertenece a su conexión. Vuelva a consultar los asientos y bloquéelos de nuevo. |
| 409 | SEAT_CONFLICT | Otro consumidor apartó o vendió alguno de esos asientos primero. Vuelva a consultar el mapa y elija otros. |
Emitir boletos
/boletos/emitirEmita los boletos de una reserva: datos de los pasajeros, tipo de pasajero por asiento, precio protegido contra cambios y PDFs listos para entregar.
Este endpoint convierte un apartado en boletos. Envíe el reservationToken que recibió al bloquear y un pasajero por asiento apartado. El precio REAL lo pone SIEMPRE el servidor; precioEsperado es opcional y solo sirve para detectar que su lista de precios quedó desactualizada (si difiere, recibirá 409 PRECIO_CAMBIADO).
Si su conexión es de integradora, cada boleto se emite pagado (estadoPago: COMPLETO) — usted cobra al pasajero y se liquida en su corte. El campo clienteFrecuenteId no aplica a integradoras (recibirá 422). Envíe un Idempotency-Key (UUID v4) para no duplicar boletos si reintenta.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/boletos/emitir" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 7e6f5a4b-3c2d-4e1f-8a9b-0c1d2e3f4a5b" \
-H "Content-Type: application/json" \
-d '{
"reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
"pasajeros": [
{
"numeroAsiento": 1,
"nombre": "JUAN PEREZ",
"telefono": "+5215578776956",
"correo": "juan@example.com",
"tarifaTierId": "7c1e5a90-2b44-4f8e-9d31-0a6b2c3d4e5f",
"precioEsperado": "500.00"
}
]
}'La respuesta trae un boleto por asiento con su folio, estado, estadoPago, montoTotal, el pdf (URLs firmadas del boleto en color y en 80mm) y el resultado del envío por whatsapp:
{
"boletos": [
{
"folio": "ESMER-2026-000001",
"numeroAsiento": 1,
"pasajeroNombre": "JUAN PEREZ",
"estado": "EMITIDO",
"estadoPago": "COMPLETO",
"montoTotal": "500.00",
"anticipoExpiraAt": null,
"priceBreakdown": {
"tierCodigo": "ADT",
"precioBase": "500.00",
"precioFinal": "500.00"
},
"pdf": {
"color1920": "https://<storage>/boletos/ESMER-2026-000001-color.png?sig=...",
"mm80": "https://<storage>/boletos/ESMER-2026-000001-80mm.png?sig=..."
},
"whatsapp": "queued"
}
]
}En este ejemplo el folio es ESMER-2026-000001 y el monto $500.00. Los PDFs son URLs firmadas temporales — vuelva a consultarlas con /boletos/:folio cuando las necesite de nuevo.
Errores que debe manejar:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 409 | PRECIO_CAMBIADO | El `precioEsperado` que envió ya no coincide con el precio vigente. Vuelva a consultar `/corridas/:id/asientos` y reintente con el nuevo precio. |
| 422 | CF_SOLO_VENTA_WEB | El `clienteFrecuenteId` solo aplica a la venta web propia de la empresa, no a integradoras. Retire ese campo. |
| 403 | VENTA_WEB_EMISION_DESACTIVADA | La emisión por el sitio web está desactivada para esta empresa. El administrador debe activarla en Ajustes → Ventas. |
Consultar un boleto
/boletos/:folioConsulte un boleto por su folio y vuelva a obtener sus PDFs cuando los necesite.
Consulta el estado actual de un boleto por su folio y devuelve de nuevo sus PDFs (las URLs firmadas caducan; este endpoint le entrega unas nuevas). Un folio de otra empresa responde 404 — igual que uno inexistente.
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/boletos/ESMER-2026-000001" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"La respuesta trae el estado del boleto, el montoTotal, el canal por el que se vendió y el pdf con URLs firmadas re-generables:
{
"folio": "ESMER-2026-000001",
"numeroAsiento": 1,
"pasajeroNombre": "JUAN PEREZ",
"estado": "EMITIDO",
"estadoPago": "COMPLETO",
"montoTotal": "500.00",
"anticipoExpiraAt": null,
"canal": "API",
"priceBreakdown": {
"tierCodigo": "ADT",
"precioBase": "500.00",
"precioFinal": "500.00"
},
"pdf": {
"color1920": "https://<storage>/boletos/ESMER-2026-000001-color.png?sig=...",
"mm80": "https://<storage>/boletos/ESMER-2026-000001-80mm.png?sig=..."
}
}Como integradora, utilice este endpoint para conciliar: es la fuente de verdad del estado de cada boleto que emitió. Los Webhooks le avisan en tiempo real cuando un boleto suyo se emite o se cancela; aun así, cierre siempre su conciliación contra este GET.
Clientes frecuentes
/clientes-frecuentes/:numeroTarjetaBusque a un cliente frecuente por su número de tarjeta para llenar sus datos en la venta web. Disponible solo para conexiones de venta web.
Busque a un cliente frecuente por su número de tarjeta (16 dígitos) para autocompletar sus datos en el checkout. Solo para conexiones de venta web — una conexión de integradora recibe 403.
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/clientes-frecuentes/4152313412341234" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO"La respuesta trae el clienteFrecuenteId (que luego enviará por pasajero al emitir), el nombre en mayúsculas y el telefono en formato internacional. Esos datos son los que la empresa dio de alta: no los edite, el servidor los sustituye al emitir con cliente frecuente:
{
"numeroTarjeta": "4152313412341234",
"clienteFrecuenteId": "f6666666-6666-4666-8666-666666666666",
"nombre": "MARIA LOPEZ",
"telefono": "+5215512345678"
}Este lookup tiene un límite estricto de consultas por minuto para proteger los datos. Una tarjeta inválida, inexistente o de otra empresa devuelve siempre el mismo 404 (no es posible deducir qué tarjetas existen).
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 404 | CLIENTE_FRECUENTE_NO_ENCONTRADO | No hay una tarjeta con ese número (o no pertenece a su empresa). Mismo mensaje para toda tarjeta que no resuelve, por seguridad. |
| 429 | CF_LOOKUP_RATE_LIMIT | Demasiadas consultas de tarjetas consecutivas. Espere el tiempo que indique el header Retry-After e intente de nuevo. |
Cancelación (integradoras)
/boletos/:folio/cancelarCancele un boleto vendido por su agencia: queda el registro completo de la venta y la cancelación, y el boleto se excluye del corte que se le cobra.
Cancele un boleto que su agencia emitió. Solo para conexiones de integradora — una conexión de venta web recibe 403. La cancelación deja el registro completo de la venta y de la cancelación; no borra nada.
No hay reembolso automático por la API: usted resuelve el reembolso con su pasajero según su propio proceso. Del lado de Boleti, el boleto cancelado simplemente se excluye del corte contable del periodo que se le cobra. Envíe un Idempotency-Key (UUID v4) para que un reintento no falle si el boleto ya quedó cancelado.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/boletos/ESMER-2026-000001/cancelar" \
-H "X-Api-Key: bk_live_SU_LLAVE" \
-H "X-Api-Secret: sk_live_SU_SECRETO" \
-H "Idempotency-Key: 8f7a6b5c-4d3e-4f2a-8b1c-9d0e1f2a3b4c"La respuesta confirma el folio, el nuevo estado (CANCELADO) y la marca de tiempo canceladoAt:
{
"folio": "ESMER-2026-000001",
"estado": "CANCELADO",
"canceladoAt": "2026-07-15T18:20:00.000Z"
}Para enterarse de una cancelación en tiempo real — incluso cuando la realiza la empresa en su taquilla — configure Webhooks: recibirá el evento boleto.cancelado. Para conciliar, la fuente de verdad sigue siendo la consulta: el corte que se le cobra ya trae el efecto de las cancelaciones del periodo, y puede consultar cualquier boleto en /boletos/:folio.
Errores que debe manejar:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 403 | CANCELACION_SOLO_INTEGRADORA | La cancelación por la API es solo para conexiones de integradora. Una conexión de venta web no puede cancelar boletos por esta vía. |
| 404 | BOLETO_NO_ENCONTRADO | No hay un boleto con ese folio (o no pertenece a su empresa). Mismo mensaje para todo folio que no resuelve. |
| 409 | BOLETO_YA_CANCELADO | Ese boleto ya estaba cancelado. La operación es idempotente: no registra la cancelación de nuevo. |
Webhooks
Reciba avisos en tiempo real cuando un boleto emitido con su conexión cambie de estado: Boleti envía una petición POST firmada a la URL que usted configure, sin necesidad de polling.
Un webhook es una petición POST con JSON que Boleti envía a su servidor cuando ocurre un evento de un boleto emitido con su conexión (una emisión o una cancelación). Cada conexión API admite un endpoint receptor. La entrega es al menos una vez (at-least-once): en casos excepcionales puede recibir un evento duplicado — deduplique por el campo id del evento. El orden de llegada NO está garantizado: procese cada evento por su contenido, no por su posición.
Configure su endpoint en app.boleti.mx, en Integraciones → Webhooks, sobre la conexión API correspondiente. La URL debe usar https://. Al crear el endpoint se le muestra su signing secret (inicia con whsec_) UNA sola vez: consérvelo en un lugar seguro; si lo pierde, regenérelo desde el mismo panel. El botón Probar entrega envía un evento test.ping firmado con su secret real para que valide su integración. Cada entrega llega con estos headers:
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
Content-Type | header · string | Sí | Siempre `application/json`. |
User-Agent | header · string | Sí | Siempre `Boleti-Webhooks/1.0`. |
X-Boleti-Event | header · string | Sí | Tipo del evento entregado (por ejemplo `boleto.emitido`). Coincide con el campo `type` del cuerpo. |
X-Boleti-Delivery | header · UUID | Sí | Identificador único de la entrega. Se mantiene estable entre los reintentos de una misma entrega — puede usarlo para rastrearla en sus registros. |
X-Boleti-Signature | header · string | Sí | Firma HMAC del cuerpo con el esquema `t=<epoch>,v1=<hex>`. Verifíquela ANTES de procesar el evento. |
El header X-Boleti-Signature sigue el esquema t=<epoch en segundos>,v1=<hex>: v1 es el HMAC-SHA256 de la cadena t + "." + cuerpo crudo, calculado con su signing secret. Recompute esa firma sobre los bytes exactos que recibió (no re-serialice el JSON), compárela con v1 mediante una comparación de tiempo constante y rechace entregas cuyo t difiera más de 5 minutos de su reloj — cada reintento se firma de nuevo con un t fresco. A continuación, la estructura del header y una verificación de referencia en Node.js:
{
"header": "X-Boleti-Signature: t=1783707727,v1=83f1c5a97e2b4d6089ab3c7e5f1d2a4b6c8e0f9d7b5a3c1e2f4a6b8c0d9e7f31",
"t": "Marca de tiempo Unix en segundos del intento de entrega. Cada reintento se firma de nuevo con un t fresco — tolere a lo más 5 minutos de diferencia contra su reloj.",
"v1": "HMAC-SHA256 en hexadecimal de la cadena t + '.' + cuerpo crudo, calculado con su signing secret (whsec_…)."
}const crypto = require("node:crypto");
// rawBody: los bytes EXACTOS del cuerpo del POST (string o Buffer).
// No re-serialice el JSON parseado — la firma es sobre los bytes crudos.
function verificarFirmaBoleti(secret, rawBody, signatureHeader) {
const partes = new Map(
signatureHeader.split(",").map((par) => par.split("=", 2)),
);
const t = Number(partes.get("t"));
const v1 = partes.get("v1") || "";
const ahoraSeg = Math.floor(Date.now() / 1000);
// Tolerancia recomendada: 5 minutos (300 segundos).
if (!Number.isInteger(t) || Math.abs(ahoraSeg - t) > 300) return false;
const esperada = crypto
.createHmac("sha256", secret)
.update(t + "." + rawBody)
.digest("hex");
if (v1.length !== esperada.length) return false;
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperada));
}Su endpoint debe responder un código 2xx en menos de 10 segundos; cualquier otra respuesta, una redirección o un timeout cuentan como intento fallido. Boleti reintenta hasta 8 veces en total, con espera exponencial a partir de 1 minuto (la ventana completa dura alrededor de 2 horas). Si una entrega agota sus reintentos, se descarta — puede reenviarla manualmente desde el panel. Si 10 entregas consecutivas se agotan, el endpoint se desactiva automáticamente y usted recibe una notificación dentro de app.boleti.mx; reactívelo desde el mismo panel cuando su servidor se recupere.
Buenas prácticas: responda 2xx de inmediato y procese el evento de forma asíncrona; valide la firma ANTES de procesar; deduplique por el id del evento; tolere campos nuevos dentro de data (no use parsers estrictos que rechacen propiedades desconocidas); y concilie siempre contra GET /boletos/:folio — el webhook le avisa, la consulta es la fuente de verdad.
Eventos de webhook
El catálogo de eventos v1 (boleto.emitido y boleto.cancelado), el envelope común que comparten todos y el payload de ejemplo de cada uno.
Todo evento viaja con el mismo envelope: un id único del evento (evt_ seguido de 32 caracteres hexadecimales — la llave con la que usted deduplica), el type, el apiVersion (v1), el createdAt en ISO 8601 y el objeto data con el detalle. Una emisión de N pasajeros genera N eventos, uno por boleto:
{
"id": "evt_4b8f1c6a2e9d7b3f5a0c8e2d6b4f9a1c",
"type": "boleto.emitido",
"apiVersion": "v1",
"createdAt": "2026-07-10T18:22:05.318Z",
"data": {
"boleto": { "...": "..." }
}
}boleto.emitido se envía cuando se emite un boleto con su conexión. data.boleto trae el snapshot público del boleto: folio, estado, estadoPago, canal, corridaId, fechaSalida, numeroAsiento, pasajeroNombre, origen y destino (cada uno con taquillaId, codigo, nombre y orden), montoTotal (texto con dos decimales, nunca número), moneda y anticipoExpiraAt:
{
"id": "evt_9f2c4a6e8b0d1f3a5c7e9b2d4f6a8c0e",
"type": "boleto.emitido",
"apiVersion": "v1",
"createdAt": "2026-07-10T18:22:05.318Z",
"data": {
"boleto": {
"folio": "ESMER-2026-000123",
"estado": "EMITIDO",
"estadoPago": "COMPLETO",
"canal": "API",
"corridaId": "c3333333-3333-4333-8333-333333333333",
"fechaSalida": "2026-07-15T13:30:00.000Z",
"numeroAsiento": 7,
"pasajeroNombre": "JUAN PEREZ",
"origen": {
"taquillaId": "a1111111-1111-4111-8111-111111111111",
"codigo": "CDMX",
"nombre": "CIUDAD DE MEXICO",
"orden": 0
},
"destino": {
"taquillaId": "b2222222-2222-4222-8222-222222222222",
"codigo": "GDL",
"nombre": "GUADALAJARA",
"orden": 1
},
"montoTotal": "550.00",
"moneda": "MXN",
"anticipoExpiraAt": null
}
}
}boleto.cancelado trae el mismo snapshot más dos campos: canceladoAt (cuándo se canceló) y origenCancelacion — EMPRESA si canceló la empresa (por ejemplo, un cajero en taquilla) o API si la cancelación entró por su propia conexión. Este evento le llega AUNQUE la cancelación no haya pasado por la API: si la empresa cancela en su taquilla un boleto emitido por usted, usted se entera de inmediato:
{
"id": "evt_1d3f5a7c9e0b2d4f6a8c1e3b5d7f9a0c",
"type": "boleto.cancelado",
"apiVersion": "v1",
"createdAt": "2026-07-12T16:05:11.902Z",
"data": {
"boleto": {
"folio": "ESMER-2026-000123",
"estado": "CANCELADO",
"estadoPago": "COMPLETO",
"canal": "API",
"corridaId": "c3333333-3333-4333-8333-333333333333",
"fechaSalida": "2026-07-15T13:30:00.000Z",
"numeroAsiento": 7,
"pasajeroNombre": "JUAN PEREZ",
"origen": {
"taquillaId": "a1111111-1111-4111-8111-111111111111",
"codigo": "CDMX",
"nombre": "CIUDAD DE MEXICO",
"orden": 0
},
"destino": {
"taquillaId": "b2222222-2222-4222-8222-222222222222",
"codigo": "GDL",
"nombre": "GUADALAJARA",
"orden": 1
},
"montoTotal": "550.00",
"moneda": "MXN",
"anticipoExpiraAt": null,
"canceladoAt": "2026-07-12T16:05:11.734Z",
"origenCancelacion": "EMPRESA"
}
}
}Compatibilidad: dentro de v1 pueden aparecer campos nuevos en data — su integración debe tolerarlos sin fallar. Además de los eventos de negocio existe test.ping, el evento sintético que envía el botón Probar entrega del panel, firmado con su secret real, para que valide su verificación de firma de extremo a extremo.