Documentación de la API
Conecta tu página de venta o tu agencia con Boleti: busca corridas, consulta asientos y emite 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 te 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 cuelgan de la misma URL base:
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/ping" \
-H "X-Api-Key: bk_live_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO"Cuando algo sale mal, la respuesta siempre trae el mismo formato: un statusCode HTTP, un code estable para que tu código lo distinga, y un message en español pensado para mostrarse tal cual:
{
"statusCode": 401,
"code": "API_CREDENCIALES_INVALIDAS",
"message": "Tu llave o tu secreto no son válidos. Revisa que los hayas copiado completos."
}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 eres integradora: la integración es REST con consultas periódicas (polling). Por ahora no hay webhooks — cuando necesites saber el estado de algo, consúltalo con GET.
Todas las peticiones POST aceptan el header Idempotency-Key con un UUID v4. Si reintentas 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: tu llave pública y tu secreto. Ambos se generan al crear una conexión API en app.boleti.mx.
Entra a app.boleti.mx y crea una conexión API para tu empresa. Ahí obtienes tu llave (empieza con bk_live_) y tu secreto (empieza con sk_live_).
Importante: el secreto se muestra una sola vez, justo al crear la conexión en app.boleti.mx. Guárdalo en un lugar seguro; si lo pierdes, tendrás que regenerarlo. Y nunca lo pongas en el navegador ni en código que viaje al cliente — solo debe vivir en tu servidor.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
X-Api-Key | header · string | Sí | Tu llave pública. Identifica a la empresa y al tipo de conexión. |
X-Api-Secret | header · string | Sí | Tu secreto. Solo debe vivir en tu servidor, nunca en el navegador. |
Idempotency-Key | header · UUID v4 | En POSTs | Manda un UUID v4 nuevo por operación; si reintentas la misma operación, repite el mismo. |
Para comprobar que tus llaves funcionan, pégale al ping:
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/ping" \
-H "X-Api-Key: bk_live_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO"Si todo está bien, responde 200 con los datos de tu 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 | Superaste tu límite de peticiones por minuto. Espera lo que indique el header Retry-After y reintenta. |
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 tu 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 tu programa distinga el caso, y un message en español, coloquial y listo para mostrarle al usuario tal cual. Ramifica tu 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 de este asiento cambió mientras apartabas. Vuelve a consultar los asientos y confirma el nuevo precio."
}Aplica una regla simple: si el statusCode es 401 o 403 revisa tus credenciales o tu tipo de conexión; si es 404 el recurso no existe o no es de tu empresa; si es 409 hubo una carrera (otro te ganó el asiento o el precio cambió) y debes volver a consultar; si es 422 mandaste algo que tu tipo de conexión no puede hacer; y si es 429 bajaste el ritmo (ver Límites de peticiones).
Estos son los code que puede devolver la API pública. Recuerda 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 | Tu llave o tu secreto no son válidos, o la conexión está desactivada. Revisa que los copiaste completos. |
| 403 | API_CREDENCIAL_TIPO_NO_PERMITIDO | Tu tipo de conexión no puede usar este endpoint (por ejemplo, una integradora intentando un endpoint solo de venta web, o al revés). |
| 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 es de tu empresa). Mismo mensaje para toda tarjeta que no resuelve, por seguridad. |
| 404 | BOLETO_NO_ENCONTRADO | No hay un boleto con ese folio (o no es de tu empresa). Mismo mensaje para todo folio que no resuelve. |
| 409 | RESERVATION_RACE | Otro consumidor apartó o vendió alguno de esos asientos primero. Vuelve a consultar el mapa de asientos y elige otros. |
| 409 | PRECIO_CAMBIADO | El `precioEsperado` que mandaste ya no coincide con el precio vigente. Vuelve a consultar `/corridas/:id/asientos` y reintenta 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 hacerla. |
| 422 | TARIFA_TIER_INACTIVO | El tipo de pasajero (`tarifaTierId`) que mandaste ya no está activo para esta corrida. Vuelve a consultar los tipos de pasajero y usa uno vigente. |
| 422 | CF_SOLO_VENTA_WEB | El `clienteFrecuenteId` solo aplica a la venta web propia de la empresa, no a integradoras. Quita ese campo. |
| 429 | API_RATE_LIMIT_EXCEDIDO | Superaste tu límite de peticiones por minuto. Espera lo que indique el header `Retry-After` y reintenta. |
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 ramificas por code: es la única llave estable para tu integración.
Límites de peticiones
Cuántas peticiones por minuto acepta cada conexión, cómo leer el header Retry-After cuando recibes 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 (lo ves en tu ping, en el campo rateLimit). Es por conexión, no por IP: si repartes tu tráfico entre varios servidores con la misma llave, todos suman al mismo cupo.
Cuando lo superas, la API responde 429 con el code API_RATE_LIMIT_EXCEDIDO. No es un error de tu integración: solo vas muy rápido. Bájale al ritmo y reintenta.
{
"statusCode": 429,
"code": "API_RATE_LIMIT_EXCEDIDO",
"message": "Superaste tu límite de peticiones por minuto. Espera unos segundos y reintenta."
}Toda respuesta 429 trae el header Retry-After con los segundos que debes esperar antes de reintentar. Respétalo: si reintentas antes, sigues topado y solo gastas cupo. Lo ideal es un backoff (espera Retry-After, y si vuelve a topar, ve aumentando la espera):
# El header Retry-After viene en segundos: espera ese tiempo antes de reintentar.
< HTTP/1.1 429 Too Many Requests
< Retry-After: 12
< Content-Type: application/jsonSi eres integradora y consultas disponibilidad por polling (por ejemplo /corridas/:id/asientos), usa un intervalo de al menos 5 segundos por corrida. Menos que eso desperdicia tu cupo sin darte 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 haces muchas búsquedas seguidas recibirás 429 con code CF_LOOKUP_RATE_LIMIT aunque no hayas tocado tu límite general — espácialas.
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 tu red de seguridad: si se te cae la conexión y no sabes si el POST funcionó, reintentas con LA MISMA llave y recibes 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. Manda una llave nueva por cada operación de negocio distinta; reserva el reintento con la misma llave solo para reintentar ESA misma operación:
# Primer intento: apartas 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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"
}'
# Se cayó tu red y no supiste si funcionó. Reintenta con LA MISMA Idempotency-Key:
# recibes 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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, generas 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éralo una vez por operación y repítelo en cada reintento de ESA operación. |
Una llave queda ligada al cuerpo del primer POST con el que la usaste. Si reintentas la misma llave con un cuerpo distinto, es un error tuyo (mezclaste dos operaciones): genera 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 uses como historial permanente: para consultar el estado real de un apartado o un boleto usa los endpoints GET.
Precios y tipos de pasajero
/catalogo/tipos-pasajeroCada empresa define sus tipos de pasajero (adulto, estudiante, adulto mayor…). Aquí verás cómo leer las tarifas por tipo, qué tipos piden credencial al abordar y cómo proteger tu venta con precioEsperado.
Trae 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 (lo ves 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO"Cada tipo trae su id (el tarifaTierId que usarás 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
}
]
}Usa id como el tarifaTierId de cada pasajero al emitir. El nombre es el texto que muestras en tu selector, y requiereCredencial te avisa 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 calcules tú el precio: pídelo con /corridas o /corridas/:id/asientos, que ya trae 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 pintes 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 puedas 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": "Renderiza cada piso como una cuadrícula de `cols` columnas por `rows` filas. La fila 0 es el FRENTE del autobús (chofer) y la última fila es la parte TRASERA.",
"tiposDeCelda": {
"seat": "Asiento vendible. Cruza `number` con el arreglo asientos[] de la misma respuesta para conocer estado y precios.",
"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'.",
"empty": "Celda vacía sin significado — respeta el hueco en la cuadrícula."
},
"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 del asiento (ver zonas del croquis) o null. Asientos de la misma zona comparten precio.",
"position": "Ubicación física: 'ventanilla' | 'pasillo' | 'centro'."
},
"numberingRule": "Regla con la que se numeraron los asientos (informativa): 'LTR_TOP_DOWN' | 'RTL_TOP_DOWN' | 'LTR_BOTTOM_UP' | 'COLUMN_FIRST_LTR' | 'MANUAL_ONLY'.",
"zonas": "Arreglo zonas[{id, label, color}] del croquis. `color` es un hex sugerido para pintar los asientos de esa zona; `label` es el nombre visible.",
"estadosDeAsiento": {
"DISPONIBLE": "El asiento puede bloquearse para el tramo consultado.",
"RESERVADO": "Alguien tiene un bloqueo temporal activo que solapa el tramo. Puede liberarse al expirar — vuelve a consultar.",
"VENDIDO": "Hay un boleto emitido cuyo tramo solapa el consultado. No disponible."
}
}Para pintar el mapa: recorre cada piso como una cuadrícula, dibuja solo las celdas con significado, y para cada celda type: "seat" cruza su number con el arreglo asientos[] de la misma respuesta — de ahí sacas 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 puedes vender desde cada origen, listos para llenar tus selectores de búsqueda.
Trae el grafo completo de orígenes y destinos vendibles en un solo request: la forma más rápida de armar tus dos selectores de búsqueda. También existen /catalogo/origenes y /catalogo/destinos?origenTaquillaId= si prefieres cargarlos por separado.
curl "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/catalogo/od-pairs" \
-H "X-Api-Key: bk_live_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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 puedes 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). Usa 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 puedes buscar salidas en /corridas con ese origenTaquillaId y destinoTaquillaId.
Buscar corridas
/corridasBusca 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 mandas canal: el servidor lo fuerza según tu 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 omites, se usa 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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" }
}
]
}
]
}Guarda el id de la corrida que elija el usuario: lo necesitas para consultar el mapa de asientos en /corridas/:id/asientos y para bloquear.
Asientos de una corrida
/corridas/:id/asientosConsulta 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 costar 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últalo 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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ás el token de un apartado ajeno — tú rastreas tus apartados con el token que recibes 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
}Cruza cada asiento del croquis con asientos[] por su numero. Para vender, toma 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 verdad final la valida el bloqueo: si dos consumidores piden el mismo asiento, el segundo recibe un 409 de conflicto.
Bloquear asientos
/asientos/bloquearAparta asientos por unos minutos mientras el pasajero termina su compra. También podrás extender el apartado o liberarlo si el pasajero se arrepiente.
Aparta hasta 20 asientos por unos minutos mientras el pasajero termina. Manda un Idempotency-Key (UUID v4) por operación: si reintentas la misma llamada con la misma llave, no se duplica el apartado. Opcionalmente manda 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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 (guárdalo: es tu llave del apartado y la usarás 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 puedes renovar el apartado con /asientos/extender, o soltarlo con /asientos/liberar (sin numeros sueltas el token completo; con numeros sueltas solo esos asientos). Ambas piden 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO" \
-H "Idempotency-Key: 5c4d3e2f-1a0b-4c9d-8e7f-6a5b4c3d2e1f" \
-H "Content-Type: application/json" \
-d '{ "reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d" }'
# Soltar el apartado (sin "numeros" 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO" \
-H "Idempotency-Key: 6d5e4f3a-2b1c-4d0e-9f8a-7b6c5d4e3f2a" \
-H "Content-Type: application/json" \
-d '{ "reservationToken": "8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d" }'Errores que debes manejar:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 404 | RESERVATION_NOT_FOUND | El apartado no existe, ya venció o no es de tu conexión. Vuelve a consultar los asientos y bloquéalos de nuevo. |
| 409 | SEAT_CONFLICT | Otro consumidor apartó o vendió alguno de esos asientos primero. Vuelve a consultar el mapa y elige otros. |
Emitir boletos
/boletos/emitirEmite los boletos de una reserva: datos de los pasajeros, tipo de pasajero por asiento, precio protegido contra cambios y PDFs listos para entregar.
Convierte un apartado en boletos. Manda el reservationToken que recibiste 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 tu lista de precios quedó vieja (si difiere, recibes 409 PRECIO_CAMBIADO).
Si tu conexión es de integradora, cada boleto se emite pagado (estadoPago: COMPLETO) — tú cobras al pasajero y se liquida en tu corte. El campo clienteFrecuenteId no aplica a integradoras (recibirás 422). Manda un Idempotency-Key (UUID v4) para no duplicar boletos si reintentas.
curl -X POST "https://boleti-e7ebfya2hsgqbrgf.mexicocentral-01.azurewebsites.net/api/v1/public/v1/boletos/emitir" \
-H "X-Api-Key: bk_live_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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 — vuelve a consultarlas con /boletos/:folio cuando las necesites de nuevo.
Errores que debes manejar:
| Código HTTP | Código de error | Descripción |
|---|---|---|
| 409 | PRECIO_CAMBIADO | El `precioEsperado` que mandaste ya no coincide con el precio vigente. Vuelve a consultar `/corridas/:id/asientos` y reintenta con el nuevo precio. |
| 422 | CF_SOLO_VENTA_WEB | El `clienteFrecuenteId` solo aplica a la venta web propia de la empresa, no a integradoras. Quita 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/:folioConsulta un boleto por su folio y vuelve a obtener sus PDFs cuando lo necesites.
Consulta el estado actual de un boleto por su folio y obtén de nuevo sus PDFs (las URLs firmadas caducan; este endpoint te da unas frescas). 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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, usa este endpoint para reconciliar: es la fuente de verdad del estado de cada boleto que emitiste, sin necesidad de webhooks.
Clientes frecuentes
/clientes-frecuentes/:numeroTarjetaBusca 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.
Busca 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_SECRETO"La respuesta trae el clienteFrecuenteId (que luego mandas 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 edites, 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 puedes 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 es de tu empresa). Mismo mensaje para toda tarjeta que no resuelve, por seguridad. |
| 429 | CF_LOOKUP_RATE_LIMIT | Demasiadas consultas de tarjetas seguidas. Espera lo que indique el header Retry-After e intenta de nuevo. |
Cancelación (integradoras)
/boletos/:folio/cancelarCancela un boleto vendido por tu agencia: queda el registro completo de la venta y la cancelación, y el boleto se excluye del corte que se te cobra.
Cancela un boleto que tu 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: tú resuelves el reembolso con tu pasajero como acostumbras. Del lado de Boleti, el boleto cancelado simplemente se excluye del corte contable del periodo que se te cobra. Manda 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_TU_LLAVE" \
-H "X-Api-Secret: sk_live_TU_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"
}Como no hay webhooks, tu conciliación es por consulta (polling): el corte que se te cobra ya trae el efecto de las cancelaciones del periodo. Consulta el estado de cualquier boleto cuando lo necesites en /boletos/:folio.
Errores que debes 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 aquí. |
| 404 | BOLETO_NO_ENCONTRADO | No hay un boleto con ese folio (o no es de tu empresa). Mismo mensaje para todo folio que no resuelve. |
| 409 | BOLETO_YA_CANCELADO | Ese boleto ya estaba cancelado. La operación es idempotente: no vuelve a registrar la cancelación. |