Tutorial paso a paso · Modelo C
Esta guía es un walkthrough completo del Modelo C — widget_betickets_payment (widget embebido + cobro beTickets). Está pensada para un integrador que parte de cero y necesita poner el widget en producción en el site de su canal.
Si todavía estás eligiendo modelo de integración, consulta primero la tabla comparativa o el overview.
Demo ejecutable
El repositorio incluye una demo lista para abrir en el navegador en ticketera/sales-channels/sala-el-sol/. Refleja exactamente lo que hace esta guía. Si en algún paso te pierdes, abre ese HTML como referencia.
1. ¿Cuándo elegir el Modelo C?
Elige este modelo si todas estas afirmaciones son ciertas para tu canal:
- Quieres que el comprador seleccione asientos dentro de tu propio site (no en un portal externo) — branding total, sin redirecciones para descubrir entradas.
- No quieres asumir el cobro: no tienes integración con un TPV, no quieres gestionar PCI-DSS, ni emitir facturas como vendedor.
- Aceptas que el comprador sea redirigido al portal de beTickets en el último paso (entrada de datos personales + pago con Redsys), con el branding del canal aplicado.
Si quieres mantener al comprador 100% en tu site y cobrar tú, mira el Modelo D. Si no necesitas widget, mira los Modelos A y B.
2. Lo que hace y lo que no hace el integrador
| Responsabilidad | Modelo C |
|---|---|
| Mostrar el catálogo de eventos en el site | Integrador (puede listar eventos como prefiera) |
| Selección de sesión y asientos | Widget (lo entrega beTickets) |
| Botón "Ir al pago" | Widget (lo pinta automáticamente) |
| Recogida de datos del comprador | Portal beTickets (tras redirect) |
| Cobro con pasarela | beTickets (Redsys) |
| Envío de entradas por email | beTickets |
Notificar pago a beTickets (/confirm) | No aplica — beTickets cierra la orden internamente |
Llamada a /auth/token | No aplica desde el integrador — el widget la hace por dentro |
Resumen: el integrador solo embebe el widget. Todo lo demás lo gestiona el widget + el portal de beTickets.
3. Cómo funciona internamente (resumen de un minuto)
TU SITE │ beTickets
│
comprador ──► tu página de evento │
│ │
│ <div id="bt-widget">… │
│ │
▼ │
widget.iife.js ──── POST /sales-channel/v1/auth/token (widget_key → Bearer)
│ ──── GET /sales-channel/v1/{slug}/config (modo + branding)
│ ──── GET /sales-channel/v1/{slug}/{lang}/sessions/{id}
│ │
selección de asientos │
(crea orden vacía + añade seats) │
│ │
▼ │
botón "Ir al pago" │
│ │
└─► window.location.href = │
{portal}/{slug}/{lang}/checkout?order_token=…
│
▼
Portal beTickets
(datos + Redsys + email)El widget detecta el modelo en /config (integration_mode = "widget_betickets_payment") y se configura solo:
showCheckoutButton = true→ muestra el botón "Ir al pago".checkoutBaseUrl→ toma el valor deportal_checkout_base_urlque devuelve el backend.checkoutMode = 'redirect'→ ignora cualquiercheckoutModepasado eninit().
Si pasas checkoutMode o checkoutBaseUrl en init(), el widget emite un console.warn y aplica los valores del backend de todas formas.
4. Paso 1 — Configurar el canal en el dashboard
Esto lo hace el equipo de beTickets (o un usuario con permiso de admin) en el dashboard de administración.
- Administración → Canales de venta → [tu canal] → Configuración
- En "Modelo de integración" selecciona
widget_betickets_payment. - Comprueba que el campo "URL base del portal de checkout" (
portal_checkout_base_url) apunta al portal que el comprador debe ver tras pulsar "Ir al pago". Lo normal es:- Producción:
https://tickets.bticketing.com - Preproducción:
https://tickets-pre.betickets.io
- Producción:
- En "Branding": logo, colores (
accent_color,background_color,text_color), tipografía. El widget los aplica automáticamente. - En "Widget keys" genera una nueva
widget_key(formatowk_…). Es el secreto que identifica al canal desde el frontend. Trátala como pública (es válida solo desde el dominio que autorices en CORS), pero rótala si sospechas que se filtró.
No mezcles entornos
Cada entorno (prod/pre) tiene sus propias widget_key y portal_checkout_base_url. Equivocarte de entorno es el error de integración más frecuente.
5. Paso 2 — Embeber el widget
En la página donde quieres mostrar el selector de asientos:
<!-- 1. Contenedor (el ID puede ser cualquiera) -->
<div id="bt-widget"></div>
<!-- 2. Script del widget -->
<script src="https://widget.bticketing.com/v1/widget.iife.js"></script>
<!-- 3. Inicialización -->
<script>
BeTickets.init({
container: '#bt-widget',
channelSlug: 'sala-el-sol', // tu slug
sessionId: 11, // ID de la sesión a vender
apiKey: 'wk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
lang: 'es', // 'es' | 'en' (default: 'es')
});
</script>Eso es todo lo que necesitas para el Modelo C. El widget hace por dentro:
- Inyecta sus estilos en
<style id="bt-widget-inline-styles">en el<head>. - Llama a
POST /api/sales-channel/v1/auth/tokencon tuwidget_keyy obtiene un Bearer token. - Llama a
GET /api/sales-channel/v1/{channelSlug}/configy aplica branding + detecta el modelo. - Llama a
GET /api/sales-channel/v1/{channelSlug}/{lang}/sessions/{sessionId}para pintar el mapa de butacas. - Cuando el comprador añade asientos: crea la orden, añade items, mantiene el
order_tokenenlocalStoragepara sobrevivir a reloads. - Cuando se pulsa "Ir al pago":
window.location.href = {portal}/{channelSlug}/{lang}/checkout?order_token={token}.
Parámetros mínimos vs opcionales
| Parámetro | Requerido | Notas |
|---|---|---|
container | Sí | Selector CSS o HTMLElement. Debe existir antes de llamar init(). |
channelSlug | Sí | El slug del canal en el dashboard. |
sessionId | Sí | El widget muestra una sesión por instancia. Para varias sesiones, mira §8. |
apiKey | Sí | La widget_key del canal. |
lang | No | 'es' por defecto. |
apiBaseUrl | No | Útil solo en local (http://localhost:8000/api) o pre. En prod no lo pases. |
theme | No | Sobreescribe el branding del canal. Úsalo solo si necesitas variar visuales por página. |
onOrderUpdate | No | Útil para tracking; ver §7. |
onCheckoutStart | No | Útil para tracking; ver §7. |
onError | No | Recomendado: loggea errores del widget en tu monitor. |
checkoutMode | No pasar | El widget lo ignora en Modelo C y emite warning. |
checkoutBaseUrl | No pasar | El widget lo ignora en Modelo C y emite warning. |
6. Paso 3 — Aislar el contenedor (CSS)
El widget aplica sus propios estilos dentro del contenedor mediante variables CSS:
| Variable | Origen | Sobreescrita por |
|---|---|---|
--bt-primary | /config → accent_color | theme.primaryColor |
--bt-bg | /config → background_color | — |
--bt-text | /config → text_color | — |
--bt-font | /config → font_family | theme.fontFamily |
--bt-radius | (no enviada por backend) | theme.borderRadius |
Si tu site tiene un CSS oscuro o muy agresivo (resets globales, *, variables --bg/--text propias) puede colarse en el widget. Aísla el contenedor con un wrapper neutro:
<style>
/* Aislar el host del widget si tu site tiene CSS oscuro/agresivo */
#bt-widget {
background: #ffffff;
color: #111827;
border-radius: 12px;
overflow: hidden;
/* Cuidado: variables con el mismo nombre que las tuyas */
--bg: #ffffff;
--text: #111827;
--surface: #f9fafb;
--muted: #6b7280;
--color-primary: #f59e0b;
}
</style>
<div id="bt-widget"></div>La demo sales-channels/sala-el-sol/index.html muestra exactamente este patrón para integrar el widget dentro de un layout oscuro.
7. Paso 4 — Tracking y analytics (opcional)
El widget expone callbacks que no afectan al flujo, pero te dan ganchos para enviar eventos a tus herramientas de marketing/analytics:
<script>
BeTickets.init({
container: '#bt-widget',
channelSlug: 'sala-el-sol',
sessionId: 11,
apiKey: 'wk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
lang: 'es',
// Cambio en el carrito (selección/deselección de asientos)
onOrderUpdate: (order) => {
const items = (order?.events ?? [])
.flatMap(ev => (ev.sessions ?? []).flatMap(s => s.items ?? []));
window.dataLayer?.push({
event: 'bt_cart_updated',
cart_items: items.length,
cart_value: Number(order?.total_amount ?? 0),
});
},
// Justo antes del redirect al portal de beTickets
onCheckoutStart: () => {
window.dataLayer?.push({ event: 'bt_checkout_initiated' });
window.fbq?.('track', 'InitiateCheckout');
},
// Errores del widget (auth, /config, API)
onError: (err) => {
console.error('[bt-widget]', err);
// Sentry, Datadog, etc.
},
});
</script>Conversión Purchase
El evento Purchase ocurre en el portal de beTickets, fuera de tu site. Si necesitas atribuirlo en tu lado, dos opciones:
- Configurar Meta Conversions API / GA4 Measurement Protocol en el dashboard (beTickets dispara los eventos server-to-server con los identificadores del comprador).
- Pasar parámetros UTM en el redirect; el portal los reenvía a los pixels configurados.
Coordina con el equipo de beTickets antes de duplicar tracking.
8. Paso 5 — Varios eventos en la misma página
El widget mantiene un cliente HTTP y un Bearer token compartidos a nivel de módulo. Por eso solo puedes tener una instancia activa a la vez por canal. Si necesitas cambiar de sesión (por ejemplo, al pulsar en otro evento de un listado lateral):
<script>
let instance = BeTickets.init({
container: '#bt-widget',
channelSlug: 'sala-el-sol',
sessionId: 11,
apiKey: WIDGET_KEY,
lang: 'es',
});
function showSession(newSessionId) {
instance.destroy(); // imprescindible
instance = BeTickets.init({
container: '#bt-widget',
channelSlug: 'sala-el-sol',
sessionId: newSessionId,
apiKey: WIDGET_KEY,
lang: 'es',
});
}
</script>Antipatrón a evitar: dos BeTickets.init() activos a la vez en la misma página con el mismo canal. Romperás la autenticación de la primera instancia.
9. Probar localmente
El repositorio incluye un demo HTML del Modelo C que sirve como referencia ejecutable:
ticketera/sales-channels/sala-el-sol/index.htmlPara probarlo en tu Laragon local:
- Levanta la API:
cd ticketera && composer dev(ophp artisan serve). - Build del widget:
cd ticketera-widget && npm run build. - Abre en el navegador:
http://localhost/beTickets/ticketera/sales-channels/sala-el-sol/. - Las URLs del demo apuntan a
http://localhost:8000/api(API) y al portal enhttp://localhost:5174(ticketera-portal/ npm run dev).
Cuando pulses "Ir al pago", verás que el redirect va a http://localhost:5174/sala-el-sol/es/checkout?order_token=.... Si tienes el portal corriendo, completarás el flujo en tu máquina.
10. Checklist antes de producción
Antes de publicar el widget en el site del canal en producción:
- [ ]
widget_keyde producción (no de pre). Verifica el prefijowk_…con el equipo de beTickets. - [ ] Modelo del canal en el dashboard =
widget_betickets_payment. Confirma conGET /api/sales-channel/v1/{slug}/configqueintegration_modedevuelve exactamente ese valor. - [ ]
portal_checkout_base_urldel canal =https://tickets.bticketing.com(o tu CNAME, p. ej.tickets.tucanal.com). - [ ] El dominio donde se sirve la página está autorizado en CORS / Origin allowlist de beTickets. Confirma con
Origin: https://tu-dominio.comen una request real. - [ ] El branding (logo, colores, tipografía) se aplica como esperas. Si no, comprueba que el contenedor está aislado del CSS del site.
- [ ] No pasas
checkoutModenicheckoutBaseUrleninit(). Si los pasas, mira losconsole.warndel widget. - [ ] El callback
onErrorestá conectado a tu sistema de logging. - [ ] Multi-sesión: si listas varias sesiones, llamas
destroy()antes de cadainit()nuevo. - [ ] HTTPS en producción. El portal de beTickets solo redirige bajo HTTPS.
- [ ] Probado el flujo end-to-end con una tarjeta real de pruebas: selección → ir al pago → datos → Redsys (3D Secure) → email con entradas.
- [ ] Idempotencia probada: si el comprador pulsa "Atrás" desde el portal y vuelve al site, el
order_tokensigue enlocalStoragey el carrito se recupera.
Para una checklist más completa de certificación (incluye partes que no son tuyas como pen-test, monitoring), ver Checklist de certificación.
11. Errores comunes y cómo diagnosticarlos
| Síntoma | Causa probable | Solución |
|---|---|---|
[BeTickets] No se encontró el contenedor | El <div id="..."> no existe cuando init() se ejecuta. | Mueve el <script> después del contenedor o usa DOMContentLoaded. |
Error de autenticación. Comprueba la widget key. | La widget_key está revocada, mal copiada, o es de otro entorno. | Regenera en el dashboard y comprueba el prefijo wk_…. |
Este canal no permite el uso del widget embebido | El canal está configurado como portal_only (Modelo A). | Cambia el modelo del canal a widget_betickets_payment en el dashboard. |
console.warn "checkoutMode is overridden by backend" | Pasaste checkoutMode o checkoutBaseUrl en init(). | Quítalos: el widget se autoconfigura. |
| El botón "Ir al pago" no aparece | El canal está en widget_external_payment (Modelo D) o el backend devuelve otro modelo. | Verifica integration_mode en GET /config. |
El redirect va a https://tickets.bticketing.com en vez de a tu CNAME | El campo portal_checkout_base_url del canal no está configurado o apunta al default. | Edita el canal en el dashboard. |
| 401 al hacer cualquier llamada | El Bearer token expiró y el widget no lo renegoció. | Llama instance.destroy() y re-inicializa. |
| Los estilos del widget se rompen con tu site | Tu CSS global (*, resets, variables CSS con mismo nombre) está pisando los del widget. | Aísla el contenedor con un wrapper como en §6. |
| El carrito se vacía al recargar | Has limpiado localStorage o el order_token ha expirado en el backend. | Espera a un expires_at razonable o consume expires_at en tu UI para avisar al comprador. |
Para más casos, consulta el troubleshooting general.
12. Referencias
- Tabla de modelos de integración
- Flujo de pago — Modelo C
- Quickstart Widget
- Referencia de configuración
BeTickets.init - Checklist de certificación
- Demo ejecutable:
ticketera/sales-channels/sala-el-sol/index.html