Events
This is the contract reference for the events the embedded widget emits to your
page via ExpeditionInsure.on(). It covers the full catalog, each payload shape
(premiumCents is the money field — integer minor units), which tier each event
fires in, and ordering and idempotency. For how to
mount the widget and subscribe, see the embed integration guide;
for the HTTP endpoints the iframe calls, see the embed HTTP API,
and for the policy.issued webhook see webhooks.
1. Loading the SDK
Section titled “1. Loading the SDK”ExpeditionInsure.on() is available once the loader script is on the page. Add
the loader and the global window.ExpeditionInsure appears:
<script src="https://expedition.insure/widget.js"></script>For the full mount-and-subscribe walkthrough see /get-started/quickstart/;
for the precise mount() / on() / embedOrigin contract see /reference/loader/.
2. The event channel
Section titled “2. The event channel”You subscribe with ExpeditionInsure.on(eventName, handler). It returns an
unsubscribe function. Subscribing to an unknown event name throws. Handlers
run isolated — a throw inside your handler is caught and logged, never breaking
the bridge or stopping other handlers. Multiple handlers per event are allowed
and run in subscribe order.
const off = ExpeditionInsure.on("quote.selected", (e) => { console.log(e.planId, e.premiumCents, e.currency);});// off(); // unsubscribe laterUnder the hood the widget bridges a namespaced ei:* postMessage channel,
origin-locked in both directions — inbound only from our embed origin and the
exact mounted iframe, outbound only to the iframe with our origin as
targetOrigin, never "*". The ei: prefix is stripped before your handler
sees the event.
End-to-end bootstrap
Section titled “End-to-end bootstrap”Subscribe to the three base-flow events before mounting. This captures the
quoteId from quote.ready, the selection from quote.selected, and handles
failure on quote.error:
let currentQuoteId = null;
ExpeditionInsure.on("quote.ready", (e) => { // Capture quoteId here — it is NOT on quote.selected. See §5. currentQuoteId = e.quoteId ?? currentQuoteId; console.log("quote ready", currentQuoteId, "plans:", e.plansCount);});
ExpeditionInsure.on("quote.selected", (e) => { // premiumCents is integer minor units (cents) — divide by 100 for display. console.log("selected", e.planId, e.premiumCents / 100, e.currency ?? "USD"); // Associate the selection with the quoteId you captured above.});
ExpeditionInsure.on("quote.error", (e) => { // code is an optional free-form string; render message defensively. console.warn("quote failed", e.code, e.message);});3. Event catalog
Section titled “3. Event catalog”Five events are valid. Subscribing to any other name throws. The Tier column refers to the three-tier integration model — see /get-started/overview/ for what each tier means.
| Event | Fires when | Tier |
|---|---|---|
quote.ready | The quote form first renders / instant options are ready. | All tiers |
quote.selected | A traveler clicks a plan card. | All tiers |
quote.error | The quote or options request failed. | All tiers |
payment.succeeded | The insurance PaymentIntent confirmed inside the iframe. | Tier-3 only |
payment.failed | The insurance payment failed inside the iframe. | Tier-3 only |
quote.ready, quote.selected, and quote.error fire in the base quote flow,
which runs in every embed tier. The payment.* events fire only in the
Tier-3 in-iframe embedded checkout (/embed/checkout → /embed/confirmed) — if
you don’t use embedded checkout, you never see them.
4. Payload shapes
Section titled “4. Payload shapes”The ei: prefix is stripped, so your handler receives the payload below for
each event name.
quote.ready
Section titled “quote.ready”{ quoteId?: string, plansCount?: number }| Field | Type | Notes |
|---|---|---|
quoteId | string | The created quote’s id. May be absent early. |
plansCount | number | Number of plans/options surfaced. |
quote.selected
Section titled “quote.selected”{ planId: string, premiumCents: number, currency?: string }| Field | Type | Notes |
|---|---|---|
planId | string | The selected plan’s id. |
premiumCents | number | Integer minor units (cents). Divide by 100 for display. |
currency | string | Optional ISO currency code (e.g. "USD"), forwarded untouched. |
quote.selectedhas noquoteId— capture it fromquote.ready. Thequote.readypayload is{ quoteId?, plansCount? }andquote.selectedis{ planId, premiumCents, currency? }— it does not carry thequoteId. To link a selection back to a quote (and to your booking), capturequoteIdonquote.readyand associate it yourself.payment.succeededalso includesquoteId. ThequoteIdmay be absent on a very earlyquote.readybut is present once options are ready, so keep the last non-empty value you see.
quote.error
Section titled “quote.error”{ code?: string, message?: string }| Field | Type | Notes |
|---|---|---|
code | string | Optional machine-readable code. |
message | string | Optional human-readable message. |
payment.succeeded (Tier-3 only)
Section titled “payment.succeeded (Tier-3 only)”{ quoteId?: string, planId?: string, premiumCents?: number, currency?: string }| Field | Type | Notes |
|---|---|---|
quoteId | string | The quote whose insurance was paid. |
planId | string | The purchased plan’s id. |
premiumCents | number | Integer minor units (cents). May be absent. |
currency | string | Optional ISO currency code (e.g. "USD"). |
All payment.succeeded fields are optional — coerce and guard before use.
payment.succeededis a UX signal, not the source of truth. It reports only the insurance charge confirmed inside our iframe, and it can be missed (the user closes the tab, the bridge drops). The authoritative post-sale signal is thepolicy.issuedwebhook, fired server-side off the Stripepayment_intent.succeededevent — that webhook is also what writes the commission. Usepayment.succeededto sequence or gate your own checkout UX; reconcile sales and attribution frompolicy.issued(see /operate/reconciliation/). Your trip charge is a separate rail — never treat the insurance payment as your trip payment.
payment.failed (Tier-3 only)
Section titled “payment.failed (Tier-3 only)”{ code?: string, message?: string }| Field | Type | Notes |
|---|---|---|
code | string | Optional free-form string. |
message | string | Optional human-readable message. |
payment.failed has the same shape as quote.error — both fields are
optional. There is no documented closed set of failure codes today: treat
code as an optional free-form string, do not switch on specific values, and
render message defensively (it may be absent). A stable, enumerated code set is
[needs-product-work].
5. Premium fields — premiumCents
Section titled “5. Premium fields — premiumCents”Both quote.selected and payment.succeeded carry the premium as
premiumCents — an integer in minor units (cents). Read it and divide by 100
yourself for display.
currency is an optional string forwarded untouched (e.g. "USD"). The loader
re-emits premiumCents exactly as received — it does not recompute it.
ExpeditionInsure.on("quote.selected", (e) => { const dollars = e.premiumCents / 100; render(dollars.toLocaleString(undefined, { style: "currency", currency: e.currency ?? "USD", }));});Reading
premiumCents(integer minor units) as whole dollars makes the value look 100× off. If a premium looks wrong, confirm you’re readingpremiumCentsand dividing by 100 for display.
6. Ordering & idempotency guarantees
Section titled “6. Ordering & idempotency guarantees”The widget events are an in-page UX channel — treat them as best-effort signals, not a transactional ledger.
quote.ready ──► quote.selected ──► [Tier-3] payment.succeeded │ │ └──► quote.error └──► payment.failed- Ordering within a session is logical, not guaranteed delivered.
quote.readyprecedesquote.selected, which precedes a Tier-3payment.succeeded. But any event can be missed (tab closed, bridge dropped, outer sandbox strips the channel), so do not require an earlier event to have fired before handling a later one. - No delivery or replay guarantee. Events are not retried, queued, or re-emitted. If your handler throws, the event is logged and lost — it is not redelivered.
quote.selectedcan fire repeatedly — once per plan-card click. The payload reflects the latest selection; the last one wins. Make your handler idempotent (e.g. overwrite, don’t append).payment.succeededmust not be treated as exactly-once. It can fire, fire late, or never arrive. For exactly-once, idempotent post-sale processing, consume thepolicy.issuedwebhook instead, which carries a stableeventId, is delivered at-least-once, and is the source of truth (see /operate/reconciliation/).
7. No opt-out / deselect event
Section titled “7. No opt-out / deselect event”There is no deselect, decline, or “insurance dismissed” event. [needs-product-work]
quote.selectedfires on every plan-card click — a later click supersedes an earlier one — but there is no signal that a traveler actively opted out of insurance. If you add the premium to a trip total offquote.selected, treat the latestquote.selectedas the current selection and provide your own “remove insurance” affordance in your UI; the widget will not tell you the traveler changed their mind.
8. Tier-1/2 checkout sequencing
Section titled “8. Tier-1/2 checkout sequencing”If you run your own checkout (Tier-1 or Tier-2), sequence it off quote.selected
— see /guides/checkout-sequencing/ for the
quote.selected → your-checkout pattern. Reiterate the rule above:
payment.succeeded is UX-only and may never arrive; reconcile from the
policy.issued webhook, never from the browser event.
Next steps
Section titled “Next steps”- /embedded-insurance/integration-guide/ — mount the widget and subscribe to these events.
- /reference/embed-api/ — the embed HTTP API the iframe calls.
- /reference/webhooks/ — the authoritative
policy.issuedwebhook contract. - /operate/reconciliation/ — go-live operations: the authoritative
policy.issuedwebhook, attribution, and commissions.