Vue
This page shows how to mount the Expedition Insure quote embed inside a Vue component, wire its events to your app, and clean up correctly. It’s for front-end engineers integrating the embed into a Vue 3 application.
The embed is a cross-origin <iframe> driven by a small loader script (widget.js) that exposes a window.ExpeditionInsure global. The contracts here — mount(), on(), and the ei:* event payloads — are the same across every framework. For the full reference, see the integration guide and the events reference. If you use React, see the React recipe instead.
1. Load the loader script
Section titled “1. Load the loader script”Add widget.js once, from the Expedition Insure origin that will serve your embed. The loader resolves its trust origin from its own src, so the same script works in every environment with no rebuild.
<script src="https://expedition.insure/widget.js" defer></script>After it loads, window.ExpeditionInsure exposes exactly three members:
| Member | Signature | Purpose |
|---|---|---|
mount | mount(target, config) => HTMLIFrameElement | Create and append the quote iframe. |
on | on(eventName, handler) => () => void | Subscribe to a lifecycle event; returns an unsubscribe function. |
embedOrigin | string (read-only) | The embed origin this loader trusts (debugging only). |
There is no unmount() and no update() today. To change a mounted iframe’s config, you re-mount — see section 4.
2. Mount in onMounted, clean up in onUnmounted
Section titled “2. Mount in onMounted, clean up in onUnmounted”Create the iframe after the component’s DOM exists (onMounted), and remove it when the component is destroyed (onUnmounted). Because there’s no unmount() API, “clean up” means clearing the container element’s children yourself.
mount(target, config) takes a CSS selector string or a direct HTMLElement, plus a config object. The only required field is pk (your publishable key, pk_op_...). It returns the created HTMLIFrameElement.
Mount throws on two conditions. It throws if the target element isn’t found, and if
config.pkis missing or not a non-empty string. Guard the call accordingly.
A direct element reference (a Vue template ref) is cleaner than a global selector, since it avoids id collisions across components.
<script setup>import { ref, onMounted, onUnmounted } from "vue";
const containerRef = ref(null);let iframe = null;
onMounted(() => { if (!window.ExpeditionInsure || !containerRef.value) return; iframe = window.ExpeditionInsure.mount(containerRef.value, { pk: "pk_op_...", destination: "antarctica", startDate: "2026-12-01", endDate: "2026-12-14", ages: [45, 47], residence: "US", tripCost: 18000, travelers: 2, });});
onUnmounted(() => { // No unmount() API — remove the iframe by clearing the container. if (containerRef.value) containerRef.value.replaceChildren(); iframe = null;});</script>
<template> <div ref="containerRef" /></template>The iframe is appended into your container with width:100%, border:0, and a 520px first-paint height that the embed replaces automatically as its content resizes. You don’t size it.
Config params
Section titled “Config params”All fields are optional except pk. The loader serializes arrays to CSV and omits empty values.
| Param | Type | Example | Notes |
|---|---|---|---|
pk | string (required) | "pk_op_..." | Publishable operator key. Public by design (like a Stripe pk_); the real trust boundary is your server-side origin allowlist. |
destination | string | "antarctica" | Trip destination slug. |
startDate | string | "2026-12-01" | ISO date. |
endDate | string | "2026-12-14" | ISO date. |
ages | number[] | string | [45, 47] | Serialized to CSV. |
residence | string | "US" | Residence country code. |
tripCost | number | string | 18000 | Total trip cost (whole dollars). |
travelers | number | string | 2 | Traveler count. |
ref | string | — | Your referral/tracking ref. |
logoUrl | string | — | Logo URL for co-branding. |
accentColor | string | — | Brand accent color. |
3. Wire events
Section titled “3. Wire events”Subscribe with window.ExpeditionInsure.on(eventName, handler). It returns an unsubscribe function — keep it and call it in onUnmounted so a destroyed component doesn’t keep firing handlers. Handlers are isolated: a throw in one of yours is caught and logged by the loader and can’t break the bridge or other listeners.
on() throws if you pass an event name that isn’t one of the five below.
| Event | Payload | When |
|---|---|---|
quote.ready | { quoteId, plansCount } | The quote loaded and plans are available. |
quote.selected | { planId, premiumCents, premium, currency } | The traveler picked a plan. |
quote.error | { code, message } | The quote failed to load or price. |
payment.succeeded | { quoteId, planId, premiumCents, premium, currency } | In-iframe checkout reported success. |
payment.failed | { code, message } | In-iframe checkout reported failure. |
Read
premiumCents— an integer in minor units (cents); divide by 100 for display.currencyis an optional string like"USD".
payment.succeededis a UX signal only — never your source of truth. It can be missed (a closed tab, a dropped message), so don’t reconcile a sale or grant entitlements from it. The authoritative post-sale signal is the server-to-serverpolicy.issuedwebhook; reconcile against that. See the webhooks reference.
Collect the unsubscribe functions and tear them all down on unmount:
<script setup>import { onMounted, onUnmounted } from "vue";
const emit = defineEmits(["selected"]);const unsubs = [];
onMounted(() => { const ei = window.ExpeditionInsure; if (!ei) return;
unsubs.push( ei.on("quote.ready", ({ quoteId, plansCount }) => { console.log("quote ready", quoteId, plansCount); }), ei.on("quote.selected", ({ planId, premiumCents, currency }) => { // premiumCents is canonical (integer minor units). emit("selected", { planId, premiumCents, currency }); }), ei.on("quote.error", ({ code, message }) => { console.warn("quote error", code, message); }), );});
onUnmounted(() => { unsubs.forEach((off) => off()); unsubs.length = 0;});</script>4. Re-mount on reactive trip-data change
Section titled “4. Re-mount on reactive trip-data change”There is no update() method — you can’t push new config into a mounted iframe. When reactive trip data changes (a new destination, dates, or traveler count), re-mount: clear the container and call mount() again with the new config.
Drive this with a watch on the reactive data. Clear the container first so you don’t stack iframes — each mount() appends a new one and the loader never removes prior iframes for you.
<script setup>import { ref, reactive, watch, onMounted, onUnmounted } from "vue";
const containerRef = ref(null);const unsubs = [];
const trip = reactive({ destination: "antarctica", startDate: "2026-12-01", endDate: "2026-12-14", ages: [45, 47], residence: "US", tripCost: 18000, travelers: 2,});
function mountEmbed() { const ei = window.ExpeditionInsure; if (!ei || !containerRef.value) return;
// Clear any prior iframe before re-mounting. containerRef.value.replaceChildren();
ei.mount(containerRef.value, { pk: "pk_op_...", ...trip });}
onMounted(() => { const ei = window.ExpeditionInsure; if (!ei) return;
// Subscribe once — listeners are global, not per-iframe. unsubs.push( ei.on("quote.selected", ({ planId, premiumCents, currency }) => { console.log("selected", planId, premiumCents, currency); }), );
mountEmbed();});
// Re-mount whenever reactive trip data changes.watch(trip, () => mountEmbed(), { deep: true });
onUnmounted(() => { unsubs.forEach((off) => off()); unsubs.length = 0; if (containerRef.value) containerRef.value.replaceChildren();});</script>
<template> <div ref="containerRef" /></template>Subscribe to events once (in onMounted), not on every re-mount: on() registers a global listener shared by all mounted instances, so re-subscribing on each mount() would duplicate your handlers. The loader installs its message bridge once and reuses it across re-mounts.
5. Full single-file component
Section titled “5. Full single-file component”A complete, copy-pasteable Vue 3 SFC combining mount, event wiring, and re-mount on reactive change.
<script setup>import { ref, reactive, watch, onMounted, onUnmounted } from "vue";
const PK = "pk_op_...";
const containerRef = ref(null);const status = ref("loading");const selected = ref(null);const unsubs = [];
const trip = reactive({ destination: "antarctica", startDate: "2026-12-01", endDate: "2026-12-14", ages: [45, 47], residence: "US", tripCost: 18000, travelers: 2,});
function mountEmbed() { const ei = window.ExpeditionInsure; if (!ei || !containerRef.value) return; containerRef.value.replaceChildren(); ei.mount(containerRef.value, { pk: PK, ...trip });}
onMounted(() => { const ei = window.ExpeditionInsure; if (!ei) { status.value = "error"; return; }
unsubs.push( ei.on("quote.ready", () => { status.value = "ready"; }), ei.on("quote.selected", ({ planId, premiumCents, currency }) => { // premiumCents is integer minor units (cents); divide by 100 for display. selected.value = { planId, premiumCents, currency }; }), ei.on("quote.error", ({ message }) => { status.value = "error"; console.warn("quote error:", message); }), // payment.succeeded is UX-only — reconcile sales from the policy.issued webhook. ei.on("payment.succeeded", ({ quoteId }) => { console.log("payment reported for", quoteId, "(awaiting policy.issued)"); }), );
mountEmbed();});
watch(trip, () => mountEmbed(), { deep: true });
onUnmounted(() => { unsubs.forEach((off) => off()); unsubs.length = 0; if (containerRef.value) containerRef.value.replaceChildren();});</script>
<template> <div> <p v-if="status === 'loading'">Loading your quote…</p> <p v-else-if="status === 'error'">Couldn't load the quote.</p> <p v-if="selected"> Selected plan {{ selected.planId }} — {{ selected.premiumCents }} {{ selected.currency }} (cents) </p> <div ref="containerRef" /> </div></template>Load widget.js in your page (see section 1) before this component mounts. For the request-level API behind the embed — keys, rate limits, and status codes — see the embed HTTP API reference.