---
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";

# Mandates, receipts and approvals

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. Every spend path on the buy side re-verifies it server-side before money moves, and every settled spend seals a signed **receipt** that names the mandate it ran under. When a policy rule holds a spend for a human, that hold is an **approval**, and it is decided in the dashboard by an admin, never by an API key.

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. |
| `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/init` mints a one-shot token; the consumer signs at the hosted page, which calls `POST /v1/consents/{token}/submit`. Both are in the code and not in the published document ([ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)). No route creates a signed mandate from an API key alone; whether one should is decision 1 in [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186#issuecomment-5612292876).",
      sdk: "Not exposed. The consent routes are not in the published document, so `cs.api` has no operation for them.",
      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, not in the published document ([ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)). `POST /v1/consumer-payments/execute` with `{ mandate, signature }`, not in the published document ([ent#978](https://github.com/codespar/codespar-enterprise/issues/978) renames it). `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\", ...)`. The by-id spend is not in the published document, so not in `cs.api`.",
      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`, a ledger feed per mandate with a different shape, not in the published document ([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`, not in the published document ([ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)), 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 signed with the same consumer secret that signed the mandate. 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).

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

**What exists in the code and is not in the published document.** `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). `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. Both are on the rename list in [ent#978](https://github.com/codespar/codespar-enterprise/issues/978) and are counted in [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186).

**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 (a boleto settlement over the mandate cap). 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` (same handler as the canonical `POST /v1/approvals/{id}/decide`; both not in the published document, [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)).

**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 in the published document.** `POST /v1/consents/init` (alias of the canonical `POST /v1/consents`) and the public `GET`/`submit` under `/v1/consents/{token}` are live and not in the published document (`openapi.json`). 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.
- **`POST /v1/consumers/mandates/{id}/spend`, `POST /v1/consumers/mandates/{id}/card` and `POST /v1/consumer-payments/execute` in the published document.** Live, described on the [Wallets reference](/docs/api/reference/wallets#spend-within-a-mandate) by hand, and not in the published document until [ent#978](https://github.com/codespar/codespar-enterprise/issues/978) and [ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186) land them.

## 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" },
]} />
