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);
});
ℹ️
publicToken se obtiene al publicar un evento desde el panel de administración. Es la única credencial necesaria para la integración embed — no se requiere JWT ni API key.

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

AtributoTipoDefaultDescripció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).

Eventodetail
seatSelected
{ seatId: "guid",
  label: "A-12",
  sectorName: "Platea",
  rowLabel: "A",
  price: 45.00,
  currency: "EUR",
  categoryColor: "#6366F1" }
seatDeselected { seatId: "guid" }
gaSelected
{ sectorId: "guid",
  sectorName: "Pista",
  qty: 3,
  price: 25.00,
  currency: "EUR" }
gaDeselected { sectorId: "guid" }

Métodos

MétodoRetornoDescripció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.

GET /embed/events/{publicToken}/seatmap

Parámetros

ParamUbicaciónDescripción
publicTokenpathToken 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

StatusDescripción
AvailableLibre para seleccionar
HeldReservado temporalmente por otro usuario
BookedVendido — confirmado tras pago
BlockedBloqueado 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.

POST /api/embed/{publicToken}/hold

Request body

{
  "seatIds": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "4fb96g75-6828-5673-c4gd-3d074g77bfb7"
  ],
  "sessionId": "sess_x7k2m9",
  "gaSelections": [
    {
      "sectorId": "guid",
      "qty": 2,
      "ticketType": "Adult"
    }
  ]
}
CampoTipoDescripción
seatIdsguid[]IDs de asientos numerados a reservar
sessionIdstring?Identificador de sesión del comprador
gaSelectionsarray?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"
}
⚠️
Gap Management: Si la selección de asientos deja huecos aislados (asientos huérfanos), la API responderá 400 con código ORPHAN_SEATS. El Web Component ya previene esta situación en el frontend.

Errores

HTTPCódigoCausa
409SEATS_UNAVAILABLEAsientos ya reservados o vendidos
409GA_UNAVAILABLECapacidad GA excedida
400ORPHAN_SEATSLa selección deja asientos aislados
400TOO_MANY_SEATSMá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.

POST /api/embed/{publicToken}/confirm

Request body

{
  "holdId": "hold_a1b2c3d4",
  "sessionId": "sess_x7k2m9",
  "externalOrderId": "pi_3MqR5K2eZvKYlo2C0X1a2b3c"
}
CampoTipoDescripción
holdIdstringRequerido. ID del hold a confirmar
sessionIdstring?Sesión del comprador (debe coincidir con el hold)
externalOrderIdstring?ID de tu pasarela de pago (Stripe, Redsys...)

Respuesta 200

{
  "holdId": "hold_a1b2c3d4",
  "bookedSeatIds": ["guid", "guid"],
  "gaBookings": [
    { "sectorId": "guid", "qty": 2 }
  ],
  "externalOrderId": "pi_3MqR5K2eZvKYlo2C0X1a2b3c"
}
ℹ️
Flujo recomendado: Frontend crea hold → redirige a pasarela de pago → tu backend recibe webhook de pago → llama a /confirm con el externalOrderId.

Liberar Hold

Cancela un hold activo y libera todos los asientos asociados. Los asientos vuelven a estar disponibles inmediatamente.

DELETE /embed/events/{publicToken}/holds/{holdId}

Parámetros

ParamUbicaciónDescripción
publicTokenpathToken público del evento
holdIdpathID 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

EventoPayloadCuá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);
});
ℹ️
Si usas el Web Component <venue-viewer>, la conexión SignalR se gestiona automáticamente. Solo necesitas implementar la conexión manual si construyes un viewer personalizado.

Modelos

SeatStatus

ValorDescripción
AvailableLibre para seleccionar
HeldReserva temporal activa
BookedVenta confirmada
BlockedBloqueado por administración

Sector Types

TipoDescripción
NumberedFilas con asientos numerados. Contiene rows[] con seats[]
TableMesas redondas o rectangulares con asientos alrededor
GeneralAdmissionZona 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.

CampoTipoDescripción
idstringUUID del asiento
labelstringEtiqueta visible (ej. "12")
sectorNamestringNombre del sector
rowLabelstring?Etiqueta de fila (ej. "A")
pricenumberPrecio unitario
currencystringCódigo ISO 4217
categoryColorstringColor hex de la categoría
ticketTypestring?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"
}
HTTPCódigoDescripción
400ORPHAN_SEATSLa selección deja asientos aislados sin vecinos
400TOO_MANY_SEATSSe superó el máximo de 10 asientos por hold
400EVENT_NOT_PUBLISHEDEl evento no está abierto para reservas
404EVENT_NOT_FOUNDToken público inválido o evento no encontrado
404HOLD_NOT_FOUNDHold ID inválido o no pertenece a esta sesión
409SEATS_UNAVAILABLEAsientos ya reservados o vendidos por otro usuario
409GA_UNAVAILABLENo queda capacidad suficiente en la zona GA