SLA, uptime, and status
This page covers what to expect from the Expedition Insure embed for availability — what we publish today, and, more importantly, how to build your trip checkout so that an embed outage degrades gracefully instead of blocking your sale. It is for the engineers who own the page the widget runs on.
Read the security model for why the embed fails closed at the framing layer, and data and privacy for what crosses the boundary. For the event payloads referenced below, see the integration guide; for how a sale reaches your ledger after payment, see reconciliation and attribution.
The short version: your trip checkout must not hard-depend on the embed. Insurance is an add-on. If the widget is slow, throws, or never loads, the customer should still be able to book the trip. Everything below is about making that true in your code.
1. What we publish
Section titled “1. What we publish”We run a public status page and a machine-readable feed for the embed:
- Status page (
developers.expedition.insure/status) — live per-component health (loader, quote API, iframe, webhook delivery, MCP) with 90-day uptime and any active incidents. - JSON feed + incident RSS — linked from the status page, for wiring embed health into your own dashboards and alerting.
A few honest caveats:
- The status page reflects our own synthetic monitoring — we probe each surface every few minutes. It’s a transparency signal, not an independent third-party uptime guarantee.
- There is no contractual SLA on the embed today: no committed availability or response-time number you can hold us to. Treat the published uptime as informational.
- The authoritative signal that a sale completed is not the embed at all — it is the
policy.issuedwebhook delivered to your server (see reconciliation and attribution). Treat the in-page widget as a best-effort UX layer, and your server-side webhook as the system of record.
A status feed tells you what we observe; your integration should still detect degradation itself. The rest of this page is how.
2. The one rule: insurance is optional at checkout
Section titled “2. The one rule: insurance is optional at checkout”Design your booking flow so the insurance step can fail without taking the trip purchase with it.
customer books trip │ ├─ insurance embed loads ──► customer adds a policy ──► you collect the quote.selected event │ and proceed to the embed's checkout/handoff │ └─ insurance embed slow / errored / absent ──► customer skips insurance ──► trip purchase still completesConcretely:
- Keep the “continue without insurance” path always reachable. Never gate your trip’s “pay” button on a successful embed mount or on receiving a
quote.ready. - Render the widget into a dedicated container that can collapse to nothing. If it never paints, your layout should not break.
- Do not block your own checkout on any embed
postMessageevent. The events (quote.ready,quote.selected,payment.succeeded,payment.failed) are advisory UX signals, not gates.
3. What “down” looks like, and how each fails
Section titled “3. What “down” looks like, and how each fails”The embed has several independent failure points. Each one degrades on its own — design for all of them.
| Failure | What the customer sees | What your page receives | Graceful response |
|---|---|---|---|
widget.js fails to load | Empty container | ExpeditionInsure global never defined | Detect a missing global, hide the container, keep checkout open |
mount() throws | Nothing renders | Synchronous exception from mount() | Wrap mount() in try/catch; on throw, hide the container |
| iframe loads but quote API is slow/down | Spinner, then an error state inside the iframe | A quote.error event ({ code, message }), or simply no quote.ready | Don’t wait on quote.ready; show your skip path |
| Origin not allowlisted | Blank frame (framing fails closed) | No events | Hide the container; contact us to allowlist your origin |
Rate limit hit (429) | Quote can’t generate | quote.error inside the iframe | Surface the iframe’s own error; keep checkout open |
Framing fails closed by design. If your origin isn’t on the allowlist, the browser refuses to render the frame rather than showing a degraded one. See the security model for the
frame-ancestorsmechanism. The practical takeaway here: a blank frame is a possible normal state, so your layout must tolerate it.
The embed HTTP API returns standard status codes — 401/403 for auth and origin problems, 429 with a Retry-After header when a publishable key exceeds 60 requests per minute, and 200 for success. Quote generation runs inside the iframe, so your page sees these as a quote.error event rather than a raw HTTP status. You don’t have to parse them yourself; you only have to not block on success.
4. Detect a missing or broken embed
Section titled “4. Detect a missing or broken embed”Guard every entry point. The loader exposes exactly three members on the global — mount, on, and embedOrigin — and mount() throws on a missing target or a missing/invalid pk, so both the global and the call are worth guarding.
<div id="ei-quote"></div>
<script src="https://expedition.insure/widget.js"></script><script> function mountInsurance() { // 1. Did the loader script even load? if (typeof window.ExpeditionInsure === "undefined") { hideInsurance(); // your function: collapse the container return; }
try { // 2. mount() throws on a bad target or a missing pk window.ExpeditionInsure.mount("#ei-quote", { pk: "pk_op_...", // ships in your HTML — public by design destination: "antarctica", tripCost: 18000, // whole dollars }); } catch (err) { console.warn("[insurance] embed failed to mount:", err); hideInsurance(); return; }
// 3. The quote API may still be slow or down. Don't wait on it — // just log the in-iframe error if it surfaces. window.ExpeditionInsure.on("quote.error", function (payload) { console.warn("[insurance] quote error:", payload && payload.code, payload && payload.message); // Your trip checkout stays open regardless. }); }
mountInsurance();</script>Notes:
on()itself throws if you subscribe to an unknown event name. Stick to the documented names (quote.ready,quote.selected,quote.error,payment.succeeded,payment.failed).- A faulty handler you register can’t break the bridge — the loader wraps each handler in
try/catchand logslistener errorif it throws. Other listeners still fire. - There is no
unmount()orupdate(). To recover a broken instance, clear the container and callmount()again.
5. Don’t time out your own checkout on the embed
Section titled “5. Don’t time out your own checkout on the embed”If you wait for quote.ready (or any embed signal) before letting the customer continue, a slow quote API becomes a blocked sale. Instead, treat the embed as fire-and-forget and let the customer drive:
- Never put your “pay for the trip” button behind a
quote.readyawait. - If you want a loading affordance, give it your own short timeout and then reveal the skip path — don’t wait indefinitely for an event that may never arrive.
- The widget reports its own height via an internal resize message, so a slow or empty iframe won’t leave a fixed gap once it settles; still, your container should look intentional when collapsed.
6. Payment timing and the source of truth
Section titled “6. Payment timing and the source of truth”When the embedded checkout completes, the iframe emits payment.succeeded so you can update your UI immediately. Do not treat it as proof of sale.
payment.succeededis a UX signal only. It can be missed — a closed tab, a dropped frame, or a strict outer sandbox all swallow it. The authoritative post-sale signal is thepolicy.issuedwebhook delivered to your server, which is what records the commission and is safe to reconcile against. Build fulfillment and accounting on the webhook, never on the in-page event. Full payload, signature, and retry behavior are in reconciliation and attribution.
This separation is exactly what makes an embed outage survivable: even if every in-page event is lost, the sale is still recorded server-side and delivered to you out of band.
When you read amounts off any event, use premiumCents (integer minor units) and divide by 100 for display.
7. Checklist
Section titled “7. Checklist”- Trip checkout completes with the embed entirely absent.
-
widget.jsmissing → container collapses, no layout break. -
mount()wrapped intry/catch; a throw hides the container. - No “pay” button gated on
quote.readyor any embed event. - A blank frame (origin not allowlisted) doesn’t break your layout.
-
quote.erroris logged, not fatal. - Fulfillment keys off the
policy.issuedwebhook, notpayment.succeeded. - Amounts read from
premiumCents(integer minor units), divided by 100 for display.
If you need your origin allowlisted, or you’re seeing repeated 429s and want to discuss limits, reach a human at help@expedition.insure.