Introducción
SmartTix.pro es un motor de seating embebible que se integra en tu plataforma de ticketing. Proporciona mapas de asientos interactivos con selección en tiempo real, gestión de holds temporales y un algoritmo de gap-management que maximiza la ocupación de cada venue.
La integración consta de dos partes: un Web Component que renderiza el mapa en el frontend de tu cliente, y una API REST que gestiona holds y confirmaciones desde tu backend.
Quick Start
Integra el viewer en tu página de checkout con 3 líneas de código. El componente se conecta automáticamente a la API para cargar el layout y gestionar la disponibilidad en tiempo real.
1. Incluir el script
<!-- Carga el Web Component desde el CDN -->
<script src="https://cdn.smarttix.pro/venue-viewer.js"></script>
2. Añadir el elemento
<venue-viewer
public-token="evt_abc123def456"
api-url="https://api.smarttix.pro"
max-seats="6"
currency="EUR"
></venue-viewer>
3. Escuchar selecciones
const viewer = document.querySelector('venue-viewer');
viewer.addEventListener('seatSelected', (e) => {
console.log('Seleccionado:', e.detail);
// { seatId, label, sectorName, rowLabel, price, currency, categoryColor }
});
viewer.addEventListener('seatDeselected', (e) => {
console.log('Deseleccionado:', e.detail.seatId);
});
Autenticación
Los endpoints embed son públicos y no requieren autenticación JWT. El acceso se controla mediante el publicToken del evento, un identificador único generado al publicar el evento.
El publicToken es seguro para exponerlo en el frontend. Solo permite operaciones de lectura y holds temporales — no es posible modificar el venue ni la configuración del evento a través de él.
CORS está abierto para todos los orígenes en los endpoints embed, lo que permite integraciones desde cualquier dominio.
Web Component
El viewer es un Custom Element estándar (<venue-viewer>) que encapsula toda la lógica de renderizado, interacción y comunicación en tiempo real. Funciona con cualquier framework o vanilla JS.
Atributos
| Atributo | Tipo | Default | Descripción |
|---|---|---|---|
public-token |
string | — | Requerido. Token público del evento |
api-url |
string | — | Requerido. URL base de la API |
max-seats |
number | 4 |
Máximo de asientos seleccionables |
currency |
string | EUR |
Código de moneda ISO 4217 |
theme |
JSON | light | {"mode":"dark","primaryColor":"#6366F1"} |
pricing |
JSON | auto | Array de precios por categoría. Ver PricingConfig |
show-availability |
string | true |
Mostrar contadores de disponibilidad |
Eventos
Todos los eventos utilizan CustomEvent con bubbles: true y composed: true (atraviesan Shadow DOM).
| Evento | detail |
|---|---|
seatSelected |
|
seatDeselected |
{ seatId: "guid" } |
gaSelected |
|
gaDeselected |
{ sectorId: "guid" } |
Métodos
| Método | Retorno | Descripción |
|---|---|---|
getSelectedSeats() |
SelectedSeat[] |
Devuelve todos los asientos seleccionados |
deselectSeat(seatId) |
void |
Deselecciona un asiento por ID |
clearSelection() |
void |
Limpia toda la selección |
Obtener Seatmap
Devuelve el layout completo del venue con la disponibilidad actual de todos los asientos. Es la llamada inicial que realiza el Web Component al montarse.
Parámetros
| Param | Ubicación | Descripción |
|---|---|---|
publicToken | path | Token público del evento |
Respuesta 200
{
"eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"eventName": "Concierto de Primavera",
"holdDurationMinutes": 10,
"venue": {
"id": "guid",
"name": "Teatro Principal",
"viewportWidth": 1200,
"viewportHeight": 800,
"sectors": [
{
"id": "guid",
"name": "Platea",
"type": "Numbered",
"categoryId": "guid",
"rows": [
{
"id": "guid",
"label": "A",
"seats": [
{
"id": "guid",
"label": "1",
"x": 120.5,
"y": 340.0,
"radius": 12,
"status": "Available",
"isAccessible": false
}
]
}
]
},
{
"id": "guid",
"name": "Pista General",
"type": "GeneralAdmission",
"categoryId": "guid",
"capacity": 500,
"available": 342
}
]
},
"categories": [
{
"id": "guid",
"name": "VIP",
"color": "#6366F1",
"price": 75.00,
"currency": "EUR"
}
]
}
Estados de asiento
| Status | Descripción |
|---|---|
Available | Libre para seleccionar |
Held | Reservado temporalmente por otro usuario |
Booked | Vendido — confirmado tras pago |
Blocked | Bloqueado por el administrador |
Crear Hold
Reserva temporalmente asientos y/o entradas de admisión general. Los asientos quedan bloqueados durante holdDurationMinutes (configurado por evento). Si el hold expira, los asientos se liberan automáticamente.
Request body
{
"seatIds": [
"3fa85f64-5717-4562-b3fc-2c963f66afa6",
"4fb96g75-6828-5673-c4gd-3d074g77bfb7"
],
"sessionId": "sess_x7k2m9",
"gaSelections": [
{
"sectorId": "guid",
"qty": 2,
"ticketType": "Adult"
}
]
}
| Campo | Tipo | Descripción |
|---|---|---|
seatIds | guid[] | IDs de asientos numerados a reservar |
sessionId | string? | Identificador de sesión del comprador |
gaSelections | array? | Selecciones de admisión general |
Respuesta 200
{
"holdId": "hold_a1b2c3d4",
"seatIds": ["guid", "guid"],
"gaSelections": [
{ "sectorId": "guid", "qty": 2, "ticketType": "Adult" }
],
"expiresAt": "2026-03-04T15:10:00Z",
"totalPrice": 195.00,
"currency": "EUR"
}
ORPHAN_SEATS. El Web Component ya previene esta situación en el frontend.Errores
| HTTP | Código | Causa |
|---|---|---|
| 409 | SEATS_UNAVAILABLE | Asientos ya reservados o vendidos |
| 409 | GA_UNAVAILABLE | Capacidad GA excedida |
| 400 | ORPHAN_SEATS | La selección deja asientos aislados |
| 400 | TOO_MANY_SEATS | Máximo 10 asientos por hold |
Confirmar Hold
Confirma un hold activo tras completar el pago. Los asientos pasan de Held a Booked de forma permanente. Esta llamada se realiza desde tu backend, nunca desde el frontend del comprador.
Request body
{
"holdId": "hold_a1b2c3d4",
"sessionId": "sess_x7k2m9",
"externalOrderId": "pi_3MqR5K2eZvKYlo2C0X1a2b3c"
}
| Campo | Tipo | Descripción |
|---|---|---|
holdId | string | Requerido. ID del hold a confirmar |
sessionId | string? | Sesión del comprador (debe coincidir con el hold) |
externalOrderId | string? | ID de tu pasarela de pago (Stripe, Redsys...) |
Respuesta 200
{
"holdId": "hold_a1b2c3d4",
"bookedSeatIds": ["guid", "guid"],
"gaBookings": [
{ "sectorId": "guid", "qty": 2 }
],
"externalOrderId": "pi_3MqR5K2eZvKYlo2C0X1a2b3c"
}
/confirm con el externalOrderId.Liberar Hold
Cancela un hold activo y libera todos los asientos asociados. Los asientos vuelven a estar disponibles inmediatamente.
Parámetros
| Param | Ubicación | Descripción |
|---|---|---|
publicToken | path | Token público del evento |
holdId | path | ID del hold a liberar |
Respuesta 204 No Content
La respuesta no tiene body. Los asientos liberados se notifican a todos los viewers conectados via SignalR.
Tiempo Real — SignalR
SmartTix utiliza SignalR (WebSocket con fallback automático) para propagar cambios de disponibilidad a todos los viewers conectados al mismo evento. El Web Component gestiona la conexión automáticamente.
Conexión
import { HubConnectionBuilder } from '@microsoft/signalr';
const connection = new HubConnectionBuilder()
.withUrl(`https://api.smarttix.pro/hubs/seats`)
.withAutomaticReconnect()
.build();
await connection.start();
// Unirse al grupo del evento
await connection.invoke('JoinEvent', 'evt_abc123def456');
Eventos del servidor
| Evento | Payload | Cuándo |
|---|---|---|
SeatsHeld |
seatIds: string[] |
Otro usuario reserva asientos |
SeatsReleased |
seatIds: string[] |
Un hold expira o se cancela |
SeatsBooked |
seatIds: string[] |
Un hold se confirma (pago completado) |
GaHeld |
gaHolds: {sectorId, qty}[] |
Hold de admisión general creado |
GaReleased |
sectorIds: string[] |
Hold GA liberado |
GaBooked |
gaBookings: {sectorId, qty}[] |
Admisión general confirmada |
// Escuchar cambios de disponibilidad
connection.on('SeatsHeld', (seatIds) => {
// Marcar asientos como no disponibles en tu UI
console.log('Asientos reservados por otro usuario:', seatIds);
});
connection.on('SeatsReleased', (seatIds) => {
// Marcar asientos como disponibles de nuevo
console.log('Asientos liberados:', seatIds);
});
<venue-viewer>, la conexión SignalR se gestiona automáticamente. Solo necesitas implementar la conexión manual si construyes un viewer personalizado.Modelos
SeatStatus
| Valor | Descripción |
|---|---|
Available | Libre para seleccionar |
Held | Reserva temporal activa |
Booked | Venta confirmada |
Blocked | Bloqueado por administración |
Sector Types
| Tipo | Descripción |
|---|---|
Numbered | Filas con asientos numerados. Contiene rows[] con seats[] |
Table | Mesas redondas o rectangulares con asientos alrededor |
GeneralAdmission | Zona de pie con capacity y available |
PricingConfig
Permite sobreescribir los precios de las categorías o definir tipos de entrada por categoría.
// Precio fijo por categoría
[
{ "category": "VIP", "price": 75 },
{ "category": "General", "price": 30 }
]
// Con tipos de entrada
[
{
"category": "Tribuna",
"ticketTypes": [
{ "ticketType": "Adulto", "price": 40 },
{ "ticketType": "Infantil", "price": 20 }
]
}
]
SelectedSeat
Objeto devuelto por getSelectedSeats() y en los eventos seatSelected.
| Campo | Tipo | Descripción |
|---|---|---|
id | string | UUID del asiento |
label | string | Etiqueta visible (ej. "12") |
sectorName | string | Nombre del sector |
rowLabel | string? | Etiqueta de fila (ej. "A") |
price | number | Precio unitario |
currency | string | Código ISO 4217 |
categoryColor | string | Color hex de la categoría |
ticketType | string? | Tipo de entrada si aplica |
Códigos de Error
Todos los errores siguen el formato estándar con un campo error que contiene el código y un campo message con la descripción legible.
{
"error": "SEATS_UNAVAILABLE",
"message": "Los asientos solicitados ya no están disponibles"
}
| HTTP | Código | Descripción |
|---|---|---|
| 400 | ORPHAN_SEATS | La selección deja asientos aislados sin vecinos |
| 400 | TOO_MANY_SEATS | Se superó el máximo de 10 asientos por hold |
| 400 | EVENT_NOT_PUBLISHED | El evento no está abierto para reservas |
| 404 | EVENT_NOT_FOUND | Token público inválido o evento no encontrado |
| 404 | HOLD_NOT_FOUND | Hold ID inválido o no pertenece a esta sesión |
| 409 | SEATS_UNAVAILABLE | Asientos ya reservados o vendidos por otro usuario |
| 409 | GA_UNAVAILABLE | No queda capacidad suficiente en la zona GA |