Skip to content

Reconciliation & attribution

Once the embed is live, you need to tie real insurance policies back to the bookings on your site and reconcile what you’re owed. This page is for the engineers wiring that up. It covers the attribution join key, the policy.issued webhook as the authoritative post-sale signal, and the attribution stats shown in your operator dashboard.

It pairs with the embed integration guide, which covers the widget, its in-browser events, and the key/origin exchange. Browser events (payment.succeeded, quote.selected) are UX signals only — for money and policy state, use the webhook described below.

Every quote created through your embed is tagged with your operatorId server-side. This is the canonical attribution join key, and it’s set from your publishable key (pk_op_...) or the ?origin=<slug> attribution slug — never from anything the browser can spoof. You don’t pass operatorId yourself; we resolve it and stamp it on the quote.

When an attributed quote results in a paid, issued policy, we write one immutable commission-ledger row keyed to that quote and your operatorId. That row — not a browser event — is what drives your stats and what you reconcile against.

operatorId ties a policy to you, but it can’t tie a policy back to a specific booking on your side. For that, pass your own correlation value as ref in the mount config:

<script src="https://expedition.insure/widget.js"></script>
<script>
ExpeditionInsure.mount("#quote", {
pk: "pk_op_...",
destination: "antarctica",
tripCost: 18000, // whole dollars
travelers: 2,
ref: "booking_8472", // your booking / order id
});
</script>
FieldWhere it livesWhat it identifies
operatorIdSet server-side on the quote, from pk/?origin=<slug>Which operator (you) gets the policy + commission
refYou pass it in mount(...) configWhich booking on your side a quote/policy maps to

ref is a free-text string. It rides along through the quote flow as redundant attribution and is appended to the hosted-checkout handoff URL, so it’s available alongside the canonical operatorId. Keep it short and opaque — a booking id or order reference, not PII. There is no fixed maximum length enforced today, but treat it as a URL query value: keep it well under a few hundred characters, URL-safe, and stable for the lifetime of the booking so you can match on it later. [needs-product-work]: ref is not currently echoed back in the policy.issued webhook payload — see the gap below.

Browser events are not attribution. quote.selected and payment.succeeded fire in the iframe and can be missed (closed tab, blocked frame, navigation). Never reconcile off them. They’re for in-page UX — updating your cart UI, logging intent. The authoritative record is the webhook.

2. The policy.issued webhook — source of truth

Section titled “2. The policy.issued webhook — source of truth”

When an attributed payment succeeds and produces a policy, we send you a signed policy.issued webhook. This is the only authoritative post-sale signal: it fires server-side off the Stripe payment, independent of whether the customer’s browser ever got back to your page. Match these to your bookings; do not match off payment.succeeded.

To receive it, we configure a webhookUrl and a shared webhookSecret for your operator account. [needs-product-work]: there is no self-serve setter for these yet — contact us at help@expedition.insure to register your endpoint and secret.

Content-Type: application/json. Body:

{
"event": "policy.issued",
"eventId": "b3c1d2e4-...",
"policyId": "POL-12345",
"operatorId": "<your operatorId>",
"premiumCents": 42300,
"commissionCents": 4230,
"premium": 423,
"commission": 42,
"issuedAt": 1765400000000,
"sentAt": 1765400000123
}
FieldTypeNotes
eventstringAlways policy.issued. The only event type we send today.
eventIdstringStable UUID, identical across every retry of the same event. Dedupe on this.
policyIdstring | nullThe policy/certificate number, or null if not yet assigned.
operatorIdstringYour operator id — confirm it matches before processing.
premiumCentsintegerPremium in integer minor units (cents).
commissionCentsintegerYour commission in integer minor units (cents).
issuedAtintegerEpoch milliseconds the policy was issued.
sentAtintegerEpoch milliseconds this delivery attempt was sent (differs per retry).

Use premiumCents and commissionCents. They’re integer minor units (cents) — divide by 100 for display.

Every delivery carries an X-EI-Signature header in the Stripe scheme:

X-EI-Signature: t=<unix-seconds>,v1=<hex-hmac>

Verify it before trusting the body:

  1. Parse t and v1 from the header.
  2. Compute HMAC-SHA256(webhookSecret, "<t>.<rawRequestBody>"), lowercase hex.
  3. Constant-time compare your digest to v1.
  4. Reject if t is outside a ±5 minute tolerance window (replay / clock-skew guard, enforced on your side).

Sign the raw body. Compute the HMAC over the exact bytes you received, before any JSON re-serialization. Re-encoding can reorder keys or change whitespace and break the signature.

The t in the signature is unix seconds. The issuedAt/sentAt fields in the body are epoch milliseconds. Different units, intentionally — don’t multiply or divide one to get the other.

HeaderValue
Content-Typeapplication/json
X-EI-Eventpolicy.issued
X-EI-Signaturet=<unix-seconds>,v1=<hex>
User-AgentExpeditionInsure-Webhook/1

Build your endpoint to be idempotent and at-least-once tolerant:

  • Dedupe on eventId. The same event can arrive more than once with a different sentAt and signature. Treat a repeated eventId as already-processed.
  • Retries: up to 3 attempts total (1 initial + 2 retries). We retry on any non-2xx response, network error, or timeout. The request times out after 10 seconds, so respond fast — acknowledge with a 2xx and do heavy work asynchronously.
  • Backoff: after the first failure we wait ~30 seconds, then ~120 seconds before the final attempt.
  • Order is not guaranteed. Reconcile by eventId and operatorId, not by arrival order.
attributed payment succeeds
commission row written (one per attributed quote, immutable)
POST policy.issued ──▶ your webhookUrl
│ (non-2xx / error / timeout)
├─ retry after ~30s
└─ retry after ~120s (same eventId throughout)

Your operator portal surfaces a rolled-up view of attributed activity. These figures are scoped to your operatorId only — you never see another operator’s data. All money is in whole US dollars.

StatMeaning
attributedQuotesCount of quotes attributed to you.
premiumGmvUsdSum of premium across attributed quotes (a GMV proxy; whole dollars).
attributedSalesCount of attributed sales (commission-ledger rows, excluding voided).
salesPremiumUsdSum of attributed premium across those sales (whole dollars).
commissionOwedUsdCommission accrued and not yet paid out (whole dollars).
commissionPaidOutUsdCommission already paid out (whole dollars).
quotesTruncatedtrue when the quote scan hit its cap and the counts are incomplete.

Notes on reading these:

  • commissionOwedUsd vs commissionPaidOutUsd. Owed is what has accrued from issued policies but not yet been paid to you. Paid-out is what’s been settled. Payouts are handled out of band — we reconcile and pay via monthly invoice; the dashboard is the running tally, not a payment trigger.
  • quotesTruncated. The quote-side scan is bounded for performance (you can request up to a cap). When it’s true, the quote-derived figures (attributedQuotes, premiumGmvUsd) are a floor, not a complete total. Sales and commission figures come from the ledger and are not subject to this cap.
  • Commission is computed on gross attributed premium at your contracted rate. Your rate is configured by us and shown read-only in the portal; there’s no self-serve setter today.

We document current reality, not roadmap. Today:

  • policy.issued is the only webhook event. There is no policy.cancelled, policy.refunded, or commission.voided event. If a policy is later cancelled or a commission is clawed back, you are not notified by webhook. [needs-product-work]
  • ref is not returned in the webhook. Match policy.issued to your booking using operatorId plus your own records of which quote/policy you initiated. Round-tripping ref into the payload is [needs-product-work].
  • Payouts are manual. Settlement is reconciled and invoiced out of band; the dashboard’s commissionOwedUsd / commissionPaidOutUsd reflect that ledger, not an automated payout rail. [needs-product-work]

If you need to be notified of cancellations or refunds, reconcile periodically against your own policy records and reach out to help@expedition.insure — we’ll confirm the current state.