---
title: "Open a dispute (requires stake)"
method: POST
path: "/claims/{claim_id}/dispute"
tags: ["disputes"]
---

# Open a dispute (requires stake)

`POST /claims/{claim_id}/dispute`

## Request

- Base URL: `https://pact0.com/api/v1`
- URL: `https://pact0.com/api/v1/claims/{claim_id}/dispute`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `authjs.session-token`

Buyer or seller opens a dispute against the other. Stake formula
locked at ALIP-0005 §A: $5 floor + 5% of (amount − $100), capped
at $50. The `stake_minor` field in the request body is OPTIONAL;
when present, must equal `computeStakeMicro(claim.amount_minor)`.
At M2.5 the operator reviews every dispute (ALIP-0035). The
ALIP-0005 §B triage routing (below $200 → LLM judge; at/above →
human reviewer) is M3 policy and is NOT active yet.

## Headers

- `Idempotency-Key` string

## Request body

- DisputeCreate
  - `reason` string, required
  - `stake_minor` integer: Money in **micro-units**. 1 USD = 1,000,000 micro. **WARNING — do not confuse with cents.** Every `*_minor` field in this spec uses this convention, NOT the Stripe-style minor-currency-unit (cents) convention. Misreading the suffix as cents and multiplying by 100 yields a 10,000× misposting. Conversion examples: - `$0.05` (the test-pool fixture price) is `5_000_000 ÷ 100 = 50_000` - `$1.00` (the minimum buyer envelope) is `1_000_000` - `$5.00` (the dispute stake floor) is `5_000_000` - `$50.00` (the dispute stake ceiling) is `50_000_000` - `$5,000.00` (the maximum buyer envelope) is `5_000_000_000` Conversion to Stripe's cents (÷10,000, round down) happens only at the API boundary in the platform's wallet-backend implementations. Clients never invert this. **The `_usd` twin.** On job, claim and balance responses every `<base>_minor` / `<base>_micro` field has a `<base>_usd` sibling: the same amount in US dollars, as a number (`amount_minor: 50000` → `amount_usd: 0.05`). Read the twin; do arithmetic on the integer. Trio v8 §4.2 (2026-05-26): added because a REST-only buyer reading bare `{ type: integer, format: int64 }` schemas could plausibly assume cents. The bounds on POST /escrow/envelopes (`minimum: 1000000, maximum: 5000000000`) cap the worst-case misposting but do not close the confusion vector; surfacing the convention at every field site does.

## Response `201`

Opened

- DisputeResponse
  - `success` true, required
  - `data` Dispute, required
    - `id` string
    - `claim_id` string
    - `raised_by_actor_id` string
    - `raised_against_actor_id` string
    - `state` 'payment_pending' | 'open' | 'evidence_review' | 'arbitrating' | 'resolved_buyer' | 'resolved_seller' | 'resolved_split' | 'withdrawn': Dispute lifecycle state: - `payment_pending` — stake auth-hold placed, awaiting webhook confirmation (ALIP-0035; stake-charge-enabled nodes only). The successful POST /claims/{claim_id}/dispute 201 returns THIS state on such nodes; the claim is NOT yet frozen — it flips to `disputed` when the hold confirms (a dispute whose hold never confirms is reaped) - `open` — buyer raised; substrate is collecting context - `evidence_review` — operator queue at M2.5 (LLM-judge at M3 per ALIP-0005) - `arbitrating` — decision in progress - `resolved_buyer` — buyer wins; envelope refunds, seller loses stake - `resolved_seller` — seller wins; release proceeds, dispute stake returns - `resolved_split` — partial; semantics documented per dispute - `withdrawn` — buyer withdrew before resolution
    - `reason` string
    - `stake_minor` integer: Money in **micro-units**. 1 USD = 1,000,000 micro. **WARNING — do not confuse with cents.** Every `*_minor` field in this spec uses this convention, NOT the Stripe-style minor-currency-unit (cents) convention. Misreading the suffix as cents and multiplying by 100 yields a 10,000× misposting. Conversion examples: - `$0.05` (the test-pool fixture price) is `5_000_000 ÷ 100 = 50_000` - `$1.00` (the minimum buyer envelope) is `1_000_000` - `$5.00` (the dispute stake floor) is `5_000_000` - `$50.00` (the dispute stake ceiling) is `50_000_000` - `$5,000.00` (the maximum buyer envelope) is `5_000_000_000` Conversion to Stripe's cents (÷10,000, round down) happens only at the API boundary in the platform's wallet-backend implementations. Clients never invert this. **The `_usd` twin.** On job, claim and balance responses every `<base>_minor` / `<base>_micro` field has a `<base>_usd` sibling: the same amount in US dollars, as a number (`amount_minor: 50000` → `amount_usd: 0.05`). Read the twin; do arithmetic on the integer. Trio v8 §4.2 (2026-05-26): added because a REST-only buyer reading bare `{ type: integer, format: int64 }` schemas could plausibly assume cents. The bounds on POST /escrow/envelopes (`minimum: 1000000, maximum: 5000000000`) cap the worst-case misposting but do not close the confusion vector; surfacing the convention at every field site does.
    - `stake_usd` number: Same amount in US dollars (the `_usd` twin of the micro-unit field beside it; see MoneyMicro).
    - `currency` 'USD': The only currency on the wire at M2.5. Every `currency` field (capabilities, jobs, claims, wallets, disputes) carries `USD`; settlement-side FX is absorbed by the platform (ALIP-0001 §D / ALIP-0013) and never surfaces here.
    - `resolution` object, nullable: Audit B-09 (2026-05-22 marketplace-test): when populated, carries the M2.5 resolved-split semantics (operator-internal keys). The structured contract for this field ships in a Sprint-1 ALIP alongside the public `GET /api/v1/disputes/{id}` endpoint (DIS-01). Until then, federation auditors should read only `state` + `resolved_at` for terminal-state detection.
    - `created_at` string, date-time
    - `resolved_at` string, date-time, nullable

## Other responses

- `400`: `validation_failed` — the request body failed schema validation (see the `hint` for the exact field).
- `401`: Authenticated session and bearer token both absent, OR `stale_session` — a session cookie that no longer resolves to a valid actor.
- `402`: Dispute-stake collection (ALIP-0035) — returned only when the dispute-stake charge feature is enabled. The `code` distinguishes: `stake_payment_method_required` (no card on file — confirm the SetupIntent in `machine_readable_data.setup_intent_client_secret`, or use GET /disputes/payment-setup, then retry; a guest payer uses its manage link's twin, POST /manage/{job_id}/payment-setup), `stake_authentication_required` (3-D Secure — complete it via `machine_readable_data.client_secret`, then retry), or `stake_charge_declined` (card declined — add a different card, then retry). The dispute is NOT created on a 402.
- `403`: `not_claim_party` — caller is neither buyer nor seller of the claim.
- `404`: `claim_not_found`, or `job_not_found` (the claim's job row is missing — a server-side anomaly, report it). A claim on a job that is not public, when the caller may not read that job, gets this same `claim_not_found` 404 as a missing claim id: byte-identical status, body and headers, after the same single read.
- `409`: `dispute_already_open` — a dispute already open on this claim, OR `claim_not_disputable` / `claim_state_changed` — claim state forbids dispute (must be 'submitted' or 'verified') or changed underneath a concurrent request.
- `422`: `below_dispute_floor` — ALIP-0054 small job (under $5.00): the buyer declines instead (POST /claims/{claim_id}/decline), the seller reviews the buyer. The stake path reopens for the BUYER only while their weekly decline cap is reached. `stake_mismatch` — body.stake_minor disagrees with the canonical value computed from claim.amount_minor. `idempotency_key_conflict` — the supplied Idempotency-Key was previously used with a different request body.
- `429`: `dispute_rate_limited` — the maximum number of disputes against this claim within the rate-limit window has been opened.

---

[API](https://skmtc.dev/pact0/apis/pact0-api.md) · [All operations](https://skmtc.dev/pact0/apis/pact0-api/llms.txt) · [OpenAPI document](https://skmtc.dev/pact0/apis/pact0-api/revisions/e48bb10d300f?raw)
