Docs
The Commitment API is the layer, and every surface Recourse ships is a client of it. Five calls run a commitment from nothing to a signed record either party can verify without trusting us.
Everything on this page is generated from the shipped SDK, the endpoints the API serves, and the Rulebook. If a sample here is wrong, the build is wrong.
This document also answers Accept: text/markdown and Accept: application/json at this URL, and is addressable directly at /repr/md/docs and /repr/json/docs.
Three ways in
An agent, a developer, and an MCP client reach the same API. Nothing below is different per client; only the first fetch is.
Fetch two files.
/llms.txt says what Recourse is and where everything else is. /llms-full.txt is this whole page, the standards, and the reason codes in one plain-text document.
Install the client.
The typed SDK maps every problem+json document to one error with a stable code, and sends an idempotency key on every call that moves money.
pnpm add @recourse/sdkRun the server.
@recourse/mcp exposes the lifecycle as tools over stdio, and holds the API key in its own environment rather than in a prompt. Configuration below.
Discovery
An agent that has never heard of Recourse can learn the whole protocol from five documents. The website serves the two text files; the API serves the two well-known documents and its own description.
curl -sS https://recourse-mu.vercel.app/llms.txt
curl -sS https://recourse-mu.vercel.app/llms-full.txt
curl -sS https://api.recourse.dev/.well-known/recourse.json
curl -sS https://api.recourse.dev/openapi.jsonThe keys document publishes the Ed25519 public keys the event log and the record snapshots are signed with. It is served by the API because the service holding the keys is the one that should answer for them. Open it.
Get a key
Parties are organizations or agents, symmetrically. An agent is a party with keys, a record, and obligations — the same record an organization gets, and the same endpoints. There is no separate agent integration to migrate to.
Credentials
Organization key, sandbox. No money moves.
Organization key, live.
Agent key, bound to an agent party with a principal organization. Same endpoints, same scopes.
Send it as Authorization: Bearer <key>. The token is returned once, at creation, and never again: only its SHA-256 is stored.
Scopes
Create, propose, accept, fund, and cancel commitments.
Submit outcomes and evidence against an active commitment.
Open a dispute, post a bond, and file evidence.
Read a party's record and export an audit bundle.
A key without the scope gets 403 insufficient_scope — never a partial result.
Mint one
curl -sS https://api.recourse.dev/v1/whoami \
-H "Authorization: Bearer $RECOURSE_API_KEY"Write the terms
Terms are a typed JSON document. Recourse canonicalizes them (RFC 8785), takes the SHA-256, and the buyer signs that hash. Neither side can change what was agreed without changing the hash. Money is integer cents, USD, never a float.
{
"kind": "outcome_stream",
"standard": {
"id": "OS-1",
"version": "0.4.0"
},
"unit_price": {
"amount_cents": 175,
"currency": "usd"
},
"budget_per_period": {
"amount_cents": 500000,
"currency": "usd"
},
"period": "monthly",
"term_periods": 1,
"evidence_source": {
"type": "connector",
"connector": "zendesk"
},
"dispute": {
"buyer_review_window_hours": 120,
"statement_dispute_window_hours": 120,
"evidence_window_hours": 48,
"appeal_window_hours": 72,
"dispute_bond": {
"pct": 2,
"min_cents": 2500,
"max_cents": 250000
},
"appeal_bond": {
"pct": 5,
"min_cents": 10000,
"max_cents": 1000000
},
"path": [
"rulebook",
"panel",
"aaa"
],
"governing_law": "US-DE"
},
"rulebook_version": "0.4.0"
}The hash above is real. Canonicalize the document beside it, take the SHA-256, and you get the same 64 characters.
The other kind is deliverable: one deliverable, acceptance criteria the vendor writes, a deadline, and a price held up front. Recourse rejects a criterion that is not testable as written. Read the standards.
The five calls
These five run a commitment from nothing to a signed, verifiable record. No other call is required. A POST that creates or moves money takes an Idempotency-Key; repeating it replays the first answer instead of creating a second commitment or a second hold.
import { createClient } from "@recourse/sdk";
// The vendor writes and publishes the terms.
const vendor = createClient({
baseUrl: "https://api.recourse.dev",
apiKey: process.env.VENDOR_API_KEY,
});
// The buyer signs the terms hash and funds the hold. A different key, because
// the API refuses a command from the wrong side of the commitment.
const buyer = createClient({
baseUrl: "https://api.recourse.dev",
apiKey: process.env.BUYER_API_KEY,
});Two clients, because a commitment has two sides and neither may act for the other. Only the vendor proposes; only the buyer accepts and funds. A command from the wrong side is refused with 403 wrong_party.
The manual rail ships first: it records the instruction and its state, and money moves between the parties on their own arrangements. Stripe, Circle, and x402 adapters follow. A commitment reports the rail it is on.
// 1 — Create. The terms above are canonicalized (RFC 8785) and hashed. This
// call creates a commitment, so the SDK sends an Idempotency-Key: a retry
// returns the same commitment instead of a second one.
const commitment = await vendor.commitments.create({
kind: "outcome_stream",
vendor_party_id: vendorPartyId,
buyer_party_id: buyerPartyId,
terms,
});
// 2 — Propose. The vendor publishes the terms and gets the link the buyer opens.
const { hosted_url, terms_hash } = await vendor.commitments.propose(commitment.id);
// 3 — Accept. The buyer signs that exact hash. A hash that is not the current
// one is refused with terms_hash_mismatch, not recorded.
await buyer.commitments.accept(commitment.id, { terms_hash });
// 4 — Fund. The hold is instructed on the rail. The commitment activates once
// the hold covers the amount the terms require.
const funded = await buyer.commitments.fund(commitment.id, {
amount: { amount_cents: 500_000, currency: "usd" },
});
funded.state; // "active"
// 5 — Read the record. The chain check and the log public key come with it, so
// the caller can verify the stream without trusting this response.
const stream = await vendor.commitments.events(commitment.id);
stream.verification; // { ok: true }
stream.log_public_key; // raw Ed25519 public key, base64Over plain HTTP
curl -sS https://api.recourse.dev/v1/commitments \
-H "Authorization: Bearer $RECOURSE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"kind": "outcome_stream",
"vendor_party_id": "pty_01K3M8QW4YB7N0R6TVXZ2FJ5AD",
"buyer_party_id": "pty_01K3M9C2H8D4S1Y7GNRT6VKQ0B",
"terms": { "...": "the terms document above" }
}'Everything else the API answers
Commitment states
Every state change is an immutable, signed event, and the tables are projections of the log. A command that is not legal from the current state is refused with 409 invalid_transition; it is never silently ignored.
The chip is the same mapping every Recourse surface uses, so a state is never one colour on a dashboard and another on a record.
Errors say what failed and what to do
Every failure is RFC 9457 problem+json. Branch on code: the sentence may be reworded, the code will not. detail says what failed, next names the call that resolves it, and request_id is the one string to quote to support.
{
"type": "https://recourse.dev/problems/invalid_transition",
"title": "Invalid transition",
"status": 409,
"code": "invalid_transition",
"detail": "command \"fund\" is not allowed from state \"draft\": the terms are not signed",
"next": "GET /v1/commitments/{id} for the current state, then send the command that state allows.",
"request_id": "req_4ed73d80-cf4e-4d20-834f-2df272041685"
}import { RecourseError } from "@recourse/sdk";
try {
await buyer.commitments.accept(id, { terms_hash });
} catch (err) {
if (!(err instanceof RecourseError)) throw err;
switch (err.code) {
case "terms_hash_mismatch": {
// The terms moved. Read the current hash and sign that one.
const current = await buyer.commitments.get(id);
if (current.current_terms_hash !== null) {
await buyer.commitments.accept(id, { terms_hash: current.current_terms_hash });
}
break;
}
case "invalid_transition": {
// The command is not legal from this state. Read the state, then act.
const current = await buyer.commitments.get(id);
handle(current.state);
break;
}
case "wrong_party":
case "insufficient_scope":
// Nothing to retry: the credential is wrong, not the request.
throw err;
default:
if (err.isRetryable) await retryWithBackoff();
else throw err;
}
}Verify a commitment yourself
Nothing on a record needs taking on faith. Every claim is checkable from published material with a SHA-256 and an Ed25519 verify, and the check needs no Recourse dependency.
Recompute the terms hash.
terms_hash = sha256(JCS(terms)), lowercase hex, rendered on the wire as sha256:<hex>. Canonicalize the terms document with RFC 8785 and hash it. It matches, or the terms you are holding are not the terms that were signed.
Recompute the standard hash.
Every Outcome Standard is a published JSON file. sha256(JCS(file)) is the hash a commitment cites. A standard that changed would change its hash, and the commitment cites the version it was accepted under.
Walk the event chain.
hash = sha256(JCS({stream_id, seq, type, payload, occurred_at, prev_hash})), where prev_hash is the bare hex of the event before it and null at seq 1. Strip the sha256: prefix the API renders before you hash. A missing or altered event breaks the chain at that seq.
Check the log signature.
Each event carries an Ed25519 signature, base64, over the utf8 bytes of its bare lowercase hex hash. The raw public key is on the response as log_public_key and in the published keys document. Verify against the published key, not the one on the response, if you want the stronger check.
import { createHash, createPublicKey, verify } from "node:crypto";
import { canonicalize } from "@recourse/crypto"; // or any RFC 8785 implementation
const sha256 = (value: unknown): string =>
createHash("sha256").update(canonicalize(value), "utf8").digest("hex");
// 1 — The terms hash the buyer signed.
sha256(terms) === commitment.terms_hash?.replace(/^sha256:/, "");
// 2 — The chain. On the wire a hash reads "sha256:<hex>"; the chain is hashed
// over the bare hex, and prev_hash is null at seq 1.
const stream = await client.commitments.events(commitment.id);
const publicKey = createPublicKey({
key: Buffer.concat([
Buffer.from("302a300506032b6570032100", "hex"), // Ed25519 SPKI prefix
Buffer.from(stream.log_public_key, "base64"),
]),
format: "der",
type: "spki",
});
let prev: string | null = null;
for (const event of stream.data) {
const hash = sha256({
stream_id: event.stream_id,
seq: event.seq,
type: event.type,
payload: event.payload,
occurred_at: event.occurred_at,
prev_hash: prev,
});
if (hash !== event.hash.replace(/^sha256:/, "")) {
throw new Error(`the chain breaks at seq ${event.seq}`);
}
// 3 — The log signature: Ed25519 over the utf8 bytes of the hex hash.
const signed = verify(
null,
Buffer.from(hash, "utf8"),
publicKey,
Buffer.from(event.signature, "base64"),
);
if (!signed) throw new Error(`the signature fails at seq ${event.seq}`);
prev = hash;
}The MCP server
An agent whose client speaks MCP can use @recourse/mcp instead of HTTP. It runs over stdio and reads the API key from its own environment, so the key never enters a prompt or a tool argument. The package is not published to a registry yet; run it from a clone of the repository.
{
"mcpServers": {
"recourse": {
"command": "pnpm",
"args": ["--filter", "@recourse/mcp", "dev"],
"env": {
"RECOURSE_API_URL": "https://api.recourse.dev",
"RECOURSE_API_KEY": "rk_live_…"
}
}
}
}The API base, e.g. https://api.recourse.dev.
An org key (rk_live_…, rk_test_…) or an agent key (ak_…).
The rules are data
The Rulebook and the Outcome Standards ship as versioned JSON. Every ruling cites a rule id and the rulebook version in force when the terms were accepted. Nothing in this documentation restates them; it links to them.
Rulebook 0.4.0
Definitions, procedure, verification order, appeals, records, and amendments, with 24 reason codes a ruling may cite.
Outcome Standards
What a unit is, what evidence proves it, and what rejects it — with the hash of each published file.
The record
What the layer has settled, in aggregate. No party is named and there is no sample data.