Skip to content

Errors and status codes

This page is the failure reference for the Expedition Insure embed HTTP API: every status code the gate and routes return, what triggers each, the exact response body, whether CORS headers are present, and the one “not an error” shape you must poll rather than fail on. It’s for operator tech teams and AI-agent builders wiring up the embed. For the success contracts see /reference/embed-api/; for the event payloads your handlers receive see /reference/events/; for the loader you embed see the /embedded-insurance/integration-guide/.

Both embed endpoints — POST /api/embed/quote and GET /api/embed/options — run the same gate (publishable-key auth, then Origin allowlist, then rate limit) before any route logic. So most failures look identical across both endpoints; the per-route 400/403 cases differ and are called out below.

StatusMeaningTriggerBodyCORS headers?
200OKGate passed; route ran. Includes the pending poll shape — see §4.Route-specific JSONYes
400Bad requestMissing/invalid body or query field, after a passing gate.{ "error": "..." }Yes
401UnauthorizedNo pk presented, or pk unknown/revoked, or operator inactive / embedding disabled.UnauthorizedNo
403ForbiddenOrigin header missing or not on your allowlist; or (on options) a cross-operator/unknown quoteId.ForbiddenNo
429Too many requestsRate limit exceeded for your pk (60 / minute).Too Many RequestsYes + Retry-After

The missing CORS headers on 401/403 are deliberate. A failed gate returns no Access-Control-Allow-Origin, so even though a body string is present, the browser blocks your page from reading it. You’ll see the status code in the network panel but an opaque/blocked body in JavaScript. That’s expected — it removes any existence or permission oracle. 429 is the exception: it carries CORS headers and a Retry-After so your client can back off correctly.

Returned after a passing gate, when the route can’t use what you sent. The body is always JSON: { "error": "<message>" }, and CORS headers are present (you can read it).

CauseBody
Body missing/unparseable, or any required field missing or the wrong type{ "error": "Missing or invalid required fields" }
Quote creation rejected downstream (e.g. an invalid field combination){ "error": "<reason>" } (falls back to Quote could not be created)

Required body fields (all must be present and the right type, or you get the first message above):

FieldTypeNotes
destinationstringDestination slug or name.
tripCostnumberTotal trip cost in whole dollars (not per-traveler, not cents).
travelersnumberNumber of travelers.
residencestringTraveler residence (country code).
emailstringLead email. Use a placeholder like traveler@example.com in examples.
CauseBody
Missing quoteId query param{ "error": "quoteId required" }

These come from the gate, before any route logic. The body is a bare string and there are no CORS headers (see §1).

StatusTrigger
401No pk presented at all — neither the X-EI-Publishable-Key header nor the ?pk= query fallback.
401pk is unknown or revoked, or your operator is inactive, or embedding is disabled for your operator.
403The request has no Origin header, or its Origin is not on your operator’s allowlist (a foreign / cross-operator origin).

401 vs 403 — key vs origin. A 401 is about the key: it’s missing, bad, revoked, or your operator is switched off. A 403 is about the origin: the key is fine but the request came from a page we haven’t allowlisted for you. A copied pk from a non-allowlisted origin lands here — the key is public by design, and the Origin allowlist is the real boundary. If you need a new origin added, send it to us.

The publishable key travels in the X-EI-Publishable-Key header on every request, with ?pk= as a GET fallback. The header is read first.

X-EI-Publishable-Key: pk_op_...

On GET /api/embed/options, a quoteId that belongs to a different operator — or that’s unknown or malformed — returns a bare 403 with no CORS headers, exactly like a foreign origin. This is intentional: there is no existence oracle, so you can’t tell “wrong operator” apart from “no such quote.” Only request options for quotes your own pk created.

4. The pending shape — poll, don’t fail

Section titled “4. The pending shape — poll, don’t fail”

GET /api/embed/options returns 200 with this body while estimates are still generating (or before the quote is eligible):

{ "options": [], "pending": true }

This is not an error. It’s a 200 with CORS headers, telling you the quote exists and options aren’t ready yet. Poll the endpoint until pending is absent and a real options payload comes back. Treating pending: true as a failure is the most common integration bug.

POST /api/embed/quote -> { quoteId, quoteNumber, instantQuoteEligible }
loop:
GET /api/embed/options?quoteId=<quoteId>
200 { "options": [], "pending": true } -> wait, then poll again
200 <options payload> -> render the plans
400 { "error": "quoteId required" } -> you dropped the param
403 (no CORS body) -> wrong operator / unknown id
429 + Retry-After -> back off, then resume polling

Respect the rate limit while polling (§5) — don’t hammer the endpoint. Passing ages on the quote improves how quickly options become quotable.

The embed API allows 60 requests per minute per publishable key, on a fixed window. Exceed it and the gate returns:

HTTP 429 Too Many Requests
Retry-After: <seconds>

Unlike 401/403, the 429 response does carry CORS headers, so your client can read the status and the Retry-After value. Wait the indicated number of seconds, then resume. The window resets lazily; Retry-After tells you exactly how long until the current window clears.

Don’t poll faster than you back off. When polling options (§4), a 429 means your poll cadence plus quote creations exceeded 60/minute on that key. Honor Retry-After rather than retrying immediately, or you’ll stay rate-limited.

6. quote.error — the in-widget error event

Section titled “6. quote.error — the in-widget error event”

Failures that happen inside the iframe (the quote/options request failed, an input was rejected) don’t surface as HTTP codes to your page — the iframe makes those calls. Instead, the widget emits a quote.error event over the loader’s .on() channel:

ExpeditionInsure.on("quote.error", (e) => {
console.warn("quote error", e.code, e.message);
});

Payload shape:

FieldTypeNotes
codestring (optional)Short machine-readable code, when available.
messagestring (optional)Human-readable description, when available.

Both fields are optional — handle a quote.error with neither present as a generic failure. Use it to show a fallback (“we couldn’t price this trip — contact a human”) rather than leaving the user on a silent hang. For the full event catalog and payloads, see /reference/events/.

Map the gate codes to what you see. When quote.error fires immediately, the underlying HTTP failure is almost always one of the gate codes above: a 401 (bad or disabled key), a 403 (origin not allowlisted), or a 429 (rate limit). Check the network panel inside the iframe context, then cross-reference §1.