Skip to content

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.

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:

FunctionWhat it returns
getMyOperatorOperator 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.
getEmbedConfigThe 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’s pk_. 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.

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 slug is derived from name (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 — embedEnabled defaults to false, and a request with pk_op_... against a disabled operator is rejected 401. 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_..."
}

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 null at the gate, so any embed request still using the old pk_op_... returns 401 Unauthorized the 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 getEmbedConfig to copy the new key into your snippets.

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:

RuleDetail
Scheme + host onlyMust equal the URL’s originno path, query, or fragment. https://shop.example.com is valid; https://shop.example.com/checkout is not.
HTTPS requiredEvery origin must be https, except localhost and 127.0.0.1, which may use http for local development.
Max 20 originsMore than 20 returns "Too many origins (max 20)".
Invalid entryAny 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"] }

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 403 with no CORS headers, so the browser blocks the response body from the page. Preflight may pass for an unlisted origin, but the real GET/POST cannot read any data.

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 live pk_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 drive payment.succeeded / payment.failed deterministically. 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. Only localhost / 127.0.0.1 may use http; everything else must be https.
  • 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 429 each other — another reason to treat environments deliberately. See the embed API reference for the full limit and status-code behavior.