Collecting Premiums on Your Own Platform (Connect Distributor)
This guide is for partners who sell Sitata insurance inside their own booking portal and collect payment through their own Stripe Connect platform, while Sitata remains the merchant of record for the premium. Your traveller enters their card once in your checkout; the premium appears on their statement as a separate charge from Sitata.
If you instead collect payment entirely yourself (you are the merchant of record) or use Sitata's checkout, see the standard purchase guide.
How it works
You create two PaymentIntents on your own Stripe platform with your own API keys:
- Your trip charge โ exactly as you do today. Your statement descriptor, your merchant of record, your supplier splits.
- The insurance premium โ a separate PaymentIntent with three extra parameters that make Sitata the settlement merchant and move the funds automatically:
{
"amount": "<premium_amount from the payment session>",
"currency": "<currency_code from the payment session>",
"on_behalf_of": "<SITATA_ACCOUNT_ID>",
"transfer_data": { "destination": "<SITATA_ACCOUNT_ID>" },
"application_fee_amount": "<commission_amount from the payment session>"
}
Because both PaymentIntents are on your platform, the card is entered once โ
the resulting PaymentMethod is confirmed against both PaymentIntents.
on_behalf_of puts Sitata's statement descriptor on the premium charge
(Sitata is the card-network merchant of record), and your commission stays
with you at source via the application fee. Sitata receives the net
amount (premium minus your commission) automatically.
Two charges, one card entry
These are two separate charges โ a merchant of record is a property of a charge, so a single charge cannot be split between you and Sitata. The traveller types their card once, but sees two lines on their statement (your descriptor for the trip, Sitata's for the premium), and each charge authorizes independently.
Note that a Stripe Elements form confirms one PaymentIntent โ it cannot take two. What bridges the two PaymentIntents is the PaymentMethod the form produces. Two standard integrations:
- Collect first, confirm both server-side (recommended). Use Elements
only to tokenize:
stripe.createPaymentMethod(...)โ send thepm_...to your server โ attach it to a Customer โ create and confirm both PaymentIntents server-side (payment_method: pm_..., confirm: true). If either confirmation returnsrequires_action(3DS), runstripe.handleNextAction(clientSecret)on the client for that PaymentIntent โ possibly once per charge, sequentially. - Elements confirms the trip, server confirms the premium. Initialize
Elements with the trip PaymentIntent's client secret and confirm it with
setup_future_usage: "off_session"(attaching the PaymentMethod to the Customer), then confirm the premium PaymentIntent server-side with that PaymentMethod, surfacingrequires_actionto the client if the issuer demands a second challenge.
Decide up front how your checkout handles a partial failure โ e.g. trip succeeds but the premium declines โ typically by offering an insurance payment retry rather than voiding the booking.
Sitata never charges the traveller in this flow. Policies are issued only after Sitata verifies, on its own Stripe account, that your payment arrived.
What you need from Sitata (one-time setup)
| Item | Purpose |
|---|---|
Sitata's connected account id (acct_...) | Referenced in on_behalf_of and transfer_data.destination. You create it on your platform when you onboard Sitata โ see Connecting your platform to Sitata below. |
Your company_id and company API token | Authentication for the /api/v2/org/:company_id/... endpoints. |
| Product identifiers | The products you are licensed to sell, used in quote requests. |
Sitata also enables this flow on your account (the connect_distributor
package) and configures your commission percentage โ contact your account
manager. There is no separate "activation" step for the connection itself:
your account is ready to collect as soon as Sitata's connected account id is
recorded (below).
Regional compatibility
Sitata and your platform do not need to be in the same country. Stripe
supports cross-region funds flows across its major regions, and on_behalf_of
is exactly what settles the premium as Sitata's โ in Sitata's region โ rather
than a same-region restriction. Confirm regional compatibility with Sitata
before building.
Connecting your platform to Sitata
Sitata collects the premium as a connected account on your Stripe Connect
platform. You provision that account with Stripe's modern
Accounts v2 API and onboard Sitata
with an Account Link. With
dashboard: full, Sitata keeps its own Stripe dashboard, payouts, and statement
descriptor โ which is what lets it be the merchant of record for the premium.
Use Accounts v2 โ not OAuth
Stripe deprecated the legacy connected-account types (Standard/Express/Custom)
and OAuth-for-Standard for new platforms. Provision Sitata with
POST /v2/core/accounts, not the old OAuth "connect an existing account" flow.
Stripe also binds a connected account to one platform โ so you create a
Sitata connected account on your platform. Sitata reuses one verified legal
entity across partners via
networked onboarding, so
onboarding is quick and KYC isn't repeated.
Step 1 โ Create Sitata's connected account
Call POST /v2/core/accounts with your platform secret key. Sitata must be able
to be merchant of record and receive transfers:
curl https://api.stripe.com/v2/core/accounts \
-H "Authorization: Bearer <YOUR_PLATFORM_SECRET_KEY>" \
-H "Stripe-Version: 2026-06-24.preview" \
--json '{
"dashboard": "full",
"defaults": { "responsibilities": {
"fees_collector": "application",
"losses_collector": "application"
} },
"configuration": {
"merchant": {},
"recipient": { "capabilities": { "stripe_balance": {
"stripe_transfers": { "requested": true }
} } }
}
}'
configuration.merchantโ required for Sitata to be merchant of record (viaon_behalf_of).configuration.recipient(withstripe_transfers) โ required for Sitata to receive the net premium.dashboard: fullโ Sitata gets its own Stripe dashboard, payouts, and descriptor.
The response id (acct_...) is Sitata's connected account id โ your
on_behalf_of / transfer_data.destination target.
Step 2 โ Onboard Sitata
Create a hosted-onboarding link and send its url to Sitata:
curl https://api.stripe.com/v2/core/account_links \
-H "Authorization: Bearer <YOUR_PLATFORM_SECRET_KEY>" \
-H "Stripe-Version: 2026-06-24.preview" \
--json '{
"account": "<SITATA_CONNECTED_ACCOUNT_ID>",
"use_case": {
"type": "account_onboarding",
"account_onboarding": {
"configurations": ["merchant", "recipient"],
"refresh_url": "https://your-platform.example.com/reauth",
"return_url": "https://your-platform.example.com/return"
}
}
}'
The link is single-use. Sitata opens it, reuses its verified legal entity, adds its payout bank, and accepts the Connect service agreement.
Step 3 โ Record the account ids
In the Sitata partner portal, go to Settings โ Stripe Connect (premium collection) (shown once your account has the connect-distributor package). Enter these non-secret values โ your Stripe secret key is never entered here:
| Field | What it is | Looks like |
|---|---|---|
| Connected account ID (Sitata) | The account you created for Sitata in Step 1 | acct_... |
| Platform account ID (yours) | Your platform's own Stripe account ID (Business settings) | acct_... |
Save. The card shows Ready to collect as soon as Sitata's Connected account ID is recorded โ there is no activation step.
Step 4 โ Sitata confirms your account is connected
After you record the account ids, Sitata completes the final connection setup on its side. Sitata will confirm when your account is connected and ready for use โ no further action is needed from you.
Quote and bind flow
1. Quote
POST /api/v2/products/quote โ the sale-quote endpoint, authenticated as
your company. Send trip destinations and dates, traveller details,
currency_code, and product ids. The response is a sale group: use its
top-level id as the quote_id in step 2, and show the full premium to the
traveller. No payment intent is created.
Use /products/quote โ and its top-level id
Only POST /api/v2/products/quote creates the sale group that a payment
session needs. Do not use:
POST /api/v2/products/with_quotesโ that's a price-discovery endpoint. It returns individual product quotes (line items), not a sale group, and their ids are not validquote_ids.- The nested
product_quotes[].idorproduct_quote_idsfrom the response โ those are line-item ids, not the sale group.
The quote_id is always the response's top-level id. Sending anything else
makes step 2 return 404 Not Found (the id doesn't resolve to a sale group).
Showing several options first? (e.g. product tiers)
Use /products/with_quotes for price discovery, then /products/quote for
the chosen product:
POST /api/v2/products/with_quotesโ returns prices for the set of products this company can sell (e.g. the tiers of a product). Use it to display options. It returns line-itemProductQuotes, not a sale group.- The traveller picks one โ
POST /api/v2/products/quotewith that product โ creates the sale group; take its top-levelid. POST /api/v2/org/:company_id/payment_sessionswith thatid.
If you already know exactly which product you're selling, skip step 1 and go
straight to /products/quote.
2. Create a payment session
POST /api/v2/org/:company_id/payment_sessions
{ "quote_id": "<the sale group's top-level id from step 1>" }
A 404 Not Found from this call means quote_id did not resolve to a sale
group โ you're most likely passing a product-quote id (from
/products/with_quotes) or a nested product_quotes[].id. Use the top-level
id returned by POST /api/v2/products/quote.
Response:
{
"payment_session": {
"id": "ps_...",
"quote_id": "...",
"premium_amount": 10000,
"commission_amount": 2000,
"net_amount": 8000,
"currency_code": "USD",
"status": 0
}
}
This is your exact charging instruction: charge premium_amount in
currency_code with application_fee_amount = commission_amount, so that
exactly net_amount arrives on Sitata's account. The call is idempotent per
quote โ repeating it returns the same session (amounts refresh if the quote
was re-priced, while still unconfirmed).
3. Charge the traveller (your side)
In your checkout, confirm both PaymentIntents (trip + premium) against the traveller's card. The premium PaymentIntent uses the parameters shown above.
caution
The premium must be charged in the session's currency_code and for exactly
premium_amount, with the application fee exactly commission_amount. Any
mismatch in the amount arriving on Sitata's account fails verification and no
policy is issued.
4. Find the payment id on Sitata's account
When your premium charge succeeds, Stripe automatically transfers the premium
to Sitata and creates a payment object on Sitata's connected account. That
object has its own id, starting with py_... โ and that is the id you register
in step 5. Your own pi_... / ch_... ids won't work, because Sitata verifies
against the object on its connected account.
You can read the py_... id in a single API call with your own keys, by
retrieving your premium PaymentIntent with an expand parameter:
curl https://api.stripe.com/v1/payment_intents/{PREMIUM_PAYMENT_INTENT_ID} \
-u sk_your_secret_key: \
-d "expand[]=latest_charge.transfer.destination_payment"
Then read it from the response at:
latest_charge.transfer.destination_payment.id โ "py_..."
The path simply follows the money: your charge โ the automatic transfer it created โ the payment that transfer deposited on Sitata's account.
5. Bind โ register the payment
POST /api/v2/org/:company_id/payment_sessions/:id/confirm_payment
{ "payment_id": "py_..." }
This registers the payment id against the session (single-use). It does not
itself issue policies: Sitata verifies and issues from the authoritative
webhook Stripe delivers to Sitata on its connected account (a payment.created
event for the destination payment) โ never from your report. Registration and
that webhook converge in either order, so the call is always safe to retry.
The response is the session's current state:
status: 1(captured) โ the webhook had already verified; policies are issued.status: 0(created) โ pending; the webhook hasn't arrived yet. Sitata verifies and issues when it lands.
Verification checks the signed webhook charge: paid, not refunded, currency,
amount exactly the premium (premium_amount), arrived via the platform transfer,
and on Sitata's own connected account. On failure the session becomes status: 2
(verification_failed) with a failure_reason โ retry by registering a
corrected payment id. A given payment can only ever issue one set of policies.
6. Know when policies are issued
You don't need to poll. When each policy is issued, Sitata fires a
subscription_created webhook to your configured endpoint (travellers also
receive their confirmations by email). You can still read session state at any
time:
GET /api/v2/org/:company_id/payment_sessions/:id
Refunds and cancellations
The premium charge lives on your platform, so refunds are always executed on your side. There are two directions:
You initiate (traveller cancels with you)
Refund the traveller's premium charge on your platform with
reverse_transfer: true (and refund_application_fee at your discretion โ
whether you return your commission is between you and your Sitata agreement).
The transfer reversal automatically returns the net premium from Sitata's
account. No Sitata API call is needed: Sitata detects the reversal,
cancels the policies, and emails the travellers. Partial refunds are
supported and are applied proportionally.
Sitata initiates (traveller cancels with Sitata)
If a traveller cancels their policy through Sitata (or Sitata must cancel
it), Sitata cannot move the money โ you own the charge. Sitata sends a
connect_premium_refund_requested webhook to your configured endpoint:
{
"event": 912,
"payment_session_id": "ps_...",
"quote_id": "...",
"external_payment_id": "py_...",
"net_amount": 8000,
"currency_code": "USD",
"subscription_ids": ["..."]
}
On receiving it, execute the refund on your platform (as above, with
reverse_transfer). Reconciliation then completes automatically through the
same reversal detection. Your partner agreement defines the SLA for acting on
refund requests.
Commercial notes
- Your platform bears Stripe's processing fee on the premium charge
(standard destination-charge mechanics), so your effective margin is
application_fee_amountminus the Stripe fee. - Disputes: Sitata is the card-network merchant of record for the premium
(Sitata's statement descriptor; Sitata handles representment). But for a
destination charge โ even with
on_behalf_ofโ Stripe debits your platform for the disputed amount and fees first. Recover it from Sitata by reversing the transfer (reverse_transfer, anddebit_negative_balancesif needed). So the premium's financial chargeback liability lands on you first, then flows to Sitata via the reversal. Disputes on your trip charge are yours outright.
Webhooks
This flow relies on two webhooks from Sitata, so configure a webhook endpoint before going live:
| Event | Name | Fires when |
|---|---|---|
908 | subscription_created | Each policy is issued โ your signal that a sale bound (see step 6). |
912 | connect_premium_refund_requested | Sitata needs you to refund a premium on your platform (see Sitata initiates). |
Register an endpoint and subscribe to both (leave enabled_events empty to
receive everything). The full API is in the
Webhook Endpoints reference,
and every event is listed under
Webhook Event Types:
POST /api/v2/org/:company_id/webhook_endpoints
{
"webhook_endpoint": {
"enabled": true,
"url": "https://www.your-company.co/webhooks/sitata",
"enabled_events": [908, 912]
}
}
Every delivery carries a Sitata-Signature: t=<timestamp>,v1=<signature> header
โ an HMAC-SHA256 of "<timestamp>.<raw request body>" keyed by the
endpoint's signing_secret. Recompute and compare it before trusting a payload;
the body is { "event": <Integer>, "payload": { ... } }. For the full walkthrough
and signature details, see
Notifications & Webhooks.