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.
1. Status codes at a glance
Section titled “1. Status codes at a glance”| Status | Meaning | Trigger | Body | CORS headers? |
|---|---|---|---|---|
200 | OK | Gate passed; route ran. Includes the pending poll shape — see §4. | Route-specific JSON | Yes |
400 | Bad request | Missing/invalid body or query field, after a passing gate. | { "error": "..." } | Yes |
401 | Unauthorized | No pk presented, or pk unknown/revoked, or operator inactive / embedding disabled. | Unauthorized | No |
403 | Forbidden | Origin header missing or not on your allowlist; or (on options) a cross-operator/unknown quoteId. | Forbidden | No |
429 | Too many requests | Rate limit exceeded for your pk (60 / minute). | Too Many Requests | Yes + Retry-After |
The missing CORS headers on
401/403are deliberate. A failed gate returns noAccess-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.429is the exception: it carries CORS headers and aRetry-Afterso your client can back off correctly.
2. 400 — bad or missing fields
Section titled “2. 400 — bad or missing fields”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).
POST /api/embed/quote
Section titled “POST /api/embed/quote”| Cause | Body |
|---|---|
| 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):
| Field | Type | Notes |
|---|---|---|
destination | string | Destination slug or name. |
tripCost | number | Total trip cost in whole dollars (not per-traveler, not cents). |
travelers | number | Number of travelers. |
residence | string | Traveler residence (country code). |
email | string | Lead email. Use a placeholder like traveler@example.com in examples. |
GET /api/embed/options
Section titled “GET /api/embed/options”| Cause | Body |
|---|---|
Missing quoteId query param | { "error": "quoteId required" } |
3. 401 / 403 — auth and origin
Section titled “3. 401 / 403 — auth and origin”These come from the gate, before any route logic. The body is a bare string and there are no CORS headers (see §1).
| Status | Trigger |
|---|---|
401 | No pk presented at all — neither the X-EI-Publishable-Key header nor the ?pk= query fallback. |
401 | pk is unknown or revoked, or your operator is inactive, or embedding is disabled for your operator. |
403 | The request has no Origin header, or its Origin is not on your operator’s allowlist (a foreign / cross-operator origin). |
401vs403— key vs origin. A401is about the key: it’s missing, bad, revoked, or your operator is switched off. A403is about the origin: the key is fine but the request came from a page we haven’t allowlisted for you. A copiedpkfrom 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_...Cross-operator quoteId on options → 403
Section titled “Cross-operator quoteId on options → 403”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 pollingRespect the rate limit while polling (§5) — don’t hammer the endpoint. Passing ages
on the quote improves how quickly options become quotable.
5. 429 — rate limit and Retry-After
Section titled “5. 429 — rate limit and Retry-After”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 RequestsRetry-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
429means your poll cadence plus quote creations exceeded 60/minute on that key. HonorRetry-Afterrather 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:
| Field | Type | Notes |
|---|---|---|
code | string (optional) | Short machine-readable code, when available. |
message | string (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.errorfires immediately, the underlying HTTP failure is almost always one of the gate codes above: a401(bad or disabled key), a403(origin not allowlisted), or a429(rate limit). Check the network panel inside the iframe context, then cross-reference §1.
Next steps
Section titled “Next steps”- /reference/embed-api/ — the embed HTTP API success contracts in full.
- /reference/events/ — every
.on()event and its payload. - /embedded-insurance/integration-guide/ — install the loader and wire up handlers.