Recintos reutilizables, layouts SVG y disponibilidad por evento sin duplicar geometria.
GET
/recintos?estado_id=11&municipio_id=11020
Pública
Listar recintos por ubicacion
Regresa recintos precargados para poblar el selector del admin. Si no hay resultados, el frontend puede usar captura manual.
Response
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"nombre": "Palenque de la Feria de Leon",
"estado_id": "11",
"municipio_id": "11020",
"direccion": "C. Olimpo, Zona Recreativa y Cultural, 37500 Leon de los Aldama, Gto.",
"capacidad_total": 6985,
"venue_layout_id": "palenque-de-leon",
"zonas": [
{
"categoria": "vip",
"capacidad": 1210,
"precio_desde": 1800,
"precio_hasta": 1800
}
]
}
]
}
GET
/venues
Pública
Listar recintos
Regresa recintos fisicos reutilizables. El venue no contiene precios, ventas ni disponibilidad.
Response
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"nombre": "Palenque de la Feria de León",
"ciudad": "León de los Aldama",
"tipo": "palenque",
"layout_id": "00000000-0000-4000-8000-000000000000"
}
]
}
GET
/venues/:id/layout
Pública
Obtener layout de recinto
Regresa geometria SVG por seccion y referencias a seat layouts numerados.
Response
{
"data": {
"venue": {
"id": "00000000-0000-4000-8000-000000000000",
"nombre": "Palenque de la Feria de León"
},
"layout": {
"id": "00000000-0000-4000-8000-000000000000",
"viewBox": {
"x": 0,
"y": 0,
"width": 1000,
"height": 1000
}
},
"sections": [
{
"id": "00000000-0000-4000-8000-000000000000",
"section_id": "VIP-B7",
"name": "Vip B7",
"svgPath": "M0 0H100V100H0Z",
"seatLayoutId": "00000000-0000-4000-8000-000000000000"
}
]
}
}
GET
/eventos/:id/map
Pública
Mapa completo de evento
Combina venue, layout reusable, precios y disponibilidad del evento. La geometria nunca se guarda en el evento.
Response
{
"data": {
"event": {
"id": "00000000-0000-4000-8000-000000000000",
"venue_id": "palenque-de-leon",
"recinto_id": "00000000-0000-4000-8000-000000000000"
},
"venue": {
"id": "00000000-0000-4000-8000-000000000000",
"nombre": "Palenque de la Feria de León"
},
"layout": {
"id": "00000000-0000-4000-8000-000000000000"
},
"sections": [
{
"id": "00000000-0000-4000-8000-000000000000",
"availability": {
"AVAILABLE": 120,
"SOLD": 10
},
"prices": {
"min_price": 1800,
"max_price": 1800,
"moneda": "MXN"
}
}
]
}
}
GET
/eventos/:id/section/:sectionId
Pública
Detalle de seccion
Regresa seat layout, estados de asientos y precios para abrir una seccion numerada o general.
Response
{
"data": {
"section": {
"id": "00000000-0000-4000-8000-000000000000",
"name": "Vip B7"
},
"generatedSeats": [
{
"seat_id": "00000000-0000-4000-8000-000000000000",
"seat_code": "VIP-B7:M:701",
"row": "M",
"column": 701,
"x": 0,
"y": 0
}
],
"availability": [
{
"seat_id": "00000000-0000-4000-8000-000000000000",
"seat_code": "VIP-B7:M:701",
"status": "AVAILABLE"
}
],
"prices": []
}
}
GET
/eventos/:id/section/:sectionId/:subsectionId
Pública
Detalle oficial de subseccion
Regresa solamente los asientos de una subseccion oficial del recinto, con geometria backend para pintar posiciones, huecos y bloques separados sin recalcular en frontend. Disponible inicialmente para Diamante A1-A8 del Palenque de Leon.
Response
{
"data": {
"section": {
"section_id": "diamante",
"name": "Diamante"
},
"subsection": {
"id": "A6",
"name": "Diamante A6",
"seatCount": 28,
"available": 27,
"layout": {
"grid": {
"rows": 10,
"columns": 15,
"cellSize": 34,
"gap": 8
},
"stage": {
"label": "Escenario",
"gridRow": 9,
"startColumn": 1,
"endColumn": 15
}
}
},
"seats": [
{
"seat_id": "00000000-0000-4000-8000-000000000000",
"seat_code": "diamante:F:601",
"row": "F",
"seat_number": 601,
"estado": "AVAILABLE",
"disponibilidad": true,
"precio": 2200,
"grid_row": 2,
"grid_column": 2,
"x": 42,
"y": 42
}
],
"prices": [
{
"tipo_boleto_id": "00000000-0000-4000-8000-000000000000",
"precio": 2200,
"moneda": "MXN"
}
]
}
}
POST
/eventos/:id/reserve
Bearer token
Reservar asientos
Crea holds temporales. PERSON general usa quantity; TABLE usa commercial_unit_ids; PERSON numerado usa UUID canónicos en seats y sólo acepta secciones numbered/mixed asociadas a un tipo PERSON numerado activo del evento.
Request
{
"funcion_id": "uuid opcional",
"section_id": "uuid para mapa",
"event_section_price_id": "uuid para evento sin mapa",
"commercial_unit_ids": [
"uuid de Mesa 7"
],
"quantity": "personas; para TABLE se deriva de commercial_unit_ids",
"seats": [
{
"seat_id": "00000000-0000-4000-8000-000000000000"
}
],
"hold_minutes": 15
}
Response
{
"data": {
"hold_token": "uuid",
"reserved_until": "2026-07-03T18:15:00.000Z",
"sale_mode": "TABLE opcional",
"people_per_unit": 6,
"total_capacity": 12,
"commercial_units": [
{
"id": "00000000-0000-4000-8000-000000000000",
"label": "Mesa 7",
"unit_number": 7,
"capacity": 6
}
],
"pricing": {
"currency": "MXN",
"price_per_unit": 6000,
"subtotal": 12000,
"service_fee": 1800,
"total": 13800
}
}
}
POST
/eventos/:id/release
Bearer token
Liberar reserva
Libera asientos reservados por el usuario autenticado usando hold_token o lista de asientos.
Request
{
"hold_token": "uuid"
}
Response
{
"data": {
"released": [
"VIP-B7:M:701"
]
}
}
POST
/eventos/:id/replace-hold
Bearer token
Reemplazar completamente un hold numerado
Operacion atomica, multiseccion y exclusiva para asientos numerados. El conjunto enviado se convierte en el estado final completo del hold. Requiere el token cuando ya existe un hold activo; un hold ligado a una orden es inmutable. seats: [] con token libera el hold numerado. Errores de contrato usan 400 y conflictos de token, inventario, expiracion u orden usan 409.
Request
{
"hold_token": "uuid opcional",
"seats": [
{
"seat_id": "00000000-0000-4000-8000-000000000000",
"section_code": "oro"
}
],
"hold_minutes": 15
}
Response
{
"data": {
"hold_token": "uuid",
"reserved_until": "2026-07-03T18:15:00.000Z",
"seats": [
{
"seat_id": "00000000-0000-4000-8000-000000000000",
"seat_code": "oro:Q:1101",
"section_code": "oro",
"row": "Q",
"number": "1101",
"status": "RESERVED",
"reserved_until": "2026-07-03T18:15:00.000Z"
}
]
}
}
POST
/reservas
Bearer token
Crear orden desde un hold de asientos
Flujo estandar de reservas: con evento_id y hold_token crea (o reutiliza) la orden pendiente del hold vigente. ticket_holder_names es opcional y, cuando llega, sólo su primer nombre identifica al comprador de toda la orden; no se exige un nombre por mesa o boleto. Si se omite, el backend usa el nombre completo del usuario autenticado. Clientes anteriores pueden seguir enviando varios nombres, pero los boletos nuevos usarán el primero en todos los accesos. El pago se inicia despues con POST /pagos/tarjeta o POST /pagos/oxxo, igual que cualquier otra orden. Si el total final validado por el backend (con cargos y descuentos) es 0, la orden se confirma en la misma transacción sin Stripe: responde status 'pagado', es_gratuita true y boletos_emitidos > 0 con los boletos ya generados; el correo se encola con el mecanismo habitual. Reenviar el mismo hold_token después de esa confirmación devuelve la misma orden gratuita (sin duplicar boletos). es_cortesia es un alias histórico de 'total 0'. GET /reservas/:id también expone boletos_emitidos y es_gratuita: el cliente sólo debe afirmar que los boletos están listos cuando boletos_emitidos > 0.
Request
{
"evento_id": "00000000-0000-4000-8000-000000000000",
"hold_token": "uuid",
"ticket_holder_names": [
"Leonardo López"
],
"form_submission": {
"form_id": "00000000-0000-4000-8000-000000000000",
"answers": [
{
"field_id": "00000000-0000-4000-8000-000000000000",
"value": [
{
"value": "vodka",
"quantity": 3
},
{
"value": "whisky",
"quantity": 2
}
]
}
]
},
"general_form_submission": {
"form_id": "00000000-0000-4000-8000-000000000000",
"answers": [
{
"field_id": "00000000-0000-4000-8000-000000000000",
"value": {
"quantity": 2
}
}
]
}
}
Response
{
"data": {
"orden_id": "00000000-0000-4000-8000-000000000000",
"numero_orden": "KST-ABC123",
"subtotal": 1400,
"cargo_servicio": 210,
"descuento": 0,
"addons_subtotal": 200,
"total": 1810,
"expires_at": "2026-07-03T18:15:00.000Z",
"es_cortesia": false,
"es_gratuita": false,
"status": "pendiente",
"boletos_emitidos": 0
}
}
POST
/pagos/tarjeta
Bearer token
Iniciar o recuperar el pago con tarjeta de una orden
No exige dirección del perfil. Crea el PaymentIntent de la orden pendiente o devuelve el mismo si ya existe (solicitudes repetidas y reintentos tras un rechazo usan el mismo client_secret). Un rechazo de tarjeta no invalida la orden: sigue pendiente, con su inventario, hasta expires_at. Errores: { error, code }. 400 INVALID_REQUEST (body ausente), INVALID_ORDER_ID (orden_id ausente o no UUID), ORDER_ALREADY_PROCESSED, ORDER_EXPIRED, ORDER_TOTAL_INVALID; 404 ORDER_NOT_FOUND (inexistente o ajena); 409 PAYMENT_METHOD_CONFLICT (la orden ya usa OXXO o tiene un pago HeyBanco en curso), PAYMENT_INTENT_CANCELED (crear una nueva orden), EVENT_FUNCTION_FINISHED/EVENT_*; 502/503/429 fallas de Stripe.
Request
{
"orden_id": "00000000-0000-4000-8000-000000000000"
}
Response
{
"data": {
"client_secret": "pi_..._secret_...",
"payment_intent_id": "pi_..."
}
}
POST
/pagos/oxxo
Bearer token
Generar referencia OXXO de una orden
No exige dirección del perfil. Usa nombre y correo de la cuenta. Mismos errores que /pagos/tarjeta, más 400 BUYER_EMAIL_REQUIRED / BUYER_NAME_REQUIRED. Una orden que ya inició pago con tarjeta responde 409 PAYMENT_METHOD_CONFLICT.
Request
{
"orden_id": "00000000-0000-4000-8000-000000000000"
}
Response
{
"data": {
"referencia": "12345678901234",
"vence_at": "2026-07-04T18:00:00.000Z",
"voucher_url": "https://payments.stripe.com/oxxo/...",
"payment_intent_id": "pi_..."
}
}
POST
/reservas/:id/cancelar
Bearer token
Cancelar una orden pendiente propia
Si la orden tiene PaymentIntent, primero lo cancela en Stripe y después libera inventario y descuento. 409 ORDER_ALREADY_PAID si Stripe ya cobró (la orden se confirma), 409 PAYMENT_IN_PROGRESS si el cobro se está procesando, 403 ORDER_FORBIDDEN, 404 ORDER_NOT_FOUND, 400 INVALID_ORDER_ID.
Response
{
"success": true,
"data": {
"orden_id": "00000000-0000-4000-8000-000000000000",
"status": "cancelada"
},
"message": "Orden cancelada exitosamente"
}
GET
/event-forms/function/:funcionId/section/:sectionId
Pública
Formulario y modalidad comercial de una sección
Conserva fields para preguntas y extras. commercial_config es null en secciones normales y contiene PERSON/TABLE, precio canónico, unidades disponibles y capacidad por unidad en secciones configuradas. TABLE siempre publica max_units_per_order como null: su límite es la disponibilidad real de commercial_units. En cortesías PER_UNIT, quantityPerScope se multiplica por las mesas del hold y maxSelections se interpreta por unidad de alcance, no como límite global de la orden. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"commercial_config": {
"sale_mode": "TABLE",
"people_per_unit": 6,
"has_chairs": true,
"inventory_units": 20,
"available_units": 20,
"price_per_unit": 8500,
"min_units_per_order": 1,
"max_units_per_order": null,
"label_format": "ALPHANUMERIC",
"units_per_row": 20
},
"fields": [
{
"field_type": "checkbox",
"required": true,
"config": {
"createsAddon": true,
"includedBenefit": true,
"benefitMode": "ELECTIVE",
"benefitScope": "PER_UNIT",
"quantityPerScope": 1,
"maxSelections": 1,
"options": [
{
"label": "José Cuervo",
"value": "jose_cuervo",
"price": 0
},
{
"label": "Whisky",
"value": "whisky",
"price": 0
}
]
}
}
]
}
}
GET
/event-forms/function/:funcionId/commercial-section/:eventSectionPriceId
Pública
Formulario de una sección comercial sin mapa
Resuelve la experiencia por event_section_prices.id sin consultar seat_map_sections. Devuelve la misma configuración, campos dinámicos y addons del flujo existente. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"source": "TICKET_COMMERCIAL_SECTION",
"event_section_price_id": "00000000-0000-4000-8000-000000000000",
"commercial_section": {
"id": "00000000-0000-4000-8000-000000000000",
"source": "TICKET_COMMERCIAL_SECTION",
"label": "Mesa VIP",
"ticket_type_id": "00000000-0000-4000-8000-000000000000"
},
"commercial_config": {
"sale_mode": "TABLE",
"people_per_unit": 6,
"has_chairs": false,
"label_format": "ALPHANUMERIC",
"units_per_row": 20
},
"fields": []
}
}
GET
/event-forms/function/:funcionId/general-extras
Pública
Extras generales de preventa
Devuelve campos createsAddon disponibles para cualquier comprador de la función, sin depender de su sección. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).
Response
{
"success": true,
"data": {
"source": "FUNCTION_GENERAL",
"fields": []
}
}
GET
/event-forms/function/:funcionId
Bearer token
Listar formularios editables de una función
Admin recibe SECTION y FUNCTION activos e inactivos. Event manager conserva acceso a sus SECTION, pero los FUNCTION se excluyen de su respuesta.
Response
{
"success": true,
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"funcion_id": "00000000-0000-4000-8000-000000000000",
"scope_type": "FUNCTION",
"section_id": null,
"event_section_price_id": null,
"source": "FUNCTION_GENERAL",
"activo": true,
"fields": []
}
],
"total": 1
}
GET
/event-forms/:id
Bearer token
Obtener formulario editable por ID
Devuelve todos los campos, incluidos los desactivados. Los formularios FUNCTION son exclusivamente administrativos; event_manager recibe 403.
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"scope_type": "FUNCTION",
"source": "FUNCTION_GENERAL",
"activo": true,
"version": 1,
"fields": []
}
}
POST
/event-forms
Bearer token
Crear formulario global de extras
Sólo admin puede crear FUNCTION. extras acepta el contrato simplificado; backend deriva key, tipo, orden y configuración técnica. fields sigue disponible para compatibilidad. No admite commercial_config ni cortesÃas.
Request
{
"funcion_id": "00000000-0000-4000-8000-000000000000",
"scope_type": "FUNCTION",
"nombre": "Extras globales",
"extras": [
{
"name": "Cerveza",
"price": 100,
"description": "Opcional",
"maxQuantity": 20
}
]
}
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"scope_type": "FUNCTION",
"source": "FUNCTION_GENERAL",
"activo": true,
"version": 1
},
"message": "Formulario creado correctamente"
}
PUT
/event-forms/:id
Bearer token
Actualizar o reactivar formulario global
Sólo admin puede actualizar o reactivar FUNCTION. extras permite editar por id sin desactivar los extras omitidos. fields conserva la semántica completa anterior.
Request
{
"activo": true,
"extras": [
{
"id": "00000000-0000-4000-8000-000000000000",
"name": "Cerveza",
"price": 120,
"maxQuantity": 20
}
]
}
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"scope_type": "FUNCTION",
"activo": true,
"version": 2
},
"message": "Formulario actualizado correctamente"
}
DELETE
/event-forms/:id
Bearer token
Desactivar formulario global
Sólo admin puede desactivar FUNCTION. Es lógica e idempotente; no elimina respuestas, addons ni snapshots históricos.
Response
{
"success": true,
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"activo": false
},
"message": "Formulario desactivado correctamente"
}
GET
/eventos/funciones/:funcionId/commercial-sections/:eventSectionPriceId/units
Pública
Mesas concretas de una sección comercial
Lista id, label, unit_number, capacity y status. Estados posibles: AVAILABLE, RESERVED, SOLD, BLOCKED y DISABLED. Un RESERVED vencido se expone como AVAILABLE; los demas estados se conservan. Lectura pública solo si el evento padre está publicado, finalizado, archivado o cancelado; en otro estado responde 404 salvo Bearer token de admin, event_manager propietario o customer con su borrador (token inválido = anónimo).
Response
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"label": "Mesa 7",
"unit_number": 7,
"capacity": 6,
"status": "AVAILABLE"
}
]
}