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 patternWhat the ruling turns intoSettlement 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 madetransfer_reversal of the buyer's share back to your balance + refundadd transfer: "tr_…"
Destination charges — the money landed on the connected account at payment timea refund with reverse_transfer: true, which pulls the buyer's share back from the connected accountadd 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

  1. Create a platform account and generate an API key on the dashboard. Keys are shown once.
  2. 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: true and test: 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…" }
  }'

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

  1. Verify: HMAC-SHA256(secret, "<recourse-timestamp>.<raw body>") equals the recourse-signature header; reject timestamps older than 5 minutes; de-duplicate on event_id.
  2. If escalated or test: do nothing, return 200.
  3. For each step in rail_instruction.steps, in order: transferstripe.transfers.create({ amount, currency, destination, source_transaction: <the payment's charge> }); refundstripe.refunds.create({ payment_intent, amount, reverse_transfer? }); transfer_reversalstripe.transfers.createReversal(transfer, { amount }). Use an idempotency key derived from event_id and the step index.
  4. Return 2xx. Full wire format: /docs#webhooks.

6 · Escalations and appeals

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

Create a platform accountFull API referenceTalk to us about a pilot