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
/quoteflow, carrying your attribution. -
Tier 2 — widget recommended
An
<iframe>injected by ourwidget.jsloader, with a namespacedpostMessagebridge 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.
<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>
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.
| Field | Type | Notes |
|---|---|---|
pk required | string | Your publishable key, pk_op_… |
destination | string | Destination slug, e.g. antarctica, arctic. |
startDate / endDate | string | ISO YYYY-MM-DD. |
ages | number[] | Traveller ages at departure. If supplied, the form collapses to just email. |
residence | string | ISO country code (e.g. US, CA) or name. The customer can change it. |
tripCost | number | Total trip cost across all travellers (whole units). |
travelers | number | Number of travellers. |
ref | string | Your own reference id — echoed back for reconciliation. |
logoUrl | string | Optional logo shown above the form. |
accentColor | string | Hex 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.
| Event | Payload |
|---|---|
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 } |
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.
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
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?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.
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 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.
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),
);
}
policy.issued deliveries (with a
"simulate" button) so you can see this end to end.
Recommended plans
By default the widget surfaces every plan a trip is eligible for. Operators can instead be configured with a curated whitelist of plans, in priority order — the widget then shows only those, and prices them live and instantly via the carrier's pricing API (firm prices in ~300 ms instead of a multi-carrier scrape).
This is configured per operator on our side from a recommended-plans table; nothing changes in your snippet. Tell us which carriers/plans you want to feature and we'll wire it to your publishable key.
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.issuedwebhook (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.