Implementación del iframe
Tu iframe se comunica con la página parent (el host de Crowder dentro del sitio de la ticketera) usando window.postMessage. No hay HTTP ni backend involucrado en este canal: todo ocurre dentro del browser del usuario.
Todos los mensajes —en ambas direcciones— comparten la misma estructura del protocolo y se diferencian por el campo status (y, para el autosize, por el type).
Mensajes del canal postMessage
Sección titulada «Mensajes del canal postMessage»Hay dos familias de mensajes en el canal postMessage:
- Mensajes de protocolo de negocio (handshake, contexto, selección, error). El discriminador es el
status; no llevantype. type: "display"— configuración visual del iframe (autosize). Acá eltypees obligatorio (es el único discriminador, no llevastatus) y el payload es el camposizes.
status | Dirección | Propósito |
|---|---|---|
ready | iframe → parent | Avisás que el iframe cargó y registró su listener. |
context | parent → iframe | Crowder te envía el contexto (evento, tickets, usuario). Puede repetirse si cambian los items. |
selected | iframe → parent | Preview de la selección. Trae partnerItems para alimentar el resumen del checkout. Podés emitirlo N veces mientras el usuario arma su oferta. |
cleared | iframe → parent | El usuario vació la selección. Limpia el resumen. |
submit | parent → iframe | El usuario presionó Continuar. Crowder te pide que persistas la oferta y devuelvas la interaction. |
submitted | iframe → parent | Respuesta a submit: trae la interaction opaca ya persistida en tu backend en lifecycle valid. |
error | iframe → parent | No pudiste construir o concretar la oferta (incluye fallo al persistir tras submit). |
type: "display" | iframe → parent | Autosize: informás la altura de tu contenido para que el host ajuste el <iframe>. |
Handshake: emitir ready y escuchar context
Sección titulada «Handshake: emitir ready y escuchar context»- Tu iframe carga y ejecuta su JS.
- Registrás el listener de
message. - Emitís
{ status: "ready" }al parent. - El parent valida
event.origin === iframeOriginy responde con{ status: "context", ... }. - Validás
event.origin === parentOriginy procesás el contexto.
const PARENT_ORIGIN = 'https://tickets.ticketera.com';
window.addEventListener('message', (event) => { if (event.origin !== PARENT_ORIGIN) return; const msg = event.data; if (!msg || typeof msg !== 'object') return;
if (msg.status === 'context') { handleContext(msg); }
if (msg.status === 'submit') { handleSubmit(); // persiste la oferta y emite `submitted` (o `error`) }});
window.parent.postMessage( { status: 'ready' }, PARENT_ORIGIN // targetOrigin: nunca uses '*');Payload del contexto
Sección titulada «Payload del contexto»Es autocontenido: no dependas de cookies ni sesiones inferidas del origin del parent.
{ "status": "context", "locale": "es-AR", "currency": "ARS", "eventInfo": { "id": 16349, "name": "Coldplay — River Plate", "startAt": "2026-11-15T21:00:00-03:00" }, "venue": { "name": "Estadio Monumental", "city": "Buenos Aires", "country": "AR" }, "items": [ { "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "show": "2026-04-21T21:00:00-03:00", "sectorName": "Platea Norte", "rateName": "General", "sectionName": "A", "row": "12", "seat": "34", "quantity": 1, "price": 85000.00 } ], "user": { "email": "juan.perez@example.com", "firstName": "Juan", "lastName": "Pérez", "country": "AR" }}venue, user.firstName, user.lastName, user.country son opcionales. En items, row/seat aparecen solo cuando hay asiento numerado, y holder aparece solo cuando el evento exige nominación.
Obligatoriedad del slot (acuerdo, no protocolo)
Sección titulada «Obligatoriedad del slot (acuerdo, no protocolo)»Si tu app embebida es obligatoria para avanzar el checkout o es opcional (aditiva) es un acuerdo entre la ticketera y vos, configurado en el dashboard de Crowder. No viaja en el protocolo: el iframe nunca recibe un flag que distinga “obligatorio” de “opcional”.
La consecuencia operativa es que la app es 100% responsable de habilitar/deshabilitar el botón Continuar de Crowder mediante los mensajes selected y cleared:
- Si tu app es obligatoria, emití
selectedsolo cuando tengas todos los datos obligatorios completos y válidos dentro del iframe. Si el usuario rompe esa validez (borra un campo, deselecciona el ítem único), emitíclearedpara deshabilitar Continuar de nuevo. - Si tu app es opcional, emití
selectedcuando el usuario arme una oferta (para agregarla al resumen) yclearedsi la vacía. Continuar de todas formas queda habilitado en este caso, porque el usuario puede saltear tu slot.
En ambos casos: selected habilita Continuar, cleared lo deshabilita. El partner es el único que sabe si su propio formulario o selección está completo — Crowder no lo puede inferir.
Autosize del iframe (type: "display")
Sección titulada «Autosize del iframe (type: "display")»Sos responsable de informar al host la altura de tu contenido para que Crowder ajuste el <iframe> y no aparezca scrollbar interno. Esto se hace con un mensaje type: "display" que viaja iframe → parent. No lleva status: el discriminador es el type, y el payload es el campo sizes.
{ "type": "display", "sizes": { "iframeHeight": 842 } }Emití display al montar (una vez medido el contenido inicial) y cada vez que el contenido cambia de altura. Usá ResizeObserver, no setInterval.
const PARENT_ORIGIN = 'https://tickets.ticketera.com';const root = document.getElementById('embed-root');
let lastHeight = 0;const sendDisplay = () => { const iframeHeight = root.getBoundingClientRect().height; if (iframeHeight === lastHeight) return; lastHeight = iframeHeight; window.parent.postMessage( { type: 'display', sizes: { iframeHeight } }, PARENT_ORIGIN );};
window.addEventListener('load', sendDisplay);new ResizeObserver(sendDisplay).observe(root);Crowder hace clamp con el min-height del slot (480px mobile, 560px desktop/tablet). Si el contenido es menor, el slot se mantiene en su piso; si es mayor, el iframe crece hasta iframeHeight.
El contexto puede llegarte más de una vez
Sección titulada «El contexto puede llegarte más de una vez»Si el usuario cambia los tickets después de que ya le mostraste una oferta, vas a recibir un nuevo context:
- Si la selección actual sigue siendo válida con los nuevos tickets, no hace falta hacer nada.
- Si ya no es válida, emití
clearedoerrory refrescá tu UI.
TicketItem
Sección titulada «TicketItem»Cada elemento del array items del context:
| Campo | Tipo | Obl. | Descripción |
|---|---|---|---|
uuid | string | sí | Identificador único del item en la sesión. Lo genera Crowder. |
quantity | int | sí | ≥ 1. |
price | number | sí | ≥ 0, hasta 2 decimales. |
show | string ISO 8601 | sí | Fecha y hora de la función. |
sectorName | string | sí | Nombre del sector en Crowder. |
rateName | string | sí | Nombre de la tarifa aplicada. |
sectionName | string | sí | Nombre de la sección. |
row / seat | string | no | Si hay asiento numerado. |
holder | Holder | no | Datos del titular nominado (ver abajo). |
Subobjeto dentro de un TicketItem cuando el evento exige nominación. Cuando holder está presente, los cuatro campos son obligatorios.
| Campo | Tipo | Descripción |
|---|---|---|
firstName | string | Nombre del titular. |
lastName | string | Apellido. |
documentType | enum | DNI, CPF, RG, PASSPORT, OTHER. |
documentNumber | string | Número del documento (sin formato). |
Emitir la selección (preview del resumen)
Sección titulada «Emitir la selección (preview del resumen)»El selected es un preview: alimenta el resumen del checkout (a la izquierda del iframe) con los partnerItems que el usuario tiene armados en este momento. No compromete stock, no persiste nada todavía, y podés emitirlo todas las veces que necesites.
Cuándo emitir:
- Flujos tipo store (catálogo, productos sueltos, agregar/quitar items): emití un
selectedpor cada cambio. Cada agregado, eliminación o reemplazo se refleja en el resumen. - Flujos tipo formulario (una sola oferta que el usuario completa progresivamente, datos del seguro, datos del huésped): emití
selectedrecién cuando el formulario esté completo y válido. Mientras se completa, el iframe es dueño de su UI.
Si el usuario vacía todo, emití cleared y el resumen se limpia.
{ "status": "selected", "partnerItems": [ { "uuid": "HOTEL-3N-PREMIUM", "type": "HOTEL", "description": "Hotel 3 noches · Hilton Buenos Aires", "price": 120000.00, "quantity": 1 } ]}{ "status": "cleared" }{ "status": "error", "error": { "code": "out_of_stock", "message": "El paquete seleccionado ya no está disponible." }}El estado del botón Continuar lo definen los mensajes que emite tu iframe (ver Obligatoriedad del slot):
| Último mensaje del iframe | Continuar |
|---|---|
| (ninguno) | depende del acuerdo: deshabilitado si tu app es obligatoria, habilitado si es opcional |
selected | habilitado |
cleared | deshabilitado |
Continuar: handshake submit / submitted
Sección titulada «Continuar: handshake submit / submitted»Cuando el usuario presiona el botón Continuar del checkout, Crowder no salta directo a HTTP: primero te avisa al iframe vía submit para que persistas la oferta en tu backend y le devuelvas la interaction opaca. Recién con esa interaction Crowder puede llamar al GET de estado y, eventualmente, disparar purchaseReserved.
- El usuario presiona Continuar.
- parent → iframe:
{ status: "submit" }. - Tu iframe toma la última selección (la misma que ya viajó en el último
selected) y llama a tu backend (por ejemploPOST /interactions). Tu backend valida, genera elinteractionopaco y lo persiste en lifecyclevalidcon lospartnerItemscotizados. - iframe → parent:
{ status: "submitted", interaction, currency, partnerItems }. - Crowder hace
GET {endpoint}/{interaction}para revalidar la cotización y sigue el lifecycle por HTTP server-to-server.
{ "status": "submit" }{ "status": "submitted", "interaction": "pkg_abc123", "currency": "ARS", "partnerItems": [ { "uuid": "HOTEL-3N-PREMIUM", "type": "HOTEL", "description": "Hotel 3 noches · Hilton Buenos Aires", "price": 120000.00, "quantity": 1 } ]}{ "status": "error", "error": { "code": "out_of_stock", "message": "El paquete seleccionado ya no está disponible." }}Los partnerItems del submitted deben coincidir con los del último selected que pintó el resumen. Si tu backend tuvo que recortar o ajustar la oferta al persistirla, emití error en lugar de submitted y refrescá la UI del iframe — no devuelvas un submitted con items distintos a los que el usuario está viendo.
interaction
Sección titulada «interaction»- Lo generás vos. Es un string opaco para Crowder.
- Identifica una operación tuya. Aparece solo en
submittedy en las responses HTTP; en los endpoints server-to-server viaja únicamente en la URL. - En este punto representa una cotización (
valid), no un hold.
PartnerItem
Sección titulada «PartnerItem»Cada elemento del array partnerItems:
| Campo | Tipo | Obl. | Descripción |
|---|---|---|---|
uuid | string | sí | Identificador único del item, lo generás vos. |
quantity | int | sí | Siempre 1. Para vender N unidades, emití N partnerItems o usá type: PACKAGE. |
price | number | sí | ≥ 0, hasta 2 decimales. |
type | enum | sí | Ver tabla abajo. |
description | string | sí | Texto visible. ≤ 200 chars. |
Valores de type:
| Valor | Descripción |
|---|---|
INSURANCE | Seguros (viaje, cancelación, asistencia médica, etc.). |
STORE_PRODUCT | Producto físico de tienda (merchandising, retail). |
HOTEL | Alojamiento (noches de hotel, hostel, apart). |
TRAVEL | Transporte y viajes (vuelos, buses, trenes, traslados). |
SERVICE | Servicio o experiencia (tours, actividades, clases). |
PACKAGE | Combo indivisible de varios items vendidos como una unidad. |
Moneda. No viaja por item: vive a nivel raíz y debe coincidir con la del
context.
partnerItems es opcional tanto en selected como en submitted. Si viaja, debe llevar entre 1 y 10 elementos. Si tu iframe es puramente un formulario que habilita Continuar (sin agregar items al carrito), emití selected sin partnerItems para habilitar el botón, y submitted con la interaction (y sin partnerItems) cuando Crowder te lo pida.