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

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).

POST /disputes

Submit a dispute bundle for adjudication. Synchronous: the ruling returns on the response (SLA: minutes on clean bundles).

Request body

FieldTypeRequiredMeaning
rulepackstringDoctrine to adjudicate under. Currently marketplace-contractor-v1 (see GET /rulepacks).
currencystringISO 4217, e.g. USD.
amount_minorintegerDisputed amount in minor units (cents). The ruling's money instruction always sums exactly to this.
termsobject{ format: "text" | "lcp", content: string } — the agreed terms.
claimobject{ by: "claimant" | "respondent", claimed_breach: string, remedy_sought?: string }.
logarrayChronological 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).

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.

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

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_…" } ] } }

Billing

Free trial: every account starts with 25 free rulings, any tier — no card required. Metered usage begins only when the credits are exhausted. The playground needs no account at all.

Pay as you go (default): usage is metered per ruling at the published rate card — $4.00 Tier 1, $20.00 Tier 2, $150.00 Tier 3 — no minimum, no commitment, invoiced monthly from your dashboard. Pay by card via Stripe Checkout, or in USDC via Bridge settling on Tempo.

Volume & enterprise: annual commitments with reserved capacity price Tier 1 at $1–2, and add liability-backed rulings, DSA/ODS readiness, and shadow-pilot onboarding — talk to us.

The platform pays; disputants never do; the fee is never a percentage of the disputed value.