Integration guide · platforms on Stripe Connect
Recourse for Stripe Connect platforms
You hold the money on Stripe. Recourse decides the dispute and hands you the exact Stripe calls that carry out the decision. Three pieces: name the venue in your terms, send us the record, execute the instruction. This page is the whole integration.
Before you start: where is the money when a dispute happens?
| Your Connect pattern | What the ruling turns into | Settlement spec |
|---|---|---|
| Separate charges and transfers, transfer not yet made — the payment sits on your platform balance until you pay the seller (marketplaces, delivery, gig platforms) | transfer the seller's share to their connected account + refund the buyer's share | { rail: "stripe_connect", payment_intent, respondent_account } |
| Separate charges and transfers, transfer already made | transfer_reversal of the buyer's share back to your balance + refund | add transfer: "tr_…" |
| Destination charges — the money landed on the connected account at payment time | a refund with reverse_transfer: true, which pulls the buyer's share back from the connected account | add destination_charge: true |
Direct charges (the seller is the merchant of record and you never touch the funds) are out of scope: there is nothing on your account for a ruling to move. If you delay transfers until disputes clear — the usual pattern — you are in the first row, and rulings are two API calls.
1 · Name the venue in your terms
Your platform agreement with both sides — drivers and restaurants, buyers and sellers — refers disputes to determination under the Recourse Standard Rules. That sentence is the consent; nobody signs anything per dispute. Generate it at /clause (choose SaaS / platform terms), read the Rules in twenty minutes, and give your legal team the published decisions. Determinations bind as a matter of contract — expert determination, not arbitration — and your users get the full reasoning with every ruling.
2 · Set up your account
- Create a platform account and generate an API key on the dashboard. Keys are shown once.
- On the same dashboard, set your enforcement webhook URL and copy the signing secret. Every ruling for your disputes is POSTed there, HMAC-signed. Use Send a test event to prove your endpoint verifies signatures — the test event is marked
escalated: trueandtest: true, so a correct executor holds and executes nothing.
3 · Submit the dispute
When your support flow escalates a case beyond a macro, send us the record. One request:
curl https://recourse.so/disputes \
-H "authorization: Bearer rk_live_…" \
-H "content-type: application/json" \
-H "prefer: respond-async" \
-d '{
"external_ref": "order_88213",
"rulepack": "marketplace-contractor-v1",
"currency": "USD",
"amount_minor": 4250,
"appeal_window_hours": 72,
"terms": { "format": "text", "content": "<the order terms and the relevant platform policy, as shown to both sides>" },
"claim": { "by": "claimant", "claimed_breach": "Order marked delivered at 19:42; customer photo shows no package; driver GPS shows a stop two streets away.", "remedy_sought": "Refund." },
"log": [
{ "at": "2026-09-01T19:02:00Z", "actor": "platform", "kind": "status_change", "content": "Order accepted by restaurant; driver assigned 19:20." },
{ "at": "2026-09-01T19:42:00Z", "actor": "respondent", "kind": "delivery", "content": "Marked delivered: left at door. Photo attached." },
{ "at": "2026-09-01T19:50:00Z", "actor": "claimant", "kind": "message", "content": "Nothing at my door. Building has one entrance." }
],
"deliverables": [
{ "name": "driver-dropoff.jpg", "url": "https://cdn.your-platform.example/evidence/…" },
{ "name": "gps-trace.json", "content": "…" }
],
"settlement": { "rail": "stripe_connect", "payment_intent": "pi_3Q…", "respondent_account": "acct_1Q…" }
}'
- amount_minor is the amount actually in dispute (the driver's payout, the item price, the whole order — your call; the ruling always sums to it).
- terms is what both sides agreed: the relevant policy text is fine. log is the timeline as your system knows it — status changes, messages, photos, GPS. Images can be attached as
images[]for the arbiter to inspect. - respondent_account is whoever is being claimed against — the driver or the restaurant. If the customer is the respondent (a restaurant claiming a fraudulent refund request), swap roles: the party who opened it is the claimant.
- appeal_window_hours holds the instruction so either party can appeal (Rules: one appeal, heard by a three-model panel). Set 24–72 if you show rulings to users before executing; omit it to execute immediately.
prefer: respond-asyncreturns202with a progress URL — rulings take 20–40 seconds. Without it the ruling comes back on the response.
4 · What comes back
The record (GET /disputes/:id) carries the ruling — fault allocation, money instruction, a reasoning trace citing the doctrine rule behind each finding, confidence — and a rail_instruction that is literally the Stripe calls to make:
"rail_instruction": {
"rail": "stripe_connect", "payment_intent": "pi_3Q…", "funds": "platform_balance",
"steps": [
{ "op": "transfer", "amount_minor": 1700, "currency": "USD", "destination": "acct_1Q…" },
{ "op": "refund", "amount_minor": 2550, "currency": "USD", "payment_intent": "pi_3Q…" }
]
}
Show the ruling to both parties — the summary and reasoning are written for them. Your users' trust in the outcome is the point, and the reasoning is what earns it.
5 · Execute it
When the instruction is final (immediately, or after the appeal window passes with no appeal), Recourse POSTs the event to your webhook. Two options:
Option A — run our executor (recommended)
A small service that verifies the signature and makes the Stripe calls with your key. Recourse never sees the key. Get integrations/stripe from the repo (or ask us for the file) and run:
STRIPE_SECRET_KEY=sk_live_… ESCROW_WEBHOOK_SECRET=whsec_… STRIPE_EXECUTOR_PORT=8791 \
npx tsx integrations/stripe/execute-ruling.ts
# then set your dashboard webhook URL to https://your-host/webhooks/recourse
It is idempotent per event (safe to redeliver), holds on escalated, treats test events as a no-op, and refuses to execute an appeal instruction whose original was already executed (it asks you to reconcile instead).
Option B — your own endpoint
- Verify:
HMAC-SHA256(secret, "<recourse-timestamp>.<raw body>")equals therecourse-signatureheader; reject timestamps older than 5 minutes; de-duplicate onevent_id. - If
escalatedortest: do nothing, return 200. - For each step in
rail_instruction.steps, in order:transfer→stripe.transfers.create({ amount, currency, destination, source_transaction: <the payment's charge> });refund→stripe.refunds.create({ payment_intent, amount, reverse_transfer? });transfer_reversal→stripe.transfers.createReversal(transfer, { amount }). Use an idempotency key derived fromevent_idand the step index. - Return 2xx. Full wire format: /docs#webhooks.
6 · Escalations and appeals
- Escalated rulings (low confidence, high value, thin evidence) arrive with
escalated: true: hold. The Recourse bench reviews and a final instruction follows. - Appeals. Either party may appeal once:
POST /disputes/:id/appealwith{ "by": "respondent", "grounds": "…" }from your key. The panel re-hears the record; its determination is final and arrives as a new event markedsupersedes: "<first id>". With an appeal window set, the first instruction is simply never sent. Without one, the executor will not execute the superseding instruction on top of an executed one — settle the difference yourself.
7 · Test it in a Stripe sandbox
Point the executor at a sandbox key (sk_test_…) and run integrations/stripe/e2e-test.ts: it creates a test connected account and a held test payment, files a dispute naming them, and checks the transfer and refund landed. SCENARIO=split tells a partial-performance story so both steps run. We ran exactly this before publishing this page.
8 · How you pay Recourse
Separate from the money moving between your users: you pay for rulings. 25 free, then a card on file (Stripe, saved from your dashboard) and the month's usage — $4 per automated verdict, $20 for a bench review or an appeal, $150 for a human panel — charged on the 1st. No card after the trial, or a failed charge, and the API answers 402 with the fix until it's resolved; nothing already filed is affected. Volume agreements are invoiced under contract instead. Full billing terms.
9 · Go-live checklist
- Terms updated for both sides; a link to the Rules where users will look for it.
- Transfers delayed until disputes clear (or
transfer/destination_chargeset in the settlement spec). - Webhook URL saved, test event delivered and verified, executor running with the live key on your infrastructure.
- Decide your appeal window and whether users see the ruling before execution.
- Billing: card saved on the dashboard (required once the 25 free rulings are used); rulings meter per API key at the rate card and are charged monthly.
Create a platform accountFull API referenceTalk to us about a pilot