Embed Widgets: Events
Full postMessage catalog, SSE pushed events, origin allowlist, and the webhook-vs-postMessage authoritative rule. Listen to widget lifecycle events from the parent page.
Listening from the parent page#
Every widget emits typed postMessage events you can listen to from the parent page. The SDK exposes a typed subscription helper via AforoEmbed.on(type, handler), or you can use the raw window.addEventListener('message') pattern.
⚠ Webhook is authoritative#
Why? An attacker who controls a browser tab can forge postMessage events to your page. If you provision access based on aforo.subscription.created alone, an attacker can spoof the event and trick your app into granting access without payment.
Use postMessage events to update local UI state (e.g. show a success modal, navigate to a thank-you page, refresh a counter). Use signed webhooks to update persistent state (e.g. provision API keys, send welcome emails, write to your database). The webhook is your source of truth.
Outbound event catalog#
Every event follows the envelope shape { type: 'aforo.<widget>.<event>', payload: {…}, version: '1', emittedAt: '…', source: '<widget>' }. Payloads never contain PII (no email, no JWT, no customer ID — only the IDs you passed into the widget).
Lifecycle (every widget)
Widget-specific events
Inbound events the SDK accepts#
Some widgets accept events from the parent page. The SDK enforces an inbound allowlist — events outside this set are silently ignored.
aforo:checkout-completed— tell a SubscribeButton in event-only mode that the parent finished checkout.aforo:checkout-cancelled— tell a SubscribeButton the parent cancelled the checkout.aforo:invoice-paid— tell an InvoiceList to refresh after the parent processes a payment outside the widget.aforo:refresh-requested— generic refresh signal for any widget that supports manual reload.aforo:payment-challenge-completed— tell a CheckoutFlow that a 3DS challenge completed in a popup window.
SSE pushed events#
Widgets that need real-time updates open a Server-Sent-Events stream when they mount. Aforo pushes lifecycle events scoped to the (tenant, customer) pair as they happen — no polling required.
aforo.invoice.created— a new invoice was finalizedaforo.invoice.paid— an invoice transitioned to PAIDaforo.subscription.status_changed— a subscription transitioned to a new lifecycle stateaforo.payment.failed— a payment attempt failed (during checkout or dunning)aforo.usage.threshold_breached— usage crossed a configured threshold
The connection auto-reconnects with exponential backoff (capped at 60s) if dropped. Multiple widgets on the same page share a single connection per tenant. When the last widget unmounts, the connection stays open for 30 seconds in case a new widget mounts — avoiding an unnecessary disconnect / reconnect cycle.
Origin allowlist#
The SDK validates the origin of every inbound postMessage event against the embed key's registered allowed_domains. Outbound events use window.parent.postMessage(payload, parentOrigin) where parentOrigin is the resolved parent origin — never '*'.
event.origin === 'https://embed.aforo.ai' before processing. The browser will not enforce this for you on the receiving side.See the for the full inbound allowlist + 100ms dedup window + parent-origin validation rules.