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.

  1. 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:
    recourse.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.000Z
    Send base64({ address, message, signature, chainId }) as the X-Sign-In-With-X header on every request. That signature is kept on the dispute record as the party's venue agreement.
  2. Terms. POST /disputes with the header and no payment returns 402 Payment Required with an x402 body: accepts[0] gives the scheme (exact), network, asset (USDC), payTo, and maxAmountRequired — the case fee for the dispute's value tier, in USDC atomic units. Prices: GET /x402/requirements?amount_minor=….
  3. Pay with the request. Sign an EIP-3009 TransferWithAuthorization for that amount to payTo, wrap it as an x402 payment payload, and retry with it base64-encoded in X-PAYMENT. The response carries X-PAYMENT-RESPONSE with the settlement (transaction hash, or sandbox: true when 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) and GET /x402/balance/:address; fees are drawn from it automatically.
  4. Don't hold the socket. Rulings take tens of seconds. Send Prefer: respond-async (or ?async=1) to get 202 { id, progress, result }; poll GET /disputes/:id/progress for the live milestones and fetch GET /disputes/:id when it reports done.

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

FieldTypeRequiredMeaning
rulepackstring✔Doctrine to adjudicate under. Currently marketplace-contractor-v1 (see GET /rulepacks).
currencystring✔ISO 4217, e.g. USD.
amount_minorinteger✔Disputed amount in minor units (cents). The ruling's money instruction always sums exactly to this.
termsobject✔{ 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.
claimobject✔{ by: "claimant" | "respondent", claimed_breach: string, remedy_sought?: string }.
logarray✔Chronological events: { at: ISO8601, actor: "claimant"|"respondent"|"platform"|"system", kind: "message"|"delivery"|"payment"|"revision_request"|"status_change"|"other", content: string }.
deliverablesarray✔{ name, description?, content?, url? } — inline text for textual work; URLs for binary artifacts (URL-only evidence may trigger escalation).
settlementobjectWhich rail holds the disputed funds — { rail: "escrow_webhook" } (default), stripe_connect, tempo, or x402. See Settlement rails.
external_refstringYour reference for the transaction; echoed on webhooks.
metadataobjectFree-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)

FieldTypeMeaning
contextobjectThe counterparty's legal-context.json as fetched at transaction time. Either this or context_url.
context_urlhttps URLWhere to fetch it now. The venue fetches over https only, public addresses only, 1 MB cap.
documentstringThe 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_hash0x-hexThe 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.
acceptanceobjectLevel 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

RailFunds held byExecution
recourse_escrowRecourse 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 accountsanyone submits the venue's signature; the contract pays the proportional split, with appeal rounds. Protocol docs · Live
kleros_escrowKleros's escrow contract (unmodified) with Recourse as its ERC-792 arbitrator — for funds already in a Kleros escrowthe winning agent submits the venue's signature; the escrow pays out (binary)
stripe_connectthe platform's Stripe account — funds held on the balance, already transferred, or a destination chargeStripe Connect guide: drop-in executor or your own endpoint; transfer / refund / transfer_reversal with the platform's own key, idempotent per event
escrowcomEscrow.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
x402an x402 facilitator holding an agent's payment authorizationfacilitator settles the splits (case fees to the venue are live)
tempoa custodial holder on Tempotransfer intents with the dispute id in the memo; for non-custodial settlement on Tempo use kleros_escrow there
escrow_webhookfiat 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.

RailFunds held byInstruction shapeStatus
escrow_webhook (default)Fiat escrow partner (Trustap/Tazapay/Shieldpay class)Direct split against held fundsLive vs reference receiver
stripe_connectPlatform's Stripe account (delayed transfer / separate charge)transfer + refund steps your Stripe integration executes verbatimSpec-stable
tempoHolding contract / wallet on Tempo (Stripe's payments L1)Stablecoin transfer_intents with the dispute id in the memoSpec-stable
x402x402 facilitator holding the agent-payment authorizationsettle.splits the facilitator executes — the dispute layer the x402 docs tell developers to buildSpec-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)

  1. Free trial: every account starts with 25 free rulings, any tier, no card. The playground needs no account at all.
  2. 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.
  3. 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.
  4. 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).