Operator portal — keys and origins
The operator portal is where you self-serve the credentials your embed needs: your operator record, your publishable key (pk_op_...), and the list of origins allowed to call the embed API. This page is for partner engineering teams wiring Expedition Insure into a site or agent.
Sign in at expedition.insure/partner with Google or an email one-time code — the same account, no separate password. New partners can request a key from the docs home, or provision one directly once signed in.
Once you have a key and origins set, head to the embedded insurance integration guide to drop in the loader, and the events reference for the postMessage events the widget emits. Webhooks (the authoritative policy.issued signal) are covered in webhooks.
What the portal gives you
Section titled “What the portal gives you”When you sign in, the portal resolves your operator from your account and surfaces a sanitized summary — never your webhook secret, and your key as a prefix only (pk_op_ + first few chars). The two reads you use most:
| Function | What it returns |
|---|---|
getMyOperator | Operator summary: operatorId, name, slug, website, isActive, embedEnabled, embedAllowedTiers, allowedOrigins, commissionRateBps (read-only), publishableKeyPrefix, hasPublishableKey. Returns null when you are signed out or signed in without an operator — that state drives the “provision” call to action. |
getEmbedConfig | The embed-snippet bundle: your slug, your full publishableKey (public by design, like a Stripe pk_), allowedOrigins, a sampleOrigin, and ready-to-paste link and widget snippets. |
Your publishable key is public by design.
pk_op_...is meant to ship in browser code — exactly like Stripe’spk_. The security boundary is your per-operator origin allowlist plus a per-key rate limit, not key secrecy. Do not treat it as a secret.
1. Provision an operator
Section titled “1. Provision an operator”The easiest way to provision is the partner registration page: it walks you through the setup steps, signs you in, and creates your operator account in one flow, then drops you into the dashboard. Under the hood it calls provisionOperator({ name, website? }) — which creates your operator record in one call. You must be signed in. name is required, trimmed, non-empty, and at most 120 characters.
On success it inserts your operators row, grants your account the operator role, mints a default publishable key, and returns once:
{ "operatorId": "<operatorId>", "slug": "<derived-slug>", "publishableKey": "pk_op_...", "publishableKeyPrefix": "pk_op_..."}A few rules to know up front:
- One operator per account. If your account already owns an operator you get
"This account already has an operator". - Admin accounts cannot self-provision — operator and admin roles are mutually exclusive.
- Your
slugis derived fromname(lowercased, non-alphanumerics become-), with a numeric suffix (-2,-3, …) if it collides with an existing slug. The slug drives attribution (?origin=<slug>). - Embedding starts disabled — by design. Provisioning alone does not turn on the public embed surface —
embedEnableddefaults tofalse, and a request withpk_op_...against a disabled operator is rejected401. Going live is a short, deliberate review step: provision and test against your origins first, then email help@expedition.insure to enable the embed and confirm your commission terms. You keep self-serve control of your key and origins throughout.
2. Provision and rotate your publishable key
Section titled “2. Provision and rotate your publishable key”Provisioning mints your first key automatically. To roll it — on suspected exposure, or as routine hygiene — call rotatePublishableKey() (no arguments). It returns the new key once:
{ "publishableKey": "pk_op_...", "publishableKeyPrefix": "pk_op_..."}What breaks during rotation
Section titled “What breaks during rotation”Rotation is a hard cutover, not a grace period:
- Every existing non-revoked key for your operator is revoked immediately, and a single fresh key is issued.
- A revoked key resolves to
nullat the gate, so any embed request still using the oldpk_op_...returns401 Unauthorizedthe moment rotation completes. - There is no overlap window where both keys work. Sequence your rollout: mint the new key, deploy it everywhere it appears (loader config, server-rendered snippets, agent configs), then rotate so the old key is retired only after the new one is live.
Rotation does not change your origins, your slug, or your operator settings — only the key. After rotating, re-read
getEmbedConfigto copy the new key into your snippets.
3. Update your allowed origins
Section titled “3. Update your allowed origins”The origin allowlist is the real security boundary for browser traffic: the embed API echoes only a matched origin in its CORS response and uses the same list to gate framing. Manage it with updateAllowedOrigins({ origins: string[] }), which replaces the whole list with what you send.
Each entry is normalized (trimmed, deduped) and validated as a bare origin:
| Rule | Detail |
|---|---|
| Scheme + host only | Must equal the URL’s origin — no path, query, or fragment. https://shop.example.com is valid; https://shop.example.com/checkout is not. |
| HTTPS required | Every origin must be https, except localhost and 127.0.0.1, which may use http for local development. |
| Max 20 origins | More than 20 returns "Too many origins (max 20)". |
| Invalid entry | Any entry that fails validation throws Invalid origin: <value> and the whole call is rejected. |
Example payload:
{ "origins": [ "https://example.com", "https://shop.example.com", "http://localhost:3000" ]}The call returns the cleaned list:
{ "allowedOrigins": ["https://example.com", "https://shop.example.com", "http://localhost:3000"] }Propagation
Section titled “Propagation”The allowlist gates two things at the edge: which origin the embed API reflects in its Access-Control-Allow-Origin header, and the framing allowlist. The list is read on each request against your operator record, so a saved change takes effect for new requests right away.
What can lag is the browser side, not ours: embed responses always send Vary: Origin, so a CDN or browser cache keyed on a previously-allowed origin can serve a stale CORS decision until that cache entry expires. Preflight (OPTIONS) responses also carry Access-Control-Max-Age: 86400, so a browser may reuse a cached preflight for up to 24 hours. If you remove an origin and still see it working, clear the relevant cache or wait out the preflight max-age — the data-carrying request itself is always re-checked against the live allowlist.
A request from a non-allowlisted origin returns a bare
403with no CORS headers, so the browser blocks the response body from the page. Preflight may pass for an unlisted origin, but the realGET/POSTcannot read any data.
4. Test versus live
Section titled “4. Test versus live”Your standard pk_op_... key is a live credential against the production embed API. For testing the in-iframe (Tier-3) checkout without real money, generate a separate test key.
- Generate a test key. In the portal’s API keys card, click Generate test key to mint a
pk_op_test_...key. Swap it into the same embed snippet in place of your livepk_op_...key — nothing else changes. A test key drives the embedded checkout in Stripe test mode, so it can never settle real money, and a live key can never run in test mode. Pay with Stripe test cards to drivepayment.succeeded/payment.faileddeterministically. See testing and test cards. - Use distinct origins per environment. Add your local and staging origins (e.g.
http://localhost:3000,https://staging.example.com) alongside production origins in the same allowlist. Onlylocalhost/127.0.0.1may usehttp; everything else must behttps. - Rotate before going live if a live key was pasted into shared or non-production code, so production ships a key that never left your control. Rotating your live key does not affect your test key, and vice versa.
On rate limits. The embed API allows 60 requests per minute per publishable key (fixed window). If you share one key between heavy staging traffic and production, they share that budget and can
429each other — another reason to treat environments deliberately. See the embed API reference for the full limit and status-code behavior.