Ir al contenido

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

#FaseMensajeTransportetype / verbostatusEmisor → ReceptorTriggerCampos que viajanEfecto / ReacciónSi falla
1HandshakeIframe listopostMessagereadyIframe → CrowderEl iframe cargó y registró su listener.statusCrowder 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.
2HandshakeContexto al iframepostMessagecontextCrowder → IframeTras ready, o cuando cambian los items durante la sesión.status, locale, currency, eventInfo, venue, items (tickets), userIframe 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.
3aSelecciónSelección (preview)postMessageselectedIframe → CrowderEl 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.
3bSelecciónSelección vacíapostMessageclearedIframe → CrowderUsuario invalidó o vació la selección.statusResetea resumen y deshabilita Continuar.Inocua: si se pierde, el estado del iframe se sobreescribirá con el próximo selected/cleared. Último mensaje gana.
3cSelecciónError de ofertapostMessageerrorIframe → CrowderPartner no pudo construir/concretar la oferta.status, errorMuestra 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.5DisplayAutosizepostMessagedisplayIframe → CrowderMontaje + cambios de altura (ResizeObserver sobre documentElement).type, sizes.iframeHeightiframe.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.
3dContinuarPedido de submitpostMessagesubmitCrowder → IframeEl usuario presionó Continuar en el checkout. Crowder le pide al iframe que persista la oferta y devuelva la interaction.statusIframe 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).
3eContinuarSubmit aplicadopostMessagesubmittedIframe → CrowderEl 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.
4LifecycleGET estado (request)HTTPGET {endpoint}/{interaction}Crowder → PartnerTras 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.
5aLifecycleEstado validHTTP 200responsevalidPartner → CrowderCotización vigente, sin compromiso de stock, sin TTL.status, interaction, currency, partnerItemsCrowder 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.
5bLifecycleEstado reservedHTTP 200responsereservedPartner → CrowderReserva activa, TTL corriendo.status, interaction, currency, partnerItems, expiresAtCrowder 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.
5cLifecycleEstado expiredHTTP 410responseexpiredPartner → CrowderEl TTL venció sin pago (solo desde reserved).status, interaction, errorCrowder bloquea el checkout de esa interacción.Terminal. Para retomar el flujo el usuario necesita un nuevo selected (nueva interaction).
5dLifecycleEstado confirmedHTTP 200responseconfirmedPartner → CrowderCompra pagada, fulfillment en curso.status, interaction, currency, partnerItems, purchaseCrowder considera la compra firme.Si Crowder ve confirmed cuando esperaba reserved, asume webhook (6b) perdido y se realinea. No re-dispara purchasePaid.
5eLifecycleEstado refundedHTTP 200responserefundedPartner → CrowderDevuelta total: todos los items vigentes fueron devueltos. Solo desde confirmed.status, interaction, currency, partnerItems, purchase, refundEstado terminal negativo (devolución total). Devoluciones parciales mantienen confirmed con partnerItems reducido.Terminal. Reintentos de (6d) sobre refunded deben devolver el mismo ack.
6aLifecycleWebhook reservarHTTP POST {endpoint}/{interaction}/eventseventpurchaseReservedCrowder → PartnerCrowder le pide al partner que reserve stock real. Transición valid → reserved.status, partnerItemsPartner 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.
6bLifecycleWebhook pagoHTTP POST .../eventseventpurchasePaidCrowder → PartnerPago confirmado en Crowder. Transición reserved → confirmed.status, currency, purchase, partnerItemsPartner 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’LifecycleWebhook expiraciónHTTP POST .../eventseventpurchaseExpiredCrowder → PartnerVenció el TTL de la reserva sin pago. Transición reserved → expired.statusPartner 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.
6dLifecycleWebhook devoluciónHTTP POST .../eventseventpurchaseRefundedCrowder → PartnerDevolució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, itemsPartner 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).
7aAckAck reservaHTTP 200ackreservedPartner → CrowderReservó stock real (TTL arranca acá).status, expiresAtCrowder 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.
7bAckAck pagoHTTP 200ackconfirmedPartner → CrowderMarcó la compra como confirmada.status200 sin fulfillment real interno: silencioso para Crowder. Conciliación bilateral. No usar deferred acá si el fulfillment es crítico para el usuario.
7dAckAck devoluciónHTTP 200ackconfirmed (parcial) / refunded (total)Partner → CrowderAplicó la devolución. confirmed si quedan items vigentes, refunded si era el último.statusIgual que 7b: 200 confiable solo si el partner lo aplicó realmente. El partner es responsable de discriminar parcial vs. total contra su propio lifecycle.
7hAckAck expiraciónHTTP 200ackexpiredPartner → CrowderLiberó el hold tras el purchaseExpired.statusCrowder 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.
7eAckDeferredHTTP 202ackdeferredPartner → CrowderRecibido, 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.
7fAckTransición inválidaHTTP 409ack errorerror (invalid_transition)Partner → CrowderEl lifecycle del partner no admite el evento (ej: purchasePaid sobre valid).status: "error", error.code: "invalid_transition", error.messageCrowder no muta; resolución bilateral.Crowder consulta (4) GET para realinearse al lifecycle real. Si persiste tras realineamiento → intervención manual.
7gAckError genéricoHTTP 400 / 401 / 403 / 404 / 5xxack errorerrorPartner → CrowderPayload mal formado, auth, not_found o falla interna.status: "error", error.code, error.message4xx 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.

Todas las devoluciones post-pago viajan por el mismo webhook (purchaseRefunded); lo que cambia es el reason:

Quién originareason
Usuario pide devoluciónuser_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 operativoother

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.

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

Qué acepta el partner según su lifecycle actual:

Lifecycle actualpurchaseReservedpurchasePaidpurchaseExpiredpurchaseRefunded
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

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:

Contextocode típicos
postMessage selected/submit con status: errorout_of_stock, price_changed, invalid_context, internal_error
GET de estado con status: expiredexpired, 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
AspectoRegla
Discriminador raízstatus 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 origenReceptor 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 HTTPAuthorization: Bearer {token}. El partner emite la api_key y la comparte por canal seguro.
Timeout / retry GET5s, 1 retry, backoff 500ms.
Reintentos webhookBackoff 1m → 5m → 30m → 2h → 12h → 24h; luego failed en backoffice.
Idempotencia GETSin efectos colaterales: no decrementar stock, no extender expiresAt, no mutar lifecycle.
Idempotencia webhookPartner deduplica por PK (interaction, status) y re-responde el ack original.
Viewport iframe → hostVía display (sizes.iframeHeight).
Viewport host → iframeSin postMessage: leer window.innerWidth/innerHeight.
Encoding / NamingUTF-8 JSON, camelCase.
Campos desconocidosSe ignoran. Campos opcionales ausentes se omiten.