Quitanza

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

{
  "label": "buyer-agent"
}

Responses:

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:

GET /v1/mandates/{id}

Fetch a mandate with its usage.

Responses:

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

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

Responses:

GET /v1/escrows

List all escrows.

Responses:

GET /v1/escrows/{id}

Fetch one escrow.

Responses:

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.

Responses:

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.

Responses:

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.

Responses:

GET /v1/escrows/{id}/custody

The custody journal entry for an escrow.

Present once a settlement has been attempted. A failed entry means custody (e.g. the chain) did not confirm: the escrow stays in its pre-terminal state, carries custody.status settle-failed, and a custody.failed event has fired. A confirmed entry records that the settlement went through.

Responses:

POST /v1/escrows/{id}/custody/retry

Retry a failed custody settlement.

Re-runs the journaled settlement. Idempotent: a settlement custody has already confirmed returns its quitanza without touching custody again; a settlement is never silently lost and never double-sent. On success the escrow reaches its terminal state and the quitanza is issued.

Responses:

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.

Responses:

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.

Responses:

GET /v1/escrows/{id}/dispute

Fetch the dispute on an escrow.

Responses:

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

Attach content-addressed evidence.

Responses:

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} and, for a split, its shares. Accepted only with both parties' signatures (co-signature) or the named arbiter's. A split ruling must carry shares: integer basis points {payerBps, payeeBps} summing to 10000; signatures commit to the exact division, and shares on any other outcome are rejected. 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 (a split default divides evenly).

Responses:

Quitanzas

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

GET /v1/log/head

The transparency log's signed tree head (public).

No authentication, rate limited per client IP. Every issued quitanza's hash is a leaf of an append-only Merkle log (RFC 6962 shapes); this returns the current size and root, signed by the active issuer key over { logSize, rootHash, signedAt }. Checkpoint heads somewhere the log cannot reach: any later head must prove consistency with yours.

Responses:

GET /v1/log/proof/{quitanzaId}

Inclusion proof for a quitanza (public).

No authentication, rate limited per client IP. Proves the quitanza is a leaf of the log: its leaf index, leaf hash (SHA-256 of 0x00 plus the document's canonical JSON), and the audit path to the current root. Verify against the signed tree head, offline.

Responses:

GET /v1/log/consistency/{firstSize}/{secondSize}

Consistency proof between two log sizes (public).

No authentication, rate limited per client IP. Proves the log at secondSize (omit for the current size: /v1/log/consistency/{firstSize}) extends the log at firstSize without rewriting anything behind it. Compare firstRoot against your checkpointed head, secondRoot against the current signed head.

Responses:

POST /v1/verify

Verify a posted quitanza (public).

No authentication. Post a full quitanza document; the response says whether the issuer signature verifies over the canonical body and whether the signing key is one of this deployment's published eras (with the era's validity window when it is). The on-chain anchor is echoed when present. Strictly rate limited per client IP, body capped at 32 KB. Trail intactness needs the server-held trail and lives at GET /v1/quitanzas/{id}/verify.

Responses:

GET /v1/escrows/{id}/quitanza

The quitanza that closed an escrow.

Responses:

GET /v1/quitanzas/{id}

Fetch a quitanza by id.

Responses:

GET /v1/quitanzas/{id}/verify

Verify a quitanza against the live trail.

Checks the issuer signature over the canonical body (the signing key must be one of this deployment's published issuer keys), the integrity of the full evidence trail, and that the quitanza's trailHead matches the trail at its recorded length. The issuer field names the key that signed this quitanza, which after a rotation may differ from the key signing today. The same checks run offline with no Quitanza code. See the quitanza format spec.

Responses:

Webhooks

Durable signed event delivery: Ed25519 signature over the canonical JSON body, retries with backoff, inspectable dead-letter list. Every event is also replayable per escrow via GET /v1/escrows/{id}/events, derived from the evidence trail.

GET /v1/escrows/{id}/events

Replay every event the escrow emitted.

Every event, in order, exactly as webhooks received it: derived from the hash-chained evidence trail, so a replay can never disagree with the tamper-evident record. Use this to recover anything a webhook endpoint missed, or instead of webhooks entirely.

Responses:

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:

GET /v1/webhooks

List registered webhooks and the signing key.

Responses:

GET /v1/webhooks/dead-letters

Deliveries that exhausted every retry.

Responses:

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.

Responses:

Meta

Service metadata.

GET /health

Service health, version, uptime and issuer key.

Responses:

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

The issuer keys this deployment signs quitanzas with.

Public, no authentication. Serves every Ed25519 issuer key era as { keys: [{ keyId, alg, publicKey, validFrom, validUntil? }] }, oldest first; the last key signs today and rotated-out keys carry validUntil. A quitanza verifies against a domain when its signing key appears here and the signature checks out offline, so proofs signed before a rotation keep verifying by keyId.

Responses:

POST /v1/admin/issuer/rotate

Rotate the issuer key (admin).

Generates a fresh Ed25519 issuer keypair and signs all future quitanzas with it. The outgoing key receives a validUntil stamp and stays in the well-known issuer document, so quitanzas issued before the rotation keep verifying by keyId. Root key only; refused on open sandboxes with no key configured.

Responses:

POST /v1/admin/keys

Create a bearer key (admin).

Mints a new bearer key for this deployment. The response is the only place the secret ever appears: the server stores its sha256 and nothing else. An optional rateLimitMax gives the key its own request budget per window. Root key only; refused on open sandboxes.

Responses:

GET /v1/admin/keys

List bearer keys (admin).

Every created key: id, label, createdAt, revokedAt when revoked, and any per-key rate limit. Secrets and hashes are never returned. Root key only.

Responses:

DELETE /v1/admin/keys/{keyId}

Revoke a bearer key (admin).

Stamps the key revoked, effective immediately; the id stays listed for audit. Idempotent. Root key only. The root key itself cannot be revoked here: it lives in the deployment environment.

Responses:

GET /v1/admin/metrics

Operator metrics (admin).

One JSON page of operator sight: escrows by state, quitanzas issued, transparency-log size, webhook delivery counters, lifetime rate-limit rejections, API key counts, request-log line count and rotation state, uptime and version. Root key only.

Responses:

GET /v1/admin/rate

Live rate-limit counters (admin).

The current window's request counters by budget unit: authenticated traffic counts per key (key:root, key:), unauthenticated probes per client IP. Each entry shows the budget used and remaining; keys with their own limit also report it as max. Root key only; refused on open sandboxes.

Responses:

GET /v1/openapi.json

This document.

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

Responses:

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

This page as markdown