Skip to content

Server-to-server (secret keys)

This page covers tier-3 server-to-server integration with a secret key (sk_op_...) — what it’s for, its honest current status, the planned request shape, and how to store and rotate the secret. It’s for operator backends that want to call Expedition Insure from a trusted server instead of (or alongside) the browser embed.

Before reading further, know the headline: the server API is not callable today. Secret keys are minted, stored, and resolvable, but no HTTP route consumes one yet. Treat this page as forward guidance, not a working contract. For what does work today, see the integration guide (browser embed, publishable keys), the events reference, and the webhooks guide.

The browser embed authenticates every request with a publishable key (pk_op_...). A publishable key is public by design — like a Stripe pk_ — because the security boundary is your per-operator origin allowlist plus a per-key rate limit, not key secrecy. It ships in your page’s HTML and runs in the browser.

A secret key (sk_op_...) is the opposite: it is meant to live only on your server and never reach a browser. You’d reach for one when you want to call Expedition Insure from a trusted backend — for example to create a quote server-side, fetch options without exposing the call to the client, or run a higher-trust tier-3 flow where origin-based browser trust doesn’t apply.

Not available yet. The use cases above describe the intended role of sk_op_. As of today there is no endpoint that accepts a secret key. See the next section.

This is the part to read carefully before you plan any work around secret keys.

What exists end-to-end today:

  • Minting — a secret key can be issued for your operator. It is returned once at mint time and never retrievable again.
  • Storage — Expedition Insure stores only a SHA-256 hash of the secret (plus a short non-secret prefix for identification), never the raw secret.
  • Resolution — an internal resolver can look up an operator from a presented secret by hashing it and matching the stored hash.

What does not exist today:

  • No HTTP route consumes a secret key. The resolver has zero callers. There is no tier-3 server-to-server endpoint.
  • The two embed routes that exist — POST /api/embed/quote and GET /api/embed/options — authenticate only with a publishable key (pk_op_...) via the embed origin/rate-limit gate. Presenting an sk_op_... to them does nothing useful.

Net: the secret key is plumbed end-to-end (mint → store-as-hash → resolver), but the server API it is meant to gate is not callable. Right now an sk_op_... gates nothing reachable over HTTP. Build against the publishable-key embed flow today; revisit this page when the server API ships.

[needs-product-work] A tier-3 server-to-server endpoint that consumes sk_op_... is not implemented. The request shape below is planned, not live — do not integrate against it yet.

Planned request shape [needs-product-work]

Section titled “Planned request shape [needs-product-work]”

When the server API lands, expect it to mirror the existing embed contract closely, with the secret key replacing the browser’s origin-based trust. Nothing below is callable today; it is a forecast of the shape so you can plan storage and rotation now.

The publishable key is presented today via the X-EI-Publishable-Key header. A server endpoint would analogously carry the secret key in a request header, sent only from your backend over HTTPS — never embedded in client code, never in a query string.

POST /api/.../quote HTTP/1.1
Host: <to be published>
Content-Type: application/json
Authorization: Bearer sk_op_...

The body and response would track the existing embed quote contract. For reference, today’s publishable-key quote route accepts these fields:

FieldTypeRequiredNotes
destinationstringyesTrip destination.
tripCostnumberyesTotal trip cost (whole dollars).
travelersnumberyesTraveler count.
residencestringyesResidence country code.
emailstringyesLead email, e.g. traveler@example.com.
startDatestringnoISO date.
endDatestringnoISO date.
durationDaysnumbernoDerived from startDate/endDate when omitted.
travelerAgesnumber[]noAges at departure.
travelerDobsstring[]noResolved to age at departure server-side.
currencystringnoDefaults to the quote’s currency.
namestringnoDefaults to "Embedded quote".

A successful quote response today returns:

{ "quoteId": "<quoteId>", "quoteNumber": "EXP-XXXX", "instantQuoteEligible": true }

Money fields. Throughout the platform, premiumCents is the integer-minor-units field — integrate against premiumCents and divide by 100 for display. Whole-dollar values like tripCost are plain dollars.

Payment sequencing. If a server flow ever surfaces a payment.succeeded signal, treat it as a UX-only hint that can be missed. The authoritative post-sale signal is the policy.issued webhook — reconcile against that, never against a payment-success event.

Even though the server API isn’t live, the storage and rotation discipline below is what you’ll need the moment it is — and it’s good hygiene for any secret credential.

  1. Capture the secret at mint time. A secret key is shown once and is never retrievable again. If you lose it, you must rotate (issue a new one).
  2. Keep it server-side only. Never ship sk_op_... to a browser, a mobile client, a public repo, or client-side bundles. The publishable key (pk_op_...) is the credential for browser contexts; the secret key is for your backend.
  3. Store it in a secrets manager, not in source control or plain config files. Use environment variables or a dedicated secrets store (for example your platform’s secret manager). Inject it at runtime.
  4. Send it only over HTTPS, only in a request header from your server. Never put it in a URL, query string, or log line.
  5. Identify keys by their prefix. Expedition Insure stores a short non-secret prefix (sk_op_ plus a few characters) so you can recognize which key is in use without exposing the full secret. Use that prefix in your own dashboards and logs.

Rotate a secret key when you suspect exposure, when staff with access leave, or on a regular schedule. Plan your backend so rotation is a config change, not a code change:

  1. Read the secret from one place (a single env var or secret-store entry) so rotating means updating one value.
  2. Issue a new secret for your operator, then update the stored value and redeploy/restart so your backend picks it up.
  3. Revoke the old key once the new one is confirmed working. Revocation is idempotent.
  4. Never email, paste, or commit a secret during rotation. Move it only through your secrets manager.

Because the server API is not live yet, you cannot exercise an end-to-end rotation against a working endpoint today. Set up the single-source-of-truth storage now so you’re ready when it ships.

  • Integration guide — the browser embed with publishable keys (works today).
  • Events reference — the quote.* and payment.* events the embed emits.
  • Webhooks — the signed policy.issued webhook, the authoritative post-sale signal.