---
title: Mandates, receipts and approvals
description: What a mandate is, where one is born per channel (REST, SDK, CLI, dashboard, meta-tool), who signs it and why you never do, how it is paused and revoked, what the receipt of a spend is, and why an API key cannot decide a held approval.
---

import { Callout } from "fumadocs-ui/components/callout";

A **mandate** is the consumer's signed authorization for one agent to spend: which agent, for what purpose, to which payees, up to which cap per currency and per transaction, until when. On the governed spend paths (a boleto the agent pays with `codespar_pay`, the consumer-payments routes and the mandate spend route `POST /v1/consumers/mandates/{id}/spend`) it is re-verified server-side before money moves, and a settled spend seals a signed **receipt** that names the mandate it ran under. A direct Pix the agent sends with `codespar_pay` in a live project (to a key, a dynamic QR code or bank details) is not checked against a mandate: the runtime records it and does not refuse it. When a spend is held for a human, that hold is an **approval**, and it is decided in the dashboard by an admin, never by an API key. A policy rule can hold a spend; so can the per-transaction cap, on two paths only: a boleto the agent pays with `codespar_pay`, and a Pix on the consumer-payments routes. A boleto on the consumer-payments routes above the per-transaction cap, any other rail above it, and any spend over the periodic (monthly) or total cap are refused, and an approval does not lift them.

This page is the entry point. It says what exists today, per channel, with the routes that are in the published API document and the ones that exist in the code but are not in it yet. The consent flow in depth is on [Directed-pay](/docs/concepts/directed-pay); the policy layer that opens approvals is on [Guardrails](/docs/concepts/guardrails).

<Callout title="You do not sign anything" type="info">
The mandate is signed by CodeSpar, with a per-consumer secret that lives in CodeSpar's vault and never leaves it. Your backend starts the consent, the consumer signs at a hosted page, and you get back the mandate and its signature to store. Spending by mandate id (`codespar_pay`, `POST /v1/consumers/mandates/{id}/spend`) reconstructs and re-verifies the stored row, so most integrations never handle the signature at all.
</Callout>

## What a mandate carries

| Field | What it bounds |
|---|---|
| `agent_id` | The one agent this allowance authorizes. A spend by another agent is refused. |
| `purpose` | The signed purpose. The verifier binds every spend to it; it is not a caller field. |
| `merchant_allowlist` | The payees the agent may pay, pinned by `merchant_pin_kind` (`pix-key`, `merchant-id` or `mcc`). Never empty; a single `"*"` is an explicit wildcard. |
| `withdrawal_allowlist`, `dda_allowlist` | Separate, signed lists for cash-out destinations (`ted:<ispb>:<branch>:<account>`) and for the documents a DDA registration may expose. A merchant wildcard never authorizes either. The withdrawal rule is mid-rollout: until its flag is on, a cash-out destination named explicitly in the signed `merchant_allowlist` is also accepted, and the stricter refusal is recorded rather than applied. Sign cash-out destinations into `withdrawal_allowlist`. |
| `cap_minor`, `per_tx_cap_minor` | Total and per-transaction ceilings in minor units, per currency slot. There is no FX inside a mandate: a `BRL` cap does not authorize a `USDC` spend. |
| `slots` | The per-currency legs of a multi-slot mandate (for example `BRL` over Pix plus `USDC` on-chain), minted from one signature. |
| `expires_at` | UNIX seconds. Checked on every spend. |
| `status` | `active`, `paused`, `revoked` or `expired`. Only `active` spends. |

The signed material (`nonce`, `secret_version`, `hmac_signature`) is never projected by the reads; `GET /v1/mandates/{id}` returns the fields above and nothing that could be replayed.

## Two tables, one word

The word "mandate" names two different rows in the API, and the routes do not mix them.

- **Consumer mandate**, the canonical one. The allowance a consumer signed at the consent page. Lives under `/v1/mandates` and is what `codespar_pay`, `POST /v1/consumers/mandates/{id}/spend`, the card authorizer and the wallet withdrawal route verify. Signed with the consumer's HMAC secret. This page is about this one unless it says otherwise.
- **Org-side mandate**. An allowance the organization issues to its own agent for an **account wallet**, charged by `POST /v1/wallets/{id}/execute`, which decrements `remaining_amount` under a guard before the payment is attempted. Lives under `/v1/orgs/{orgId}/mandates`. Signed over a canonical string (12 fields on format version 2, 14 on version 3, 15 on version 4) with the org's HMAC key, and for a registered agent also with the agent's and the platform issuer's Ed25519 keys. That dual signature is what [Agent trust](/docs/concepts/agent-trust#verify-a-mandate-offline) lets a counterparty verify offline. Issuance is off by default: on a default deployment `POST /v1/orgs/{orgId}/mandates` answers `501 not_implemented` before any row is written.

A card bound to a consumer mandate (`GET /v1/consumers/mandates/{id}/card`) spends under that mandate's caps and allowlist; the `{id}` there is a consumer mandate id.

<MandateFlow />

## Where a mandate is born, per channel

Every cell below was read from the code, not from a roadmap. "Not exposed" means no surface exists on that channel today. Routes marked "not in the published document" exist in the API and are missing from `openapi.json`; the tracking issue is linked. `cs.api` is the typed client the SDK generates from the published document, so it covers exactly what that document covers.

<ChannelMatrix rows={[
  {
    op: "Create a consumer mandate",
    cells: {
      rest: "`POST /v1/consents` mints a one-shot token (`POST /v1/consents/init` is the deprecated alias); both entered the published document on 2026-09-11. The consumer then signs at the hosted page. " +
        "`POST /v1/consents/{token}/submit` is what that page calls, and it entered the published document on 2026-09-21, with `GET /v1/consents/{token}` (published) next to it.",
      sdk: "`cs.api.post(\"/v1/consents\", ...)` mints the token. The submit is in the document since 2026-09-21, so a released client will type it; the one on npm today was generated before that, and it is the consumer's browser that calls it anyway, not your backend.",
      cli: "[`codespar mandate create`](/docs/cli/commands#mandate-create-multi-slot) drives the same two calls; the operator supplies the provider token.",
      dashboard: "`/dashboard/consumers`, **+ New consent**, opens the hosted page in a popup.",
      metaTool: "Not exposed. No tool creates a mandate; `codespar_pay` spends under one that already exists.",
    },
  },
  {
    op: "Create an org-side mandate",
    cells: {
      rest: "`POST /v1/orgs/{orgId}/mandates` (published). `501 not_implemented` unless the deployment enables org-side issuance.",
      sdk: "`cs.api.post(\"/v1/orgs/{orgId}/mandates\", ...)`",
      cli: "Not exposed.",
      dashboard: "The sandbox cookbook at `/dashboard/sandbox/agent-mandate-payment` issues a demo one against the demo backend.",
      metaTool: "Not exposed.",
    },
  },
  {
    op: "Read and list",
    cells: {
      rest: "`GET /v1/mandates`, `GET /v1/mandates/{id}` (published). Org-side: `GET /v1/orgs/{orgId}/mandates`, `GET /v1/orgs/{orgId}/mandates/{id}`.",
      sdk: "`cs.api.get(\"/v1/mandates\")`, `cs.api.get(\"/v1/mandates/{id}\", ...)`",
      cli: "[`codespar wallet`](/docs/cli/commands#wallet) reads the per-currency rollup (`GET /v1/consumers/{id}/wallet`), not the mandate rows.",
      dashboard: "`/dashboard/consumers` lists funding sources and the consent log.",
      metaTool: "`codespar_wallet` with `action: \"balance\"` reads the rollup, not the mandate rows.",
    },
  },
  {
    op: "Pause, resume, revoke",
    cells: {
      rest: "`POST /v1/mandates/{id}/pause`, `POST /v1/mandates/{id}/resume`, `POST /v1/mandates/{id}/revoke` (published). Org-side: `POST /v1/orgs/{orgId}/mandates/{id}/revoke`; there is no org-side pause.",
      sdk: "`cs.api.post(\"/v1/mandates/{id}/pause\", ...)` and its two siblings.",
      cli: "Not exposed.",
      dashboard: "Not exposed. The consumers page shows the consent log and has no transition button.",
      metaTool: "Not exposed.",
    },
  },
  {
    op: "Spend under a mandate",
    cells: {
      rest: "`POST /v1/consumers/mandates/{id}/spend` by id, and `POST /v1/consumer-payments/execute` with `{ mandate, signature }`. Both are in the published document since 2026-09-11 ([ent#1176](https://github.com/codespar/codespar-enterprise/issues/1176)); [ent#978](https://github.com/codespar/codespar-enterprise/issues/978) still proposes renaming the second. Both carry `ted` for an outbound bank transfer, under the mandate's signed withdrawal allowlist. `POST /v1/wallets/{id}/transfer` (published) is a cash-out and needs `mandate` plus `signature`. `POST /v1/wallets/{id}/execute` (published) charges an org-side `mandate_id` on an account wallet.",
      sdk: "`cs.api.post(\"/v1/wallets/{id}/transfer\", ...)`, `cs.api.post(\"/v1/wallets/{id}/execute\", ...)`, and since 0.13.0 also `cs.api.post(\"/v1/consumers/mandates/{id}/spend\", ...)` and `cs.api.post(\"/v1/consumer-payments/execute\", ...)`.",
      cli: "[`codespar spend --mandate <id>`](/docs/cli/commands#spend)",
      dashboard: "`/dashboard/consumers`, **Execute**, streams the lifecycle step by step. `/dashboard/wallets/{id}`, **Execute**, takes an org-side mandate id.",
      metaTool: "[`codespar_pay`](/docs/concepts/meta-tools/pay) resolves the consumer's active mandate server-side; `mandateId` is optional and, when given, is tested on its own terms.",
    },
  },
  {
    op: "Read a receipt",
    cells: {
      rest: "`GET /v1/consumers/receipts/{id}`, `GET /v1/consumers/{consumerId}/receipts` (published). `GET /v1/consumers/mandates/{id}/receipts` (published), a ledger feed per mandate with a different shape ([ent#978](https://github.com/codespar/codespar-enterprise/issues/978)). Nothing per wallet: decision 3 in [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186#issuecomment-5612292876).",
      sdk: "`cs.api.get(\"/v1/consumers/receipts/{id}\", ...)`",
      cli: "Not exposed.",
      dashboard: "Not exposed as a receipt view.",
      metaTool: "[`codespar_ledger`](/docs/concepts/meta-tools/ledger) with `action: \"receipt\"` or `\"receipts\"`.",
    },
  },
  {
    op: "Decide a held approval",
    cells: {
      rest: "`POST /v1/orgs/{orgId}/approvals/{id}/decide` (published), and it refuses API keys by design (see below). Poll `GET /v1/approvals/{id}/status` (published).",
      sdk: "`cs.api.get(\"/v1/approvals/{id}/status\", ...)` to poll. Deciding is not exposed.",
      cli: "Not exposed.",
      dashboard: "`/dashboard/approvals`, admin or owner.",
      metaTool: "Not exposed. The tool result carries `approval_required` and the `approval_id` to poll.",
    },
  },
]} />

## Who signs, and why you never do

At consent submit the API provisions a per-consumer HMAC secret (or resolves the active version), builds the canonical mandate from the intent your backend declared plus the rail the consumer chose, signs it, and returns `{ mandate, signature }` to your callback. The signature is a 64-character hex HMAC-SHA256. You store the pair, or just the mandate id: nothing on your side ever holds the key, so nothing on your side can forge, extend or re-sign an allowance.

On every spend the verifier checks the signature against the secret version stamped on the row (a row edited in the database fails here), the expiry, that the funding source is still active, the cumulative cap in the mandate's own accounting unit, the per-transaction cap, the agent, the purpose, and membership of the payee in the signed allowlist. The refusal names the gate that tripped.

Two entry points differ only in who reconstructs the signed material:

- **By id** (`codespar_pay`, `POST /v1/consumers/mandates/{id}/spend`, the CLI): the server loads the stored row and its signature. The caller never sees the HMAC.
- **By proof** (`POST /v1/consumer-payments/execute`, `POST /v1/wallets/{id}/transfer`, the public payment-link gateway): the caller presents `{ mandate, signature }` it stored at consent time. Possession of the signature is what locates the row, and the row's stored signature is re-checked anyway.

Org-side mandates follow the same idea with a different key: the org HMAC covers the canonical string, and a registered agent adds two Ed25519 signatures over the same bytes. `@codespar/sdk/mandate` and `codespar mandate verify` verify that token offline, with the public keys from the agent's DID document.

## Lifecycle

```
                 pause                    revoke
   active  ─────────────►  paused  ───────────────►  revoked (terminal)
     ▲                       │                          ▲
     └───────────────────────┘                          │
              resume                                    │
     └──────────────────────────────────────────────────┘
                              revoke
```

| Route | Transition |
|---|---|
| `POST /v1/mandates/{id}/pause` | `active` to `paused`. Spends are refused while paused. |
| `POST /v1/mandates/{id}/resume` | `paused` to `active`. |
| `POST /v1/mandates/{id}/revoke` | `active` or `paused` to `revoked`. Terminal. |

The body is optional (`{ "reason": "..." }`, up to 280 characters). Each applied transition flips the status and appends a consent evidence row in the same transaction, so the LGPD trail stays append-only. Repeating a transition already at its target answers `200` with `changed: false`; an illegal one answers `409 invalid_transition`; a concurrent change answers `409 transition_conflict`.

**Expiry** is enforced at spend time from `expires_at`: a mandate past it is refused on every path whatever its stored status. No sweep flips a consumer mandate's row to `expired` today; the expiry sweep that writes that status exists for org-side mandates. Revoking an agent key (see [Agent trust](/docs/concepts/agent-trust)) stops new org-side mandates from being minted under it and does not revoke consumer mandates, which never read the agent key table.

## Receipts

A receipt is the signed record of one spend, the Control Record: four linked records, **mandate** (authority), **quote** (the offer the agent accepted), **payment** (settlement) and **delivery** (proof, when it lands), chained by a SHA-256 over their canonical JSON and then sealed twice. Its `state` is `paid`, `delivered`, `exception` or `voided`, and `exceptions` carries any settle-time mismatch (for example a payment that hit a payee the quote did not name).

**Two seals, because one of them proves nothing to an outsider.** `receipt_sig` is an HMAC under the same consumer secret that signed the mandate. It proves the receipt to whoever holds that secret, and nobody else: verifying it means being handed the key that also mints it, so to a buyer's accountant or a marketplace reconciling a payout it has been a claim rather than evidence. `receipt_sig_ed25519` is the other half — an Ed25519 signature over `codespar-receipt:v1:<receipt_id>:<chain>`, made with CodeSpar's platform issuer key, with `receipt_sig_kid` naming which published key made it. The public keys are served with no credential at [`/.well-known/codespar-receipt-keys.json`](https://api.codespar.dev/.well-known/codespar-receipt-keys.json), so anyone with the receipt JSON and a stock Ed25519 library can check it without asking us for anything. The signer is deliberately CodeSpar and not the organization being attested: a key held by the party under scrutiny would prove exactly what the HMAC already proves.

The Ed25519 signature is **additional**. The HMAC seal did not change and still means what it always meant. And what the asymmetric half attests is that CodeSpar sealed this receipt for a settlement CodeSpar executed — it is not a bank's confirmation that money moved.

<Callout title="Two limits on the Ed25519 seal" type="warn">
**It is only on receipts sealed since the API gained it.** There is no backfill and there will not be one: signing an older receipt today would attest to what the database says now, not to what happened then. Those receipts read `null` in `receipt_sig_ed25519` and `receipt_sig_kid`, which is an absent signature and not a failed one — their HMAC seal is untouched.

**Check it against the key document of the deployment that issued the receipt.** Production and the sandbox both publish a key under the same `kid` (`did:web:id.codespar.dev#1`) with the same `issuer` and different key material, so a sandbox receipt checked against the production keys fails as though somebody had tampered with it. Nothing in either document tells them apart yet; naming the deployment in the `kid` or the `issuer` is tracked in [ent#1641](https://github.com/codespar/codespar-enterprise/issues/1641).
</Callout>

**Where the id comes from.** The spend that sealed the receipt returns it:

- `POST /v1/consumer-payments/execute` and its stream variant answer `receipt_id` alongside the payment; the by-id spend route returns the same lifecycle response.
- `codespar_pay` returns a `receipt` object (`id`, `state`, `chain`) on the result.
- The x402 facilitator returns `receipt: { id, state, chain }` on settle.
- `POST /v1/wallets/{id}/execute` (account wallets, org-side mandate) does **not** seal a receipt; its evidence is the audit trail it returns.

**How to read it.** `GET /v1/consumers/receipts/{id}` for one, `GET /v1/consumers/{consumerId}/receipts` for a consumer's list, both project-scoped (a receipt in another project answers `404`). From an agent, `codespar_ledger` with `action: "receipt"` and a `receipt_id`, or `action: "receipts"` for the consumer. The [Receipts reference](/docs/api/reference/receipts) has the field list.

**Two more, and both are in the published document since [ent#1406](https://github.com/codespar/codespar-enterprise/issues/1406).** `POST /v1/consumers/receipts/{id}/delivery` seals the delivery proof onto a paid receipt and re-anchors the chain (an order confirmation, an NF-e access key, a tracking code); the proof is ranked, and a weaker or equal one is an idempotent no-op rather than a clobber. `GET /v1/consumers/mandates/{id}/receipts` lists the settled debits of one mandate straight from the wallet ledger, a different shape from the Control Record: it shows money that moved, not attempts. Both are still on the rename list in [ent#978](https://github.com/codespar/codespar-enterprise/issues/978).

**What does not exist.** A receipt read keyed by wallet (`/v1/wallets/{id}/receipts` or a `wallet_id` filter). Whether to add one is decision 3 in the [ent#1186 exposure comment](https://github.com/codespar/codespar-enterprise/issues/1186#issuecomment-5612292876).

## Approvals

The mandate is the standing authorization. An approval is a one-off hold placed in front of a single call by a rule the operator wrote (an approval-required or budget rule in `/dashboard/policies`) or by a guard the runtime does not let a rule override (the per-transaction cap, for a boleto the agent pays with `codespar_pay` or a Pix on the consumer-payments routes). The runtime writes a `pending_approvals` row with an expiry (24 hours by default) and answers the caller with `approval_required` and an `approval_id`. Nothing dispatched. The caller polls `GET /v1/approvals/{id}/status` until it leaves `pending`; `approved` means the held call was re-executed server-side and `execution_result` carries the upstream outcome.

**Who decides.** An admin or owner, at `/dashboard/approvals`. The dashboard proxies to `POST /v1/orgs/{orgId}/approvals/{id}/decide`, the older spelling of the canonical `POST /v1/approvals/{id}/decide` — one handler, two paths, both in the published document since [ent#1406](https://github.com/codespar/codespar-enterprise/issues/1406).

**Why an API key cannot decide.** The decide route answers `403 bearer_token_cannot_decide` to any project API key, on purpose. The key that asked for a spend must not be the key that approves it; otherwise a leaked `csk_` key approves its own transfer and the approval is theatre. The route accepts service-key callers and expects the approver's identity as a user token that the backend verifies, whose subject must hold admin or owner. This is dual control, and it is a design decision, not a missing endpoint. The [ent#1186 exposure comment](https://github.com/codespar/codespar-enterprise/issues/1186#issuecomment-5612292876) records it as decision 2: if programmatic approval is ever wanted, it is a different design (a narrow mandate minted in advance), not this route opened to keys.

<Callout title="What is enforced today" type="warn">
Refusing a caller that sends **no** user token is gated on `APPROVAL_DECIDE_ENFORCE_USER_TOKEN`, which is off by default. While it is off, a service-key caller with no token still decides, and the approver recorded is the `x-codespar-user` header the caller wrote; the audit chain marks those decisions `approval.decide_unverified` with `decided_by_source: "header_asserted"`. A caller that does send a token is verified regardless of the flag. The API-key refusal is not gated. [Guardrails](/docs/concepts/guardrails#approvals) tracks the rollout.
</Callout>

**Confirming a one-off spend outside the dashboard.** If your product confirms a purchase with the consumer on your own channel (a message, a push, a call), that confirmation is your logic and it runs **before** you dispatch: ask, get the yes, then call `codespar_pay` or the spend route. Do not model it as an approval you then try to decide by API; the route will refuse you, by design. When the risk is bounded rather than per-call, mint a narrow mandate instead: one payee in the allowlist, a per-transaction cap equal to the price, a short `expires_at`. The consent flow signs it, the spend runs under it, and the receipt records exactly that authority.

## What is not exposed yet

- **Creating a consumer mandate from an API key alone.** Not in the API. Today a mandate is born only through the consent flow (the consumer signs), the CLI that drives it, the dashboard popup that opens it, or the x402 facilitator translating an x402 authorization. Decision 1 in the [ent#1186 exposure comment](https://github.com/codespar/codespar-enterprise/issues/1186#issuecomment-5612292876).
- **The consent routes.** `POST /v1/consents` and its alias `POST /v1/consents/init` entered the published document on 2026-09-11 (ent#1176). The public `GET` and submit under `/v1/consents/{token}` stay out of it on purpose: the consumer's browser calls them, not a partner backend. Class (c) in [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186).
- **Deciding an approval by API key.** By design, see above. Decision 2.
- **A receipt read per wallet.** Decision 3.
- **The by-id spend and the signed-envelope spend.** `POST /v1/consumers/mandates/{id}/spend` and `POST /v1/consumer-payments/execute` entered the published document on 2026-09-11 ([ent#1176](https://github.com/codespar/codespar-enterprise/issues/1176)), and `cs.api` reaches both from `@codespar/sdk` 0.13.0. They carry `ted` for an outbound bank transfer under the mandate's signed withdrawal allowlist.
  `POST /v1/consumers/mandates/{id}/card` is still in the code and not in the published document ([ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)).

## Next

<NextStepsGrid items={[
  { label: "REFERENCE", title: "Mandates", description: "List, read, pause, resume and revoke a consumer mandate, from the published document.", href: "/docs/api/reference/mandates" },
  { label: "REFERENCE", title: "Receipts", description: "Read one receipt or a consumer's list; the Control Record fields.", href: "/docs/api/reference/receipts" },
  { label: "REFERENCE", title: "Approvals", description: "Poll a held call until it is decided.", href: "/docs/api/reference/approvals" },
  { label: "REFERENCE", title: "Orgs", description: "The org-side mandate table: issue (off by default), list, read, revoke.", href: "/docs/api/reference/orgs" },
  { label: "CONCEPT", title: "Directed-pay", description: "The consent flow in depth: tables, rails, verification, evidence.", href: "/docs/concepts/directed-pay" },
  { label: "CONCEPT", title: "Guardrails", description: "The policy layer that opens approvals and the dual-control rollout.", href: "/docs/concepts/guardrails" },
  { label: "COOKBOOK", title: "Shopping agent", description: "A buy-side agent that pays a real store's Pix under a signed mandate.", href: "/docs/cookbooks/shopping-agent" },
]} />
