Skip to main content

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:

  1. Your trip charge โ€” exactly as you do today. Your statement descriptor, your merchant of record, your supplier splits.
  2. 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 the pm_... to your server โ†’ attach it to a Customer โ†’ create and confirm both PaymentIntents server-side (payment_method: pm_..., confirm: true). If either confirmation returns requires_action (3DS), run stripe.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, surfacing requires_action to 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)

ItemPurpose
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 tokenAuthentication for the /api/v2/org/:company_id/... endpoints.
Product identifiersThe 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 (via on_behalf_of).
  • configuration.recipient (with stripe_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:

FieldWhat it isLooks like
Connected account ID (Sitata)The account you created for Sitata in Step 1acct_...
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 valid quote_ids.
  • The nested product_quotes[].id or product_quote_ids from 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:

  1. 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-item ProductQuotes, not a sale group.
  2. The traveller picks one โ†’ POST /api/v2/products/quote with that product โ†’ creates the sale group; take its top-level id.
  3. POST /api/v2/org/:company_id/payment_sessions with that id.

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_amount minus 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, and debit_negative_balances if 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:

EventNameFires when
908subscription_createdEach policy is issued โ€” your signal that a sale bound (see step 6).
912connect_premium_refund_requestedSitata 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.