API documentation
Base URL: https://recourse.so · All requests and responses are JSON. Authenticate with an API key from your dashboard: Authorization: Bearer rk_live_…
Authentication
API keys (platforms). Create keys in the dashboard. Keys are shown once at creation and stored hashed. Revoke any key instantly from the dashboard. Requests without a valid key are rejected with 401 when key enforcement is on (RECOURSE_REQUIRE_KEYS=1; development mode is open).
Wallet identity & x402 — for agents, no account needed
An autonomous agent has no email and shouldn't need a human to sign up for it. On Recourse its wallet is its identity, its signature is its consent to the Rules, and it pays the case fee with the request. Nothing else is required.
- Identity + consent. Sign an EIP-4361 Sign-In-With-Ethereum message with the wallet (Base, chain id 8453; fresh nonce; issued within the last 5 minutes; single use). Its statement must be the venue's consent text — fetch it from
GET /x402/requirements:
Sendrecourse.so wants you to sign in with your Ethereum account: 0xYourAgentWallet… I agree that any dispute I submit is determined by Recourse under the Recourse Standard Rules v1.0 (https://recourse.so/rules/v1.0), final and binding as a matter of contract. URI: https://recourse.so/disputes Version: 1 Chain ID: 8453 Nonce: 6f1c9a2b3d4e5f60 Issued At: 2026-09-04T09:00:00.000Zbase64({ address, message, signature, chainId })as theX-Sign-In-With-Xheader on every request. That signature is kept on the dispute record as the party's venue agreement. - Terms.
POST /disputeswith the header and no payment returns402 Payment Requiredwith an x402 body:accepts[0]gives the scheme (exact), network, asset (USDC),payTo, andmaxAmountRequired— the case fee for the dispute's value tier, in USDC atomic units. Prices:GET /x402/requirements?amount_minor=…. - Pay with the request. Sign an EIP-3009
TransferWithAuthorizationfor that amount topayTo, wrap it as an x402 payment payload, and retry with it base64-encoded inX-PAYMENT. The response carriesX-PAYMENT-RESPONSEwith the settlement (transaction hash, orsandbox: truewhen the venue has no facilitator configured — nothing moves, the record still issues). Alternatively keep a prepaid balance:POST /x402/top-up(same 402 dance, minimum $5) andGET /x402/balance/:address; fees are drawn from it automatically. - Don't hold the socket. Rulings take tens of seconds. Send
Prefer: respond-async(or?async=1) to get202 { id, progress, result }; pollGET /disputes/:id/progressfor the live milestones and fetchGET /disputes/:idwhen it reportsdone.
Both parties should have signed the underlying agreement by wallet (the demo's agreement object shows the shape); the respondent's consent to the venue is the clause in that agreement, the claimant's is the signature above. See it end to end at /agents/demo — the agent there is a real wallet doing exactly this against this API.
POST /disputes
Submit a dispute bundle for adjudication. Synchronous by default: the ruling returns on the response (typically 20–40 seconds). Add Prefer: respond-async for 202 + a progress URL. Wallet-authenticated requests are charged the case fee (see Wallet identity & x402). Optional appeal_window_hours (0–336) holds enforcement so either party can appeal; POST /disputes/:id/appeal with { by, grounds } files the one appeal the Rules provide (panel, final). Enforcement events go to the webhook set on your dashboard. Platforms on Stripe: the integration guide. Agents with funds on chain: the Recourse Protocol.
Request body
| Field | Type | Required | Meaning |
|---|---|---|---|
rulepack | string | ✔ | Doctrine to adjudicate under. Currently marketplace-contractor-v1 (see GET /rulepacks). |
currency | string | ✔ | ISO 4217, e.g. USD. |
amount_minor | integer | ✔ | Disputed amount in minor units (cents). The ruling's money instruction always sums exactly to this. |
terms | object | ✔ | { format: "text" | "lcp", content: string } — the agreed terms. text: the terms as prose. lcp: a Legal Context Protocol record — the venue resolves and verifies the terms and records the result on the case. |
claim | object | ✔ | { by: "claimant" | "respondent", claimed_breach: string, remedy_sought?: string }. |
log | array | ✔ | Chronological events: { at: ISO8601, actor: "claimant"|"respondent"|"platform"|"system", kind: "message"|"delivery"|"payment"|"revision_request"|"status_change"|"other", content: string }. |
deliverables | array | ✔ | { name, description?, content?, url? } — inline text for textual work; URLs for binary artifacts (URL-only evidence may trigger escalation). |
settlement | object | Which rail holds the disputed funds — { rail: "escrow_webhook" } (default), stripe_connect, tempo, or x402. See Settlement rails. | |
external_ref | string | Your reference for the transaction; echoed on webhooks. | |
metadata | object | Free-form strings; never used in adjudication. |
Example
curl https://recourse.so/disputes \
-H "authorization: Bearer rk_live_..." \
-H "content-type: application/json" \
-d '{
"rulepack": "marketplace-contractor-v1",
"currency": "USD",
"amount_minor": 60000,
"terms": { "format": "text", "content": "Logo package, one revision round, 10 days, $600." },
"claim": { "by": "claimant", "claimed_breach": "Work is not professional quality." },
"log": [
{ "at": "2026-06-12T16:30:00Z", "actor": "respondent", "kind": "delivery", "content": "Drafts delivered per spec." },
{ "at": "2026-06-13T09:00:00Z", "actor": "claimant", "kind": "message", "content": "Not what I imagined. Refund me." }
],
"deliverables": [{ "name": "logo-package", "description": "SVG/AI/PNG per spec" }]
}'
Response 201
{
"id": "dsp_4b81e2...",
"status": "adjudicated", // or "escalated"
"ruling": {
"fault_allocation": { "claimant_pct": 75, "respondent_pct": 25 },
"money_instruction": { "type": "split", "to_respondent_minor": 45000, "to_claimant_minor": 15000, "currency": "USD" },
"summary": "…plain-language ruling…",
"reasoning": [ { "rule_id": "MC-2", "finding": "…", "application": "…" } ],
"confidence": 0.84,
"escalate": false,
"tier": 1,
"engine": "recourse-t1/claude-opus-5"
}
}
Errors: 400 invalid bundle (message names the field) · 401 bad key · 502 adjudication failed (recorded; retry or escalate).
LCP terms — terms.format: "lcp"
When the transaction was made under the Legal Context Protocol, send the record the agent kept instead of pasting the terms. Before anything is charged or filed the venue resolves the terms document, checks it against the atrHash, verifies the acceptance signature, resolves the dispute-resolution clause, and stores the verification on the case as bundle.terms.lcp. The engine judges under the resolved document, with the verification stated above the terms; a hash mismatch or a failed signature is on the face of the determination, never silently accepted.
terms.content — an LCP terms record (JSON string)
| Field | Type | Meaning |
|---|---|---|
context | object | The counterparty's legal-context.json as fetched at transaction time. Either this or context_url. |
context_url | https URL | Where to fetch it now. The venue fetches over https only, public addresses only, 1 MB cap. |
document | string | The terms document bytes the agent saved. Strongly recommended: the merchant may have changed terms since, and a document in hand is what LCP says the proof lives in. Fetched from context.terms when absent. |
receipt_atr_hash | 0x-hex | The atrHash quoted in the transaction receipt. Authoritative for that transaction (LCP §12.3): the document is checked against it, and a differing published hash is noted. |
acceptance | object | Level 3 record: { scheme: "eip191" | "eip712", signer, signature, message? | typed_data?, signed_at?, party? }. eip191 = personal_sign over the atrHash string (or any message containing it); eip712 = typed data whose message carries the atrHash. Verified with the signer recovered from the signature. |
content may also be the bare https URL of a legal-context.json, or the discovery document itself.
What comes back on the record — bundle.terms.lcp
{
"claimed_level": 4, "verified_level": 4,
"terms_url": "https://data.northwind.example/terms/agent-terms-v3.md",
"document_sha256": "0x…", "atr_hash": "0x…", "terms_hash_verified": true,
"acceptance_required": true,
"acceptance": { "verified": true, "scheme": "eip191", "signer": "0x…", "party": "claimant", "bound_to": "0x…" },
"dispute_resolution": { "present": true, "names_recourse": true, "clause_id": "sha256:0x…",
"clause": { "jurisdiction": "other", "ceiling_gbp": 0, "appeal": true, "human_escalation": true, "rules_version": "1.0" },
"source_verified": true, "catalog_is_recourse": true },
"warnings": [], "verified_at": "…"
}
Errors: 400 malformed record or invalid legal-context.json (schema errors listed) · 409 outside the clause's value ceiling (Rule 5 — refer by post-dispute agreement instead) · 502 the legal context or terms could not be retrieved (send document inline). A record that verifies at a lower level than it claims is not an error — the shortfall is recorded and shown to the engine. Offline-verifiable example: fixtures/disputes/006-lcp-agent-purchase.json. Everything Recourse publishes under LCP, the clause generator and the domain checker: /lcp.
GET /disputes/:id
Retrieve a stored dispute and its ruling. Escalated disputes return the Tier 1 draft until the Recourse bench issues the final ruling, which replaces it.
GET /disputes
List disputes (id, status, summary, escalated flag), newest first.
GET /rulepacks
The published doctrine, machine-readable: rules, silence-in-terms defaults, and escalation triggers for each vertical rule-pack. The human-readable form is at /doctrine.
GET /health
Liveness + engine mode (claude or mock).
Escalation — who judges what
When a bundle trips an escalation trigger (published per rule-pack: low engine confidence, amount above ceiling, contradictory or uninspectable evidence, fraud allegations), the response comes back status: "escalated" with the engine's draft ruling attached, and the case enters the Recourse Review Bench — our internal queue, staffed by Recourse.
- Tier 2 (enhanced review): a Recourse reviewer examines the bundle with a deeper engine pass and confirms or overrides the draft.
- Tier 3 (human panel): independent adjudicators contracted by Recourse — never your staff, never ours with a conflict. Panelists are conflict-screened per case and excluded where any relationship with a party or the platform exists.
Your team is never asked to judge. Your only integration point is the escrow webhook: the draft instruction arrives flagged escalated: true (your escrow partner holds funds); the bench's final ruling follows as a superseding instruction with escalated: false.
Settlement rails — enforcement
One guide per integration: /integrations.
Who executes what
| Rail | Funds held by | Execution |
|---|---|---|
recourse_escrow | Recourse Protocol escrow on Base, Base Sepolia or Tempo (Stripe's L1; chain 4217 / testnet 42431); agents commit through the ERC-7579 module on Safe and other smart accounts | anyone submits the venue's signature; the contract pays the proportional split, with appeal rounds. Protocol docs · Live |
kleros_escrow | Kleros's escrow contract (unmodified) with Recourse as its ERC-792 arbitrator — for funds already in a Kleros escrow | the winning agent submits the venue's signature; the escrow pays out (binary) |
stripe_connect | the platform's Stripe account — funds held on the balance, already transferred, or a destination charge | Stripe Connect guide: drop-in executor or your own endpoint; transfer / refund / transfer_reversal with the platform's own key, idempotent per event |
escrowcom | Escrow.com (sandbox or production) | an approved partner performs the parties' actions As-Customer — release = buyer accept; refund = the return flow; milestone items approximate a split. Executor integrations/escrowcom |
x402 | an x402 facilitator holding an agent's payment authorization | facilitator settles the splits (case fees to the venue are live) |
tempo | a custodial holder on Tempo | transfer intents with the dispute id in the memo; for non-custodial settlement on Tempo use kleros_escrow there |
escrow_webhook | fiat escrow partner (Trustap/Tazapay class) | signed webhook; reference receiver integrations/escrow |
Every executor honours escalated: true by holding, and every instruction is on the dispute record as rail_instruction. Recourse decides; it never custodies. Spec: integrations/escrow/WEBHOOK-SPEC.md.
recourse_escrow — the Recourse Protocol
For funds committed to a RecourseEscrow transaction. Submit the bundle with settlement: { rail: "recourse_escrow", chain_id, escrow, transaction_id, amount, token_decimals } after the transaction has been disputed on chain. The determination is signed by the venue's settlement key (EIP-712, domain RecourseEscrow/1 on the escrow) over (transactionId, payeeAmount, round, notBefore, recourseDisputeId) and returned on the record as rail_instruction; anyone submits it and the contract pays the split as ruled. An appeal is round 2 and supersedes an unexecuted round 1; appeal_window_hours becomes notBefore. Full guide: Recourse Protocol.
kleros_escrow — Kleros escrow, Recourse as arbitrator
For funds held in Kleros's EscrowUniversal (unmodified) with RecourseArbitrator named as its ERC-792 arbitrator. Submit the bundle with settlement: { rail: "kleros_escrow", chain_id, escrow, arbitrator, transaction_id, dispute_id } after the escrow has opened the dispute. The determination is signed by the venue's settlement key (EIP-712, domain RecourseArbitrator/1 on the arbitrator) and the signed instruction is returned on the record as rail_instruction:
{ "rail": "kleros_escrow", "dispute_id": "7", "ruling": 1,
"mapping": "…the side the determination favours takes the escrowed amount…",
"recourse_dispute_id": "dsp_…", "signature": "0x…",
"execute": "RecourseArbitrator(0x…).executeSignedRuling(7, 1, keccak256("dsp_…"), signature)" }
Anyone may execute it — normally the winning agent — and the escrow pays out; Recourse never holds funds and pays no gas. Kleros rulings are binary (1 buyer/claimant refunded, 2 seller/respondent paid): the side the money instruction favours wins, and the full proportional determination stays on the record. Escalated determinations carry no signature — the escrow stays frozen until the bench finalises. Send the execution with an explicit gas limit (≈600k): the escrow swallows a failed token transfer rather than reverting, so gas estimation converges on a value at which the payout starves. Contracts, tests and deploy script: contracts/ in the repo. New integrations should prefer recourse_escrow, which executes the proportional split as ruled.
Judgment is rail-agnostic; enforcement is an adapter. The bundle's settlement field names the rail holding the disputed funds, and the ruling's webhook carries a rail-specific rail_instruction ready to execute. Recourse never holds funds, keys, or balances on any rail.
| Rail | Funds held by | Instruction shape | Status |
|---|---|---|---|
escrow_webhook (default) | Fiat escrow partner (Trustap/Tazapay/Shieldpay class) | Direct split against held funds | Live vs reference receiver |
stripe_connect | Platform's Stripe account (delayed transfer / separate charge) | transfer + refund steps your Stripe integration executes verbatim | Spec-stable |
tempo | Holding contract / wallet on Tempo (Stripe's payments L1) | Stablecoin transfer_intents with the dispute id in the memo | Spec-stable |
x402 | x402 facilitator holding the agent-payment authorization | settle.splits the facilitator executes — the dispute layer the x402 docs tell developers to build | Spec-stable |
"Spec-stable" = payload formats are frozen and exercised end-to-end against the reference receiver; live execution activates with partner onboarding. Name Recourse as resolver in your escrow contract, Connect flow, or x402 payment terms once — every transaction inherits the venue.
POST /witness/inbound
The email-witness ingest (early access — see the case study). Your email provider POSTs each message routed to witness@ / dispute@ here; the venue tracks the thread lifecycle (requested → witnessed → disputed), assembles the dispute bundle from the correspondence, and queues the ruling reply. Protect with the x-witness-secret header (WITNESS_INBOUND_SECRET).
{ "message_id": "<abc@mail.example>", "in_reply_to": "<...>", "references": ["<...>"],
"from": "Dana <dana@client.example>", "to": ["leo@studio.example", "dispute@recourse.so"],
"cc": ["witness@recourse.so"], "subject": "Re: Brand refresh — scope & quote",
"date": "2026-07-30T09:00:00Z", "text": "Filing a dispute...\n\nAmount: 700 EUR",
"attachments": [{ "filename": "logo-package.zip" }] }
A dispute email with no Amount: line yields a declaratory ruling. Outbound replies queue in the venue outbox and deliver via WITNESS_OUTBOUND_WEBHOOK (ESP-agnostic, same pattern as escrow enforcement).
Webhooks — wire format
One event type: ruling.money_instruction, HMAC-SHA256 signed, at-least-once delivery. Full spec with per-rail payloads and verification reference: integrations/escrow/WEBHOOK-SPEC.md in the integration kit.
POST {your registered endpoint}
recourse-timestamp: 1754460000
recourse-signature: hex(hmac_sha256(secret, timestamp + "." + body))
{ "type": "ruling.money_instruction", "dispute_id": "dsp_…", "escalated": false,
"instruction": { "type": "split", "to_respondent_minor": 45000, "to_claimant_minor": 15000, "currency": "USD" },
"rail_instruction": { "rail": "tempo", "network": "tempo-testnet", "transfer_intents": [
{ "to": "0xPROVIDER", "amount_minor": 45000, "asset": "USDC", "memo": "recourse:dsp_…" },
{ "to": "0xBUYER", "amount_minor": 15000, "asset": "USDC", "memo": "recourse:dsp_…" } ] } }
Paying for the API
Platforms (API key)
- Free trial: every account starts with 25 free rulings, any tier, no card. The playground needs no account at all.
- Then a card on file is required. Save one on the dashboard (Stripe; the card is held by Stripe, not by us). Until you do, requests answer
402 { code: "payment_method_required" }with a link to fix it. - Usage is charged monthly. Rulings meter at the rate card — $4.00 Tier 1 (automated), $20.00 Tier 2 (bench review; appeals meter here), $150.00 Tier 3 (human panel) — and the previous month's invoice is charged to the saved card on the 1st. No minimum, no commitment.
- If a charge fails the invoice goes past due and requests answer
402 { code: "past_due" }until it is paid from the dashboard (saved card retry or Checkout). Nothing you already filed is affected.
Volume & enterprise: annual commitments with reserved capacity price Tier 1 at $1–2, add liability-backed rulings, DSA/ODS readiness and shadow-pilot onboarding, and are invoiced under contract — no card, never gated. Talk to us.
AI agents (wallet identity)
No account, no card. An agent pays the case fee with the request over x402 (USDC on Base), or keeps a prepaid balance topped up the same way; the fee is by dispute value (see GET /x402/requirements). With no payment and no balance the request answers 402 carrying the exact payment terms — nothing is filed and nothing is owed. Details: wallet identity & x402.
The platform or the filing agent pays; the other party never does; the fee is never a percentage of the disputed value. Disputes opened by people in the dispute room follow claimant pricing (free during the MVP).