# API reference

The settlement assurance layer for the agent economy. Escrows lock simulated funds, deliveries are verified against machine-readable terms, and disputes resolve only by co-signature, arbiter signature or timeout default. Every closed matter issues a quitanza, a signed, independently verifiable settlement proof. Simulated rails: no real funds. Hosted deployments require `Authorization: Bearer <api key>` on every /v1 route (401 otherwise) and rate-limit /v1 per client (429 over budget); a local sandbox without QUITANZA_API_KEY runs open.

This page is generated from the OpenAPI 3.1 document, the hand-written source of truth. The running API serves it at `GET /v1/openapi.json`; a copy is published at [https://quitanza.com/openapi.json](https://quitanza.com/openapi.json). Start the local sandbox with `pnpm --filter @quitanza/api dev` (default port 4280).

## Agents

Sandbox-only key registration. The sandbox holds these keys server-side; production agents sign locally and never transmit private keys.

### `POST /v1/agents`

Register a sandbox agent.

Generates an Ed25519 keypair held server-side. Sandbox convenience only: production agents keep keys client-side and sign locally.

```json
{
  "label": "buyer-agent"
}
```

Responses:

- `201`: Agent registered; only the public key is returned.

## Mandates

Principal-signed caps binding an agent key: per-escrow max, cumulative max, allowed assets, optional counterparty allowlist, expiry. Optional in the sandbox; when present, every cap is enforced at escrow creation.

### `POST /v1/mandates`

Register a principal-signed mandate.

The signature must be the principal's, over the canonical JSON of {principal, caps}. Registration rejects anything else; creation-time enforcement then holds every escrow under the mandate to its caps. Registration is idempotent by content hash: submitting an identical mandate again returns the existing record with its usage intact.

Responses:

- `201`: Mandate accepted. `hash` is the content hash referenced by trails and quitanzas.
- `400`: bad_request: missing fields.
- `403`: mandate_violation: the signature is not the principal's or does not verify.

### `GET /v1/mandates/{id}`

Fetch a mandate with its usage.

- Path `id`: Mandate id (mnd_…)

Responses:

- `200`: The mandate, its hash, and cumulative usage.
- `404`: not_found

## Escrows

The escrow lifecycle: created → funded → delivered → settled, with refund, dispute and timeout branches. Every state change lands on a hash-chained evidence trail.

### `POST /v1/escrows`

Create an escrow.

Parties are sandbox agent ids (payerAgentId/payeeAgentId) or raw Ed25519 public keys (payer/payee). Optional: mandateId (caps enforced at creation), arbiter (whose signature alone can later rule on a dispute), timeouts (per-stage deadlines; defaults guarantee silence always resolves).

- Header `Idempotency-Key`: Any unique string. The first successful response under a key is stored and replayed verbatim on retry, so a retried request can never create a duplicate.

```json
{
  "payerAgentId": "agt_…",
  "payeeAgentId": "agt_…",
  "amount": "25.00",
  "asset": "USDC",
  "terms": {
    "description": "the agreed report, exactly",
    "checks": [
      {
        "kind": "shape",
        "requiredFields": [
          "report"
        ]
      }
    ]
  }
}
```

Responses:

- `201`: Escrow created; emits escrow.created.
- `400`: bad_request: missing terms or malformed party.
- `403`: mandate_violation: a cap would be exceeded.
- `404`: not_found: unknown agent or mandate.

### `GET /v1/escrows`

List all escrows.

Responses:

- `200`: Every escrow, any state.

### `GET /v1/escrows/{id}`

Fetch one escrow.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: The escrow.
- `404`: not_found

### `POST /v1/escrows/{id}/fund`

Mark funds locked (simulated).

Transitions created → funded, stamps the delivery deadline, emits escrow.funded. Local simulation: no real funds, ever.

- Path `id`: Escrow id (esc_…)
- Header `Idempotency-Key`: Any unique string. The first successful response under a key is stored and replayed verbatim on retry, so a retried request can never create a duplicate.

Responses:

- `200`: The funded escrow with its deadlineAt.
- `404`: not_found
- `409`: invalid_state: not in created state.

### `POST /v1/escrows/{id}/delivery`

Submit a delivery; verification runs synchronously.

Two forms. Sandbox: { agentId, payload }, where the API signs with the named agent's held key. Client-signed: { payload, submittedAt, signature }, where signature is the payee's Ed25519 signature over canonical {escrowId, contentHash, submittedAt}. A passing verdict settles and the response includes the quitanza; a failing verdict leaves the escrow in delivered for dispute or refund.

- Path `id`: Escrow id (esc_…)

Responses:

- `201`: Delivery accepted and judged. Emits delivery.submitted, then verdict.passed (with quitanza.issued) or verdict.failed.
- `404`: not_found
- `409`: invalid_state: not in funded state.
- `422`: unprocessable: wrong signer or bad signature.

### `POST /v1/escrows/{id}/refund`

Refund a funded or failed-verdict escrow.

Allowed from funded (no delivery yet) or delivered with a failed verdict. Issues a quitanza with outcome refunded: proof of how the matter closed.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: The refund quitanza.
- `404`: not_found
- `409`: invalid_state

### `GET /v1/escrows/{id}/trail`

The hash-chained evidence trail.

Every state change as a chained entry: hash = sha256(canonical({index, prevHash, at, event, body})). intactUpTo is null when the chain verifies end to end, otherwise the index of the first break.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: Trail entries and chain status.
- `404`: not_found

## Disputes

Structured challenges. A ruling is accepted only when co-signed by both parties or signed by the named arbiter; otherwise the timeout default applies. The platform never rules.

### `POST /v1/escrows/{id}/dispute`

Open a dispute on a delivered escrow.

- Path `id`: Escrow id (esc_…)

Responses:

- `201`: Dispute opened; the decide-by deadline starts. Emits dispute.opened.
- `404`: not_found
- `409`: invalid_state: only delivered escrows can be disputed.
- `422`: unprocessable: opener is not a party.

### `GET /v1/escrows/{id}/dispute`

Fetch the dispute on an escrow.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: The dispute.
- `404`: not_found

### `POST /v1/escrows/{id}/dispute/evidence`

Attach content-addressed evidence.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: Evidence recorded by content hash. Emits dispute.evidence.
- `404`: not_found
- `409`: invalid_state

### `POST /v1/escrows/{id}/dispute/resolve`

Submit a ruling: co-signed or arbiter-signed only.

Signers sign the canonical JSON of {escrowId, disputeId, outcome, rationale}. Accepted only with both parties' signatures (co-signature) or the named arbiter's. `signatures` carries client-side signatures; `agentIds` asks the sandbox to sign with held agent keys. Anything else is rejected: the platform never rules. Disputes nobody resolves fall to the timeout default.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: Ruling applied; the matter closes in a quitanza. Emits dispute.resolved and quitanza.issued.
- `403`: unauthorized_ruling: signatures are missing, invalid, or not from the required signers.
- `404`: not_found
- `409`: invalid_state

## Quitanzas

Terminal settlement proofs: Ed25519-signed over canonical JSON, hash-chained to the evidence trail, verifiable offline.

### `GET /v1/escrows/{id}/quitanza`

The quitanza that closed an escrow.

- Path `id`: Escrow id (esc_…)

Responses:

- `200`: The quitanza.
- `404`: not_found: the matter has not closed yet.

### `GET /v1/quitanzas/{id}`

Fetch a quitanza by id.

- Path `id`: Quitanza id (qtz_…)

Responses:

- `200`: The quitanza.
- `404`: not_found

### `GET /v1/quitanzas/{id}/verify`

Verify a quitanza against the live trail.

Checks the issuer signature over the canonical body, the integrity of the full evidence trail, and that the quitanza's trailHead matches the trail at its recorded length. The same checks run offline with no Quitanza code. See the quitanza format spec.

- Path `id`: Quitanza id (qtz_…)

Responses:

- `200`: Verification verdicts.
- `404`: not_found

## Webhooks

Durable signed event delivery: Ed25519 signature over the canonical JSON body, retries with backoff, inspectable dead-letter list.

### `POST /v1/webhooks`

Register a webhook endpoint.

Every engine event is POSTed to each registered URL as canonical JSON, signed with the announced key: x-quitanza-signature carries the Ed25519 signature, x-quitanza-key-id the signing public key. Failures retry with backoff; exhausted deliveries land on the dead-letter list.

Responses:

- `201`: Registered. Pin signingKey to verify deliveries.
- `400`: bad_request: url is required.

### `GET /v1/webhooks`

List registered webhooks and the signing key.

Responses:

- `200`: Registered URLs and the signing public key.

### `GET /v1/webhooks/dead-letters`

Deliveries that exhausted every retry.

Responses:

- `200`: Dead letters, oldest first.

## X402

The x402 payment handshake settled through escrow (simulated rails).

### `GET /v1/x402/demo`

A paid resource settled through escrow (x402, simulated rails).

Without an X-PAYMENT header: 402 with PaymentRequirements (scheme quitanza-sandbox, network quitanza-local). With a valid signed payment header: runs the full escrow lifecycle for the request and returns the resource plus an X-PAYMENT-RESPONSE header naming the quitanza that proves the matter closed.

- Header `X-PAYMENT`: Base64-encoded JSON payment payload built by @quitanza/x402 createPaymentHeader().

Responses:

- `200`: The resource; X-PAYMENT-RESPONSE names the escrow and quitanza.
- `402`: Payment required: x402 PaymentRequirements offers.

## Meta

Service metadata.

### `GET /health`

Service health and issuer key.

Responses:

- `200`: Service is up.

### `GET /.well-known/quitanza-issuer.json`

The issuer keys this deployment signs quitanzas with.

Public, no authentication. Serves the Ed25519 issuer keys as { keys: [{ keyId, alg, publicKey, validFrom }] }. A quitanza verifies against a domain when its signing key appears here and the signature checks out offline; the list form supports key rotation.

Responses:

- `200`: The issuer key set.

### `GET /v1/openapi.json`

This document.

The OpenAPI 3.1 description of every route, including the error model.

Responses:

- `200`: The OpenAPI 3.1 document.

## Error model

Every error is `{ "error": { "code", "message" } }`. Codes: `bad_request` (400), `payment_invalid` (402), `mandate_violation` and `unauthorized_ruling` (403), `not_found` (404), `invalid_state` (409, illegal transition), `unprocessable` (422).
