Embedded insurance

Add travel insurance to your checkout

One <script> tag puts a fully-hosted quote-and-buy flow inside your site. Your customer transacts against Expedition Insure end to end — you never touch card data or premium funds, and you get a signed webhook when a policy is issued. Most operators are live in an afternoon.

Overview

Expedition Insure is a travel-insurance marketplace for polar and safari expeditions. Embedded insurance means our quote-and-purchase flow runs on your site, attributed to you, while the customer transacts against our systems. We are the merchant of record.

There are three ways to integrate, from least to most effort:

  • Tier 1 — link / snippet. A prefilled link or copy-paste HTML to our hosted /quote flow, carrying your attribution.
  • Tier 2 — widget recommended An <iframe> injected by our widget.js loader, with a namespaced postMessage bridge for events and auto-resize. This is what the demo around this page uses.
  • Tier 3 — embed API. Call our JSON API directly from your own UI and render plans yourself.

This guide focuses on Tier 2, then covers the API and webhooks.

Quick start

Drop a mount target and the loader on the page, then call ExpeditionInsure.mount() with the trip details you already know. The widget renders a quote form, shows live plan prices, and handles the buy flow.

HTML
<div id="ei-quote"></div>

<script src="https://expedition.insure/widget.js"></script>
<script>
  ExpeditionInsure.mount("#ei-quote", {
    pk: "pk_op_your_publishable_key",
    destination: "antarctica",
    startDate: "2026-12-01",
    endDate:   "2026-12-14",
    ages:      [45, 47],
    residence: "US",
    tripCost:  18990,   // whole units of `currency` (default USD)
    travelers: 2,
  });

  // React to what the customer does (all optional):
  ExpeditionInsure.on("quote.selected", (e) => {
    console.log("plan selected", e.planId, e.premiumCents);
  });
</script>
That's the whole integration. The widget auto-resizes its iframe, so you never set a fixed height. Everything below is optional polish.

You get a publishable key (pk_op_…) and your allow-listed origins at onboarding. The key is not a secret — see Security & data.

Mount options

mount(selector, config) — only pk is required. Everything you pass pre-fills the form; the customer can still change it.

FieldTypeNotes
pk requiredstringYour publishable key, pk_op_…
destinationstringDestination slug, e.g. antarctica, arctic.
startDate / endDatestringISO YYYY-MM-DD.
agesnumber[]Traveller ages at departure. If supplied, the form collapses to just email.
residencestringISO country code (e.g. US, CA) or name. The customer can change it.
tripCostnumberTotal trip cost across all travellers (whole units).
travelersnumberNumber of travellers.
refstringYour own reference id — echoed back for reconciliation.
logoUrlstringOptional logo shown above the form.
accentColorstringHex color for buttons / highlights, e.g. #00B4A0.

Events

Subscribe with ExpeditionInsure.on(eventName, callback). Use these to update your order summary, gate your own checkout, or log analytics. Money is in whole-dollar minor units (premiumCents) — the canonical field.

EventPayload
quote.ready{ quoteId, plansCount } — first plan cards rendered.
quote.selected{ planId, premiumCents, premium, currency } — customer picked a plan. premium (whole dollars) is deprecated; use premiumCents.
quote.error{ code, message }
payment.succeeded{ quoteId, planId, premiumCents, premium, currency } — policy paid in-frame.
payment.failed{ code, message }
JavaScript
ExpeditionInsure.on("quote.selected", (e) => {
  // e.premiumCents is canonical (integer minor units)
  orderSummary.setProtection(e.premiumCents / 100);
});

ExpeditionInsure.on("payment.succeeded", (e) => {
  // The policy is paid. A signed `policy.issued` webhook
  // follows server-to-server (see Webhooks).
  analytics.track("insurance_purchased", { quoteId: e.quoteId });
});

Branding

Pass accentColor and logoUrl to match your site. The widget inherits your page width and resizes its own height, so it sits naturally inside an add-ons step or a sidebar.

JavaScript
ExpeditionInsure.mount("#ei-quote", {
  pk: "pk_op_…",
  destination: "antarctica",
  tripCost: 18990, travelers: 2,
  accentColor: "#00B4A0",
  logoUrl: "https://your-site.com/logo.svg",
});

Embed API tier 3

Prefer to render plans in your own UI? Call the JSON API directly. Authenticate with the X-EI-Publishable-Key header; requests must come from an allow-listed Origin. The API base URL is provided at onboarding. Money on the wire is in cents.

1 · Create a quote

POST /api/embed/quote
POST /api/embed/quote
X-EI-Publishable-Key: pk_op_…
Content-Type: application/json

{
  "destination": "antarctica",
  "startDate": "2026-12-01",
  "endDate":   "2026-12-14",
  "tripCost":  18990,
  "travelers": 2,
  "travelerAges": [45, 47],
  "residence": "US",
  "email": "traveller@example.com"
}

→ 200 { "quoteId": "...", "quoteNumber": "EXP-...", "instantQuoteEligible": true }

2 · Poll for options

Estimates return immediately; firm carrier prices land within a few seconds. Poll until each option carries actualPremiumCents.

GET /api/embed/options
GET /api/embed/options?quoteId=...&pk=pk_op_…

→ 200 {
  "options": [
    {
      "planId": "...", "planName": "...", "carrierName": "...",
      "evacCoverage": 500000, "medicalExpense": 500000,
      "preAuthAmountCents": 41322,     // estimate
      "actualPremiumCents": 41322      // firm price once scraped/priced
    }
  ]
}
// While still pricing: { "options": [], "pending": true } — keep polling.
Units: the REST API returns premiums in cents; the widget's postMessage events expose the same value as premiumCents. Divide by 100 to display dollars.

Webhooks

When a policy is issued we POST a policy.issued event to your configured webhookUrl. Delivery is at-least-once with retries — dedupe on eventId.

POST → your webhookUrl
POST https://your-site.com/webhooks/expedition-insure
X-EI-Event: policy.issued
X-EI-Signature: t=1765238400,v1=<hmac-sha256 hex>
Content-Type: application/json

{
  "event": "policy.issued",
  "eventId": "1f8c…",          // stable across retries — dedupe on this
  "policyId": "...",
  "operatorId": "...",
  "premiumCents": 41322,
  "commissionCents": 4132,
  "premium": 413,              // deprecated whole dollars
  "commission": 41,
  "issuedAt": 1765238400000,   // epoch ms
  "sentAt": 1765238400123      // epoch ms (this attempt)
}

Verifying the signature

t is a unix-seconds timestamp (Stripe convention). Recompute the HMAC over "${t}.${rawBody}" with your shared webhook secret and compare in constant time. Reject a t outside a ±5-minute window to block replays.

Node.js
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const t = Number(parts.t);          // unix SECONDS — do not ×1000
  if (Math.abs(Date.now() / 1000 - t) > 300) return false; // ±5 min
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(parts.v1),
  );
}
No webhook infra? We can instead email the notification to addresses you configure at onboarding. The Integration monitor on the demo shows live policy.issued deliveries (with a "simulate" button) so you can see this end to end.

Security & data

  • The publishable key is not a secret. It ships in your page source. The real boundary is a per-operator origin allow-list: the API only answers requests from your registered origins, and the widget only renders framed on them (enforced by Content-Security-Policy: frame-ancestors, fail-closed).
  • You never handle card data. Payment is collected by Stripe inside the frame; we are merchant of record. Your PCI scope is SAQ-A-equivalent.
  • Minimal PII. You receive customer PII only through the channel you configure — the signed policy.issued webhook (or email) — never the full quote record.
  • Signed, idempotent webhooks. HMAC-SHA256, ±5-min replay window, stable eventId.

Going live

  • Send us the origin(s) your widget will load on — we add them to your allow-list.
  • Receive your pk_op_… publishable key.
  • (Optional) Give us a webhookUrl + we share a signing secret, or email recipients.
  • (Optional) Tell us which plans to feature for a curated, instant-price experience.
  • Paste the snippet. Done.
Try it now: toggle Travel protection in the demo header, run the checkout, and watch the Integration monitor log every event, API call, and webhook in real time.
Expedition Insure — embedded insurance developer docs. Rendered inside the OperatorCo demo. Questions? Reach your integration contact.