Cuadro maestro de interacciones
Cuadro único con todas las interacciones del protocolo de Aplicaciones Embebidas: handshake del iframe, selección del usuario, autosize, lifecycle HTTP (cotización, reserva, compra, devolución, expiración) y acks. Cubre el happy path, el flujo de devolución (parcial o total) y, en la última columna, qué pasa cuando cada interacción falla.
Para detalles de payload por interacción ver Endpoint de estado (GET) y Webhooks de eventos (POST).
Cuadro maestro de interacciones
Sección titulada «Cuadro maestro de interacciones»| # | Fase | Mensaje | Transporte | type / verbo | status | Emisor → Receptor | Trigger | Campos que viajan | Efecto / Reacción | Si falla |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Handshake | Iframe listo | postMessage | — | ready | Iframe → Crowder | El iframe cargó y registró su listener. | status | Crowder dispara (2). | No llega (CSP, mixed content, error JS): Crowder no manda context. Si la app es obligatoria por acuerdo, Continuar bloqueado — checkout muerto. Si es opcional, sigue sin oferta del partner. |
| 2 | Handshake | Contexto al iframe | postMessage | — | context | Crowder → Iframe | Tras ready, o cuando cambian los items durante la sesión. | status, locale, currency, eventInfo, venue, items (tickets), user | Iframe pinta UI. La obligatoriedad no viaja en el payload (es acuerdo de dashboard). | event.origin no coincide: el iframe descarta silenciosamente y queda sin UI. Sin daño de protocolo pero el slot queda vacío. |
| 3a | Selección | Selección (preview) | postMessage | — | selected | Iframe → Crowder | El usuario armó una oferta válida y completa. Preview puro: no persiste, no toma stock, no genera interaction. Se puede emitir N veces. | status, currency, partnerItems (1–10) | Actualiza Resumen del checkout y habilita Continuar. | Si el iframe emite selected con datos incompletos, Continuar se habilita y el submit (3d) posterior va a fallar — culpa del partner por emitir antes de validar. |
| 3b | Selección | Selección vacía | postMessage | — | cleared | Iframe → Crowder | Usuario invalidó o vació la selección. | status | Resetea resumen y deshabilita Continuar. | Inocua: si se pierde, el estado del iframe se sobreescribirá con el próximo selected/cleared. Último mensaje gana. |
| 3c | Selección | Error de oferta | postMessage | — | error | Iframe → Crowder | Partner no pudo construir/concretar la oferta. | status, error | Muestra error, bloquea Continuar. | Si el iframe crashea sin emitir error, Crowder no se entera. Mitigación: timeout de inactividad en el host + bloqueo de Continuar cuando la app es obligatoria. |
| 3.5 | Display | Autosize | postMessage | display | — | Iframe → Crowder | Montaje + cambios de altura (ResizeObserver sobre documentElement). | type, sizes.iframeHeight | iframe.style.height = max(h, slotMinHeight). | Si no llega nunca, el slot queda en min-height y el contenido puede cortarse u ocultar el CTA. Es problema de certificación del partner, no de runtime. |
| 3d | Continuar | Pedido de submit | postMessage | — | submit | Crowder → Iframe | El usuario presionó Continuar en el checkout. Crowder le pide al iframe que persista la oferta y devuelva la interaction. | status | Iframe llama a su backend, persiste la cotización en lifecycle valid y responde (3e) submitted (o 3c error). | El iframe puede tardar (red, validaciones backend). Crowder debe mostrar UI de “procesando” y aplicar timeout razonable — si no llega submitted ni error, no avanzar al GET (4). |
| 3e | Continuar | Submit aplicado | postMessage | — | submitted | Iframe → Crowder | El backend del partner persistió la oferta en lifecycle valid y emitió la interaction opaca. | status, interaction, currency, partnerItems (1–10) | Crowder guarda la interaction y dispara (4) GET para revalidar antes de la pasarela de pago. | Si el partner persiste pero no emite submitted (crash post-commit): queda una interaction huérfana en valid del lado partner. Crowder no la conoce → la limpia un job aparte. Si emite error en lugar de submitted, Continuar queda bloqueado, sin cobro. |
| 4 | Lifecycle | GET estado (request) | HTTP | GET {endpoint}/{interaction} | — | Crowder → Partner | Tras recibir submitted (3e), antes de la pasarela, o como revalidación / polling. | (sin body) | Partner devuelve (5). Idempotente: sin efectos colaterales. | Timeout o 5xx: 1 retry con backoff 500ms. Si persiste, Crowder no puede revalidar — bloquea el avance del checkout antes de cobrar. |
| 5a | Lifecycle | Estado valid | HTTP 200 | response | valid | Partner → Crowder | Cotización vigente, sin compromiso de stock, sin TTL. | status, interaction, currency, partnerItems | Crowder considera la cotización utilizable. | Si el partner responde valid pero al recibir (6a) sale out_of_stock: race aceptable, se aborta antes de cobrar. |
| 5b | Lifecycle | Estado reserved | HTTP 200 | response | reserved | Partner → Crowder | Reserva activa, TTL corriendo. | status, interaction, currency, partnerItems, expiresAt | Crowder sabe hasta cuándo es válida. | Si expiresAt venció pero el partner aún responde reserved: el siguiente (6b) saldrá 409. Crowder no debe confiar en el expiresAt propio sin re-verificar cerca del corte. |
| 5c | Lifecycle | Estado expired | HTTP 410 | response | expired | Partner → Crowder | El TTL venció sin pago (solo desde reserved). | status, interaction, error | Crowder bloquea el checkout de esa interacción. | Terminal. Para retomar el flujo el usuario necesita un nuevo selected (nueva interaction). |
| 5d | Lifecycle | Estado confirmed | HTTP 200 | response | confirmed | Partner → Crowder | Compra pagada, fulfillment en curso. | status, interaction, currency, partnerItems, purchase | Crowder considera la compra firme. | Si Crowder ve confirmed cuando esperaba reserved, asume webhook (6b) perdido y se realinea. No re-dispara purchasePaid. |
| 5e | Lifecycle | Estado refunded | HTTP 200 | response | refunded | Partner → Crowder | Devuelta total: todos los items vigentes fueron devueltos. Solo desde confirmed. | status, interaction, currency, partnerItems, purchase, refund | Estado terminal negativo (devolución total). Devoluciones parciales mantienen confirmed con partnerItems reducido. | Terminal. Reintentos de (6d) sobre refunded deben devolver el mismo ack. |
| 6a | Lifecycle | Webhook reservar | HTTP POST {endpoint}/{interaction}/events | event | purchaseReserved | Crowder → Partner | Crowder le pide al partner que reserve stock real. Transición valid → reserved. | status, partnerItems | Partner reserva y devuelve (7a) con expiresAt. | 5xx / timeout → backoff (1m → 5m → 30m → 2h → 12h → 24h). Crowder NO dispara (6b) hasta tener reserved firme — sin reserva no se cobra. out_of_stock / price_changed → checkout abortado antes del pago. Agotado el backoff → failed en backoffice. |
| 6b | Lifecycle | Webhook pago | HTTP POST .../events | event | purchasePaid | Crowder → Partner | Pago confirmado en Crowder. Transición reserved → confirmed. | status, currency, purchase, partnerItems | Partner arranca fulfillment, ack (7b). | Caso más sensible. Crowder ya cobró. 5xx → backoff completo; el partner debe ser idempotente para no duplicar fulfillment. 409 (típicamente expired) → desincronía, requiere reembolso al usuario o resolución bilateral. Agotado el backoff sin éxito → failed + intervención. |
| 6b’ | Lifecycle | Webhook expiración | HTTP POST .../events | event | purchaseExpired | Crowder → Partner | Venció el TTL de la reserva sin pago. Transición reserved → expired. | status | Partner libera el hold y ack (7h) con { "status": "expired" }. | 5xx → backoff. 409 (lifecycle ya no es reserved, ej: pasó a confirmed por una carrera con (6b)) → política bilateral; Crowder consulta (4) para realinearse. Terminal una vez aceptado. |
| 6d | Lifecycle | Webhook devolución | HTTP POST .../events | event | purchaseRefunded | Crowder → Partner | Devolución de uno o más items. Si abarca todos los items vigentes → confirmed → refunded. Si es subset → confirmed → confirmed con partnerItems reducido. | status, reason, items | Partner ejecuta refund interno, ack (7d). | 5xx → backoff. 409 sobre items refundable: false o terminal → política bilateral (salvo reason: chargeback | fraud, que bypasean el flag). Si el partner ya devolvió por su cuenta, responder 200 igual (idempotencia). |
| 7a | Ack | Ack reserva | HTTP 200 | ack | reserved | Partner → Crowder | Reservó stock real (TTL arranca acá). | status, expiresAt | Crowder persiste expiresAt. | expiresAt ausente o pasado → Crowder rechaza y entra a reintento. 200 sin haber reservado realmente: desastre operativo no detectable por protocolo — exige garantía del partner. |
| 7b | Ack | Ack pago | HTTP 200 | ack | confirmed | Partner → Crowder | Marcó la compra como confirmada. | status | — | 200 sin fulfillment real interno: silencioso para Crowder. Conciliación bilateral. No usar deferred acá si el fulfillment es crítico para el usuario. |
| 7d | Ack | Ack devolución | HTTP 200 | ack | confirmed (parcial) / refunded (total) | Partner → Crowder | Aplicó la devolución. confirmed si quedan items vigentes, refunded si era el último. | status | — | Igual que 7b: 200 confiable solo si el partner lo aplicó realmente. El partner es responsable de discriminar parcial vs. total contra su propio lifecycle. |
| 7h | Ack | Ack expiración | HTTP 200 | ack | expired | Partner → Crowder | Liberó el hold tras el purchaseExpired. | status | Crowder marca la interacción como terminal expirada. | Reintentos sobre expired deben re-responder el mismo ack (idempotencia). Si el partner ya había liberado el hold por su cuenta (TTL local), responder 200 igual. |
| 7e | Ack | Deferred | HTTP 202 | ack | deferred | Partner → Crowder | Recibido, procesamiento async. | status, notes? | Crowder no reintenta; el partner procesa offline. | No hay notificación de cierre. Si el proceso async falla, Crowder no se entera — conciliación periódica vía (4). Evitar deferred en (7a) porque arranca el TTL y necesita ser firme. |
| 7f | Ack | Transición inválida | HTTP 409 | ack error | error (invalid_transition) | Partner → Crowder | El lifecycle del partner no admite el evento (ej: purchasePaid sobre valid). | status: "error", error.code: "invalid_transition", error.message | Crowder no muta; resolución bilateral. | Crowder consulta (4) GET para realinearse al lifecycle real. Si persiste tras realineamiento → intervención manual. |
| 7g | Ack | Error genérico | HTTP 400 / 401 / 403 / 404 / 5xx | ack error | error | Partner → Crowder | Payload mal formado, auth, not_found o falla interna. | status: "error", error.code, error.message | 4xx sale del ciclo de reintentos; 5xx entra al backoff. | 401/403 indica que la api_key está vencida o se rotó sin coordinar — bloquea TODOS los webhooks hasta resolver. 404 recurrente sobre webhooks de eventos: la interaction se perdió del lado partner, conciliación bilateral. |
Devolución y expiración, en detalle
Sección titulada «Devolución y expiración, en detalle»Quién origina la devolución
Sección titulada «Quién origina la devolución»Todas las devoluciones post-pago viajan por el mismo webhook (purchaseRefunded); lo que cambia es el reason:
| Quién origina | reason |
|---|---|
| Usuario pide devolución | user_request |
| Partner necesita anular y devolver plata (ej: aerolínea cancela vuelo, hotel cierra) | cancelled_by_partner |
| Pasarela (chargeback / fraude) | chargeback | fraud (bypasean el flag refundable) |
| Otro caso operativo | other |
Devoluciones parciales
Sección titulada «Devoluciones parciales»purchaseRefunded puede cubrir uno o más items vía items (cada uno con uuid). Si abarca todos los items vigentes de la compra → terminal refunded. Si es subset → la interacción queda en confirmed con partnerItems reducido (los items devueltos salen del array).
Crowder solo pide devolver items declarados refundable: true en el GET de estado, salvo reason: chargeback | fraud que bypasean el flag.
Expiración del hold
Sección titulada «Expiración del hold»Cuando expiresAt vence sin pago, Crowder dispara (6b’) purchaseExpired para sincronizar la liberación. El partner libera el hold y ack (7h) con { "status": "expired" }. El siguiente GET (4) responderá (5c) expired con HTTP 410.
El partner no debe esperar al webhook para liberar el stock: el TTL local sigue siendo autoritativo. El webhook es una señal complementaria — si llega tarde o se reintenta, el partner re-ack expired (idempotencia).
Validación de transiciones
Sección titulada «Validación de transiciones»Qué acepta el partner según su lifecycle actual:
| Lifecycle actual | purchaseReserved | purchasePaid | purchaseExpired | purchaseRefunded |
|---|---|---|---|---|
valid | ✅ → reserved | ❌ 409 | ❌ 409 | ❌ 409 |
reserved | ✅ idempotente | ✅ → confirmed | ✅ → expired | ❌ 409 |
expired | ❌ 409 | ❌ 409 | ✅ idempotente | ❌ 409 |
confirmed | ❌ 409 | ❌ 409 | ❌ 409 | ✅ → confirmed (parcial) / refunded (total) |
refunded | ❌ 409 | ❌ 409 | ❌ 409 | ❌ 409 |
Códigos de error sugeridos por contexto
Sección titulada «Códigos de error sugeridos por contexto»Toda respuesta de error que emite el partner usa el envelope estándar { "status": "error", "error": { ... } }, salvo el caso especial status: expired (que tiene su propio discriminador de lifecycle). Códigos típicos según dónde se emita:
| Contexto | code típicos |
|---|---|
postMessage selected/submit con status: error | out_of_stock, price_changed, invalid_context, internal_error |
GET de estado con status: expired | expired, out_of_stock, price_changed, cancelled_by_partner, unknown |
| HTTP del partner (5xx, 401, 403, 404, 409, 400) | auth_invalid, not_found, invalid_transition, invalid_payload, unsupported_event, internal_error |
Reglas transversales
Sección titulada «Reglas transversales»| Aspecto | Regla |
|---|---|
| Discriminador raíz | status para el protocolo de negocio (postMessage e HTTP). type: "display" solo para autosize (no lleva status). HTTP no lleva type. |
targetOrigin (postMessage) | Nunca "*". Iframe → parent: parentOrigin. Parent → iframe: iframeOrigin. |
| Validación de origen | Receptor descarta MessageEvent cuyo event.origin no coincida. |
interaction (id) | Opaco, lo emite el partner. Nace cuando el iframe responde submitted (3e) tras un submit (3d) — no viaja en selected. En HTTP server-to-server viaja solo en la URL (/{interaction} y /{interaction}/events); los bodies de webhooks no lo repiten. |
| Auth HTTP | Authorization: Bearer {token}. El partner emite la api_key y la comparte por canal seguro. |
| Timeout / retry GET | 5s, 1 retry, backoff 500ms. |
| Reintentos webhook | Backoff 1m → 5m → 30m → 2h → 12h → 24h; luego failed en backoffice. |
| Idempotencia GET | Sin efectos colaterales: no decrementar stock, no extender expiresAt, no mutar lifecycle. |
| Idempotencia webhook | Partner deduplica por PK (interaction, status) y re-responde el ack original. |
| Viewport iframe → host | Vía display (sizes.iframeHeight). |
| Viewport host → iframe | Sin postMessage: leer window.innerWidth/innerHeight. |
| Encoding / Naming | UTF-8 JSON, camelCase. |
| Campos desconocidos | Se ignoran. Campos opcionales ausentes se omiten. |