Ir al contenido

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

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 llevan type.
  • type: "display" — configuración visual del iframe (autosize). Acá el type es obligatorio (es el único discriminador, no lleva status) y el payload es el campo sizes.
statusDirecciónPropósito
readyiframe → parentAvisás que el iframe cargó y registró su listener.
contextparent → iframeCrowder te envía el contexto (evento, tickets, usuario). Puede repetirse si cambian los items.
selectediframe → parentPreview de la selección. Trae partnerItems para alimentar el resumen del checkout. Podés emitirlo N veces mientras el usuario arma su oferta.
clearediframe → parentEl usuario vació la selección. Limpia el resumen.
submitparent → iframeEl usuario presionó Continuar. Crowder te pide que persistas la oferta y devuelvas la interaction.
submittediframe → parentRespuesta a submit: trae la interaction opaca ya persistida en tu backend en lifecycle valid.
erroriframe → parentNo pudiste construir o concretar la oferta (incluye fallo al persistir tras submit).
type: "display"iframe → parentAutosize: informás la altura de tu contenido para que el host ajuste el <iframe>.
  1. Tu iframe carga y ejecuta su JS.
  2. Registrás el listener de message.
  3. Emitís { status: "ready" } al parent.
  4. El parent valida event.origin === iframeOrigin y responde con { status: "context", ... }.
  5. Validás event.origin === parentOrigin y procesás el contexto.
iframe-handshake.js
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 '*'
);

Es autocontenido: no dependas de cookies ni sesiones inferidas del origin del parent.

status: context
{
"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í selected solo 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í cleared para deshabilitar Continuar de nuevo.
  • Si tu app es opcional, emití selected cuando el usuario arme una oferta (para agregarla al resumen) y cleared si 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.

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
{ "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.

iframe-autosize.js
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.

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í cleared o error y refrescá tu UI.

Cada elemento del array items del context:

CampoTipoObl.Descripción
uuidstringIdentificador único del item en la sesión. Lo genera Crowder.
quantityint≥ 1.
pricenumber≥ 0, hasta 2 decimales.
showstring ISO 8601Fecha y hora de la función.
sectorNamestringNombre del sector en Crowder.
rateNamestringNombre de la tarifa aplicada.
sectionNamestringNombre de la sección.
row / seatstringnoSi hay asiento numerado.
holderHoldernoDatos 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.

CampoTipoDescripción
firstNamestringNombre del titular.
lastNamestringApellido.
documentTypeenumDNI, CPF, RG, PASSPORT, OTHER.
documentNumberstringNúmero del documento (sin formato).

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 selected por 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í selected recié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
}
]
}

El estado del botón Continuar lo definen los mensajes que emite tu iframe (ver Obligatoriedad del slot):

Último mensaje del iframeContinuar
(ninguno)depende del acuerdo: deshabilitado si tu app es obligatoria, habilitado si es opcional
selectedhabilitado
cleareddeshabilitado

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.

  1. El usuario presiona Continuar.
  2. parent → iframe: { status: "submit" }.
  3. Tu iframe toma la última selección (la misma que ya viajó en el último selected) y llama a tu backend (por ejemplo POST /interactions). Tu backend valida, genera el interaction opaco y lo persiste en lifecycle valid con los partnerItems cotizados.
  4. iframe → parent: { status: "submitted", interaction, currency, partnerItems }.
  5. Crowder hace GET {endpoint}/{interaction} para revalidar la cotización y sigue el lifecycle por HTTP server-to-server.
{ "status": "submit" }

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.

  • Lo generás vos. Es un string opaco para Crowder.
  • Identifica una operación tuya. Aparece solo en submitted y 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.

Cada elemento del array partnerItems:

CampoTipoObl.Descripción
uuidstringIdentificador único del item, lo generás vos.
quantityintSiempre 1. Para vender N unidades, emití N partnerItems o usá type: PACKAGE.
pricenumber≥ 0, hasta 2 decimales.
typeenumVer tabla abajo.
descriptionstringTexto visible. ≤ 200 chars.

Valores de type:

ValorDescripción
INSURANCESeguros (viaje, cancelación, asistencia médica, etc.).
STORE_PRODUCTProducto físico de tienda (merchandising, retail).
HOTELAlojamiento (noches de hotel, hostel, apart).
TRAVELTransporte y viajes (vuelos, buses, trenes, traslados).
SERVICEServicio o experiencia (tours, actividades, clases).
PACKAGECombo 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.