---
title: "Mandate and governance"
description: "The four commands that mint spend authority, verify a presentation token offline, issue spend cards, and read KYC settlement."
---

import { Callout } from "fumadocs-ui/components/callout";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

Four executable commands govern what an agent is allowed to spend and on what instrument: `codespar mandate create` mints the authority, `codespar mandate verify` checks a presentation token a counterparty handed you, `codespar issue` creates and controls the card the authority is spent on, and `codespar verification-status` reads where an asynchronous KYC inquiry landed. `mandate` is a command group with those two leaves; `issue` and `verification-status` are top level.


## Two commands that do not exist

`codespar mandate show` and `codespar mandate revoke` are not in the shipped binary. Measured with `codespar mandate --help`, which lists exactly `create`, `verify` and `help` and nothing else. There is no CLI path to read a mandate back or to revoke one; the lifecycle beyond creation is described in [Mandates](/docs/concepts/mandates).

## Shared behaviour

These root options are declared on the program, not on the subcommands, and apply to all four. Commander accepts `--json` in either position: `codespar --json mandate verify <token>` and `codespar issue --input '{...}' --json` were both measured and both emitted JSON.

| Option | Type | Required | What it does |
|---|---|---|---|
| `--api-key <key>` | `string` | no | API key for this invocation. Beats the environment and the config file. |
| `--base-url <url>` | `string` | no | API host. Falls back to `https://api.codespar.dev` when nothing sets it. |
| `--project <id>` | `string` | no | Sends `x-codespar-project` on requests the CLI's own HTTP client makes. |
| `--json` | `boolean` | no | Machine-readable output on stdout instead of the human block. |
| `-v, --version` | `boolean` | no | Prints the version. The short flag is `-v`, not Commander's default `-V`. |

Credentials resolve flag first, then environment (`CODESPAR_API_KEY`, `CODESPAR_PROJECT`, `CODESPAR_BASE_URL`), then `~/.codespar/config.json`, which `login` writes with mode `0600`. Three of the four commands refuse immediately without a key; `mandate verify` is the exception and is documented below.

Channels are split by design. Tables, key/value blocks and JSON go to stdout; the `✓`, `ℹ` and `⚠` lines go to stderr. In human mode `codespar issue` and `codespar verification-status` write **nothing at all to stdout**, which is worth knowing before you pipe them.

### Exit codes

Measured on these four commands, with the command that produced each.

| Exit | What is printed | Measured on |
|---|---|---|
| `0` | the result | every success, and any `--help` |
| `1` | `✗ <message>` on stderr, no stack | argument and credential refusals in all four commands |
| `1` | `error: <message>` on stderr, no `✗` | Commander's own refusals: a missing required argument or option |
| `1` | the full report, then a note on stderr | `mandate verify` when the token does not verify |
| `2` | `✗ internal error:` and a stack trace | `issue` and `verification-status` when the call fails |

<Callout type="warn">
The `2` is not a status code for "the operation failed". It is the CLI's bucket for an error it did not recognise, and the two commands here that go through a typed SDK wrapper land in it on ordinary failures, because that wrapper throws a plain `Error` rather than the CLI's own error type. Measured with one stub answering `403 insufficient_scope`: `codespar mandate create` exited `1` with `✗ POST /v1/consents/init → 403: API key is missing the cards:write scope.`, while `codespar issue` exited `2` with a stack trace for the same refusal. A script that treats `2` as a bug and `1` as a rejection will read a scope problem as a crash.
</Callout>

## `codespar mandate create`

Runs the two-leg directed-pay consent flow from your terminal: it mints a one-shot consent token carrying the intent (purpose, caps, currency or slots, payee allowlist), then submits the consumer side against that token, which is what provisions the funding source and signs the mandate. It prints the mandate id you hand to `codespar spend`.

<Endpoint method="POST" path="/v1/consents/init" base="https://api.codespar.dev" deprecated />

<Endpoint method="POST" path="/v1/consents/{token}/submit" base="https://api.codespar.dev" />

The first leg carries your API key; the token in the second leg's URL is the authentication, so the bearer header there is ignored. Two notes on those routes, both measured against the committed OpenAPI snapshot. `POST /v1/consents/init` is in the published document and marked `deprecated` there, as an alias of `POST /v1/consents` kept for two releases; the CLI still calls the alias. `POST /v1/consents/{token}/submit` is **not in the published document** ([ent#1186](https://github.com/codespar/codespar-enterprise/issues/1186)): it is the leg the consumer's browser is meant to call, and this command calls it from the tenant's terminal. See [Mandates](/docs/concepts/mandates) for why that leg is kept out of the client surface.

### Flags

| Flag | Type | Required | What it does |
|---|---|---|---|
| `-c, --consumer <id>` | `string` | yes | Consumer id. Sent as `consumer_id` on the submit leg. |
| `--agent <id>` | `string` | yes | Agent the mandate authorizes. Sent as `agent_id` on the init leg. |
| `--purpose <text>` | `string` | yes | Human purpose, signed into the mandate. Becomes `intent.purpose`. |
| `-p, --payee <list>` | `string` | yes | Allowlisted payees, comma-separated. Each entry is trimmed and empties dropped; the result becomes `intent.merchant_allowlist`. |
| `--cap <minor>` | `integer` | when no `--slot` | Total cap in minor units. Becomes `intent.cap_minor`. |
| `--per-tx-cap <minor>` | `integer` | when no `--slot` | Per-transaction cap in minor units. Becomes `intent.per_tx_cap_minor`. |
| `--slot <spec>` | `CURRENCY:RAIL:CAP:PER_TX_CAP` | no | Repeatable. Builds `intent.slots`, one entry per currency. Default is an empty list. |
| `--currency <code>` | `string` | no | Default `USDC`. The single-currency path only. |
| `--rail <rail>` | `string` | no | Default `usdc-onchain`. Sent on the submit body. |
| `--ttl <seconds>` | `integer` | no | Default `86400`. Becomes `intent.mandate_ttl_seconds`. |
| `--pin-kind <kind>` | `string` | no | Default `merchant-id`. Becomes `intent.merchant_pin_kind`. |
| `--provider-token <token>` | `string` | no | Sent on the submit body. When omitted the CLI sends the literal `<rail>:<consumer>`. |

Two rules are worth stating in prose because they are not visible in the flag list. **`--slot` wins.** When at least one `--slot` is given, `--cap`, `--per-tx-cap` and `--currency` are ignored, and the top of the intent mirrors the **first** slot: its cap, its per-transaction cap and its currency. **The rail on the submit leg is not the slot's rail.** Each slot carries its own rail inside `intent.slots`; the `--rail` value only satisfies the submit schema, so leaving the default in place on a multi-slot mandate is expected and was measured doing exactly that.

`--pin-kind` and `--currency` are checked against the vocabularies the consent contract declares, so a typo fails at your terminal instead of at the end of a consent ceremony:

```text title="exit 1"
✗ --pin-kind expects one of pix-key, merchant-id, mcc, got "inventado".
✗ --currency expects one of BRL, USD, MXN, COP, ARS, USDC, BRLA, got "EUR".
```

### Example

<Split min={380}>
<SplitPane label="Two slots, one signature">

```bash
codespar mandate create \
  --consumer con_9x1 \
  --agent agt_7fa21c \
  --purpose "Compras do mes" \
  --payee "loja@empresa.com.br" \
  --slot BRL:pix-celcoin:50000:10000 \
  --slot USDC:usdc-onchain:100:25
```

</SplitPane>
<SplitPane label="Output">

```text title="stdout"
mandate    cm_4f19ab
consumer   con_9x1
agent      agt_7fa21c
purpose    Compras do mes
allowlist  loja@empresa.com.br
slots      BRL/pix-celcoin cap 50000 (per-tx 10000)  |  USDC/usdc-onchain cap 100 (per-tx 25)
```

```text title="stderr"
✓ multi-slot mandate created
ℹ One signature, 2 currencies — caps are per-currency (no FX).
  See the wallet:  codespar wallet con_9x1
  Spend:           codespar spend --mandate cm_4f19ab --payee loja@empresa.com.br --amount 1 --agent agt_7fa21c
```

</SplitPane>
</Split>

The caps in that call are minor units, per currency, with no conversion between slots: 50000 and 10000 are R$ 500,00 total and R$ 100,00 per transaction on the BRL slot, 100 and 25 are the USDC slot's own numbers.

With the single-currency form (`--cap 10000 --per-tx-cap 2500`, no `--slot`), the block changes shape: `per-tx cap`, `total cap` and `rail` rows replace the `slots` row, and the values are labelled with `--currency`, which defaults to `USDC`.

With `--json`, stdout carries the **submit** response and nothing else, and stderr is empty. The init response is not printed in either mode.

```json title="--json, stdout"
{
  "mandate_id": "cm_4f19ab",
  "consumer_id": "con_9x1",
  "status": "active",
  "expires_at": "2026-09-13T18:00:00Z"
}
```

<Callout type="warn">
The one field the CLI reads from the submit leg is `mandate_id`; everything else in the body is passed through to `--json` untouched. When `mandate_id` is absent the human output prints `(unknown)` in its place.
</Callout>

### Refusals

Every row below was run. Validation of caps, TTL and slots happens before any request, so a malformed call costs no round trip.

| Trigger | What is printed on stderr | Exit |
|---|---|---|
| no API key resolvable | ``✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| a missing required flag | `error: required option '--agent <id>' not specified` | `1` |
| `--payee` empty after trimming | `✗ --payee is required (allowlisted payee: x402 URL, EVM address, or Pix key; comma-separated for several).` | `1` |
| no `--cap` and no `--slot` | `✗ --cap must be a positive integer in minor units (or use --slot for a multi-currency mandate).` | `1` |
| `--ttl 0` or a non-integer | `✗ --ttl must be a positive integer (seconds).` | `1` |
| a `--slot` without four fields | `✗ --slot must be CURRENCY:RAIL:CAP:PER_TX_CAP (e.g. USDC:usdc-onchain:100:100). Got: BRL:pix:500` | `1` |
| a slot cap of `0` or a non-integer | `✗ --slot BRL:pix-celcoin:0:10: cap must be a positive integer (minor units).` | `1` |
| the API refuses either leg | `✗ POST /v1/consents/init → 403: API key is missing the cards:write scope.` | `1` |

The last row is the general shape for an HTTP failure on this command: verb, path, status, then the message the API sent. A scope refusal and a validation refusal are indistinguishable by exit code; the text is what separates them.

## `codespar mandate verify`

Decodes a base64url presentation token, rebuilds the canonical signing string from the fields it carries, and checks the Ed25519 signatures against keys you supply or against the `did:web` documents it resolves. It is the only command on this page that runs with no API key: it loads the config for a base URL and never calls `requireApiKey`. Measured with `CODESPAR_API_KEY` emptied, and it printed the full report.

There is no REST route behind the offline path. In network mode it fetches DID documents, described below.

### Arguments and flags

| Name | Type | Required | What it does |
|---|---|---|---|
| `<token>` | `string` | yes | The presentation token, base64url. |
| `--agent-pubkey <hex>` | `string` | no | Raw 32-byte Ed25519 agent key, hex. Its presence forces offline mode. |
| `--issuer-pubkey <hex>` | `string` | no | Raw 32-byte Ed25519 issuer key, hex. Its presence forces offline mode. |
| `--issuer-did <did>` | `string` | no | Issuer DID for network mode. Without it the issuer DID is derived from the host segment of the agent DID. |

Either pubkey flag switches the whole command to offline: zero network calls, both signatures checked against what you passed. With neither flag it runs in network mode and resolves keys from DID documents. There is no flag that forces network mode while a key is supplied.

Keys are 64 hex characters, a leading `0x` is tolerated and case is ignored. Anything else is refused before the token is touched.

### What counts as verified

A token passes when at least one carried signature verified and none failed. In offline mode a signature whose key you did not supply is reported `skipped`, which neither passes nor fails on its own: a token with a good `agent_sig` and no `--issuer-pubkey` exits `0` with the issuer line marked skipped. That was measured, and it is the behaviour to read carefully before treating this command as a gate.

**Expiry is reported, not enforced.** A token whose `expires_at` was in the past, with both signatures good, printed `[expired]` next to the timestamp and still exited `0` with `verified`. If expiry matters to your check, read `expired` out of the `--json` body yourself.

**The principal's document is never printed.** Human output prints `principal_kyc  present` or `absent`; `--json` emits `principal_kyc_ref_present` as a boolean. The CPF or CNPJ reference itself is redacted in both.

### Example

<Split min={380}>
<SplitPane label="Offline, both keys">

```bash
codespar mandate verify "$TOKEN" \
  --agent-pubkey 2abe98d728d767007bb7329e9d1f1d7f107b2f1288457291f300ece461349b87 \
  --issuer-pubkey 272b1badc6c62899dc5a1a1c25f09b06ab57db69e31e938ac30791c7bee9f17a
```

</SplitPane>
<SplitPane label="Output">

```text title="stdout"
signatures
agent_sig   ✓ verified  kid did:web:id.codespar.dev:acme:shopper#1  via flag
issuer_sig  ✓ verified  via flag

mandate
id             mnd_3xq8w2
agent_id       agt_7fa21c
agent_did      did:web:id.codespar.dev:acme:shopper
kid            did:web:id.codespar.dev:acme:shopper#1
type           payment
amount         50000 BRL
max_amount     10000 BRL
purposes       groceries, delivery
principal_kyc  present
expires_at     1789913600 (2026-09-20T14:13:20.000Z)
format         v3
```

```text title="stderr"
✓ mandate token verified (offline mode)
```

</SplitPane>
</Split>

Corrupt `agent_sig` in that token and the same call exits `1`: the report still prints in full, the agent line reads `✗ failed`, the issuer line still reads `✓ verified`, and stderr adds `⚠ mandate token NOT verified (offline mode)` plus a line saying a carried signature failed or could not be checked. That is the shape to key a gate on, because a single bad signature is enough to fail the token even when the other one verified.

The `--json` body of the passing run:

```json title="--json, stdout"
{
  "verified": true,
  "mode": "offline",
  "format_version": 3,
  "signatures": {
    "agent_sig": { "present": true, "status": "verified", "kid": "did:web:id.codespar.dev:acme:shopper#1", "source": "flag", "detail": null },
    "issuer_sig": { "present": true, "status": "verified", "kid": null, "source": "flag", "detail": null }
  },
  "mandate": {
    "id": "mnd_3xq8w2",
    "agent_id": "agt_7fa21c",
    "agent_did": "did:web:id.codespar.dev:acme:shopper",
    "kid": "did:web:id.codespar.dev:acme:shopper#1",
    "type": "payment",
    "amount": "50000",
    "currency": "BRL",
    "max_amount": "10000",
    "parent_id": null,
    "denomination": null,
    "purposes": ["groceries", "delivery"],
    "principal_kyc_ref_present": true,
    "expires_at": 1789913600,
    "expires_at_iso": "2026-09-20T14:13:20.000Z",
    "expired": false,
    "format_version": 3
  }
}
```

A token that fails to decode answers in the same channel it would have used: `--json` prints `{ "verified": false, "error": "mandate_format_unsupported" }` and exits `1`, while the human mode throws the message as an ordinary refusal.

### Network mode

Without a pubkey flag the command resolves keys over the network, trying two URLs in order and stopping at the first that yields an Ed25519 key: the standard `did:web` mapping (`https://<host>/.well-known/did.json`, or `https://<host>/<path>/did.json` when the DID carries path segments), then the API fallback at `<base-url>/v1/agents/<did>/did.json`. Only `OKP` / `Ed25519` entries of exactly 32 bytes are taken from a document's `verificationMethod`; everything else is skipped. When the token names a key id, keys matching it are tried first. A document that cannot be fetched, or that carries no usable key, is a **failure** in network mode, not a skip. Each fetch has its own 15 second budget.

<Callout type="warn">
Network mode reaches out to resolve the DID document; the three shapes above are the offline path, which needs nothing but the token.
</Callout>

### Refusals

| Trigger | What is printed on stderr | Exit |
|---|---|---|
| no token | `error: missing required argument 'token'` | `1` |
| a token that is not base64url JSON | `✗ cannot decode token — not a valid base64url mandate presentation token.` | `1` |
| `format_version` below `2` | `✗ unsupported mandate format (need format_version >= 2).` | `1` |
| a key that is not 64 hex characters | `✗ --agent-pubkey must be 64 hex chars (a raw 32-byte Ed25519 key).` | `1` |
| a signature failed or could not be checked | the report, then `⚠ mandate token NOT verified` | `1` |

## `codespar issue`

Opens a throwaway session with no servers, calls the `codespar_issue` meta-tool through it, and closes the session in a `finally` whether the call succeeded or not. There is no `--server`: the meta-tool router picks the issuing rail.

<Endpoint method="POST" path="/v1/sessions/{id}/execute" base="https://api.codespar.dev" mayMove="Resolves `tool` as a CodeSpar meta-tool first, then as a catalog tool" />

Measured on the stub, one invocation is three requests: `POST /v1/sessions` with body `{"servers":[],"user_id":"cli-user"}`, then the execute above with `{"tool":"codespar_issue","input":{...}}`, then `DELETE /v1/sessions/<id>`.

### Flags

| Flag | Type | Required | What it does |
|---|---|---|---|
| `-i, --input <json>` | `string` | one of the two | The meta-tool arguments as a JSON object. |
| `-f, --input-file <path>` | `string` | one of the two | The same object read from a file. |
| `-u, --user <id>` | `string` | no | Session user. Defaults to the literal `cli-user`. |

`--input` and `--input-file` are mutually exclusive, and one of them is mandatory. The payload has to parse as a JSON **object**: an array or a scalar is refused by name.

### The arguments the CLI checks

This table is the subset the CLI validates locally, before opening a session. It is not the schema of the meta-tool, which has more fields and is the authority on all of them; for that, read [`codespar_issue`](/docs/concepts/meta-tools/issue).

| Field | Type | Required | What the CLI enforces |
|---|---|---|---|
| `action` | `string` | always | One of `card-virtual`, `card-physical`, `card-control`, `card-get`. |
| `cardholder_id` | `string` | for `card-virtual` and `card-physical` | Presence only. |
| `program_id` | `string` | for `card-virtual` and `card-physical` | Presence only. |
| `shipping_address` | `object` | for `card-physical` | Presence only. |
| `card_id` | `string` | for `card-control` and `card-get` | Presence only. |
| `control` | `string` | for `card-control` | Presence only. The refusal text names `freeze`, `unfreeze` and `cancel`; the CLI does not check the value against that list. |

### Example

<Split min={380}>
<SplitPane label="A virtual card">

```bash
codespar issue --input '{
  "action": "card-virtual",
  "cardholder_id": "usr_123",
  "program_id": "afg_123"
}'
```

</SplitPane>
<SplitPane label="Output (stdout is empty)">

```text title="stderr"
✓ issue card-virtual → active
ℹ Card id: crd_7bd2f1
ℹ Type: virtual
ℹ Last four: 4417
ℹ Cardholder: usr_123
```

</SplitPane>
</Split>

Stdout really is empty there. The human path prints the status line and then four conditional lines, one each for `id`, `card_type`, `last_four` and `cardholder_id`, and every one of them goes to stderr. Pass `--json` and the relationship inverts: stdout carries the result object and stderr is empty.

```json title="--json, stdout"
{
  "id": "crd_7bd2f1",
  "status": "active",
  "card_type": "virtual",
  "last_four": "4417",
  "cardholder_id": "usr_123"
}
```

That object is the stub's, shaped to exercise the five fields the CLI reads. **The real result shape of `codespar_issue` was not verified here.** The `status` in the first stderr line falls back to the literal `ok` when the result carries no status.

### Refusals

| Trigger | What is printed on stderr | Exit |
|---|---|---|
| no API key resolvable | ``✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| neither `--input` nor `--input-file` | `✗ issue requires --input '<json>' or --input-file <path>.` followed by an example | `1` |
| both of them | `✗ Pass either --input or --input-file, not both.` | `1` |
| the payload is not valid JSON | `✗ --input is not valid JSON: <parser message>` | `1` |
| the payload is an array or a scalar | `✗ --input must be a JSON object.` | `1` |
| an unknown `action` | `✗ issue.action must be one of: card-virtual, card-physical, card-control, card-get.` | `1` |
| `card-physical` with no address | `✗ issue.shipping_address is required when action=card-physical.` | `1` |
| `card-control` with no control | the message below, naming the three controls | `1` |
| the tool call fails, for any reason | `✗ internal error:` and a stack trace reading `Error: issue failed: <reason>` | `2` |

Two of those messages are long enough that a table cell mangles them. Verbatim:

```text title="stderr"
✗ issue requires --input '<json>' or --input-file <path>. Example: --input '{"action":"card-virtual","cardholder_id":"usr_123","program_id":"afg_123"}'
✗ issue.control (freeze | unfreeze | cancel) is required when action=card-control.
```

The last row of the table covers a scope refusal, a provider rejection and a genuine bug alike. Measured twice: a stub answering `{"success":false,"error":"cardholder not found in program afg_123"}` and a stub answering `403 insufficient_scope` both produced exit `2` with a stack, the only difference being the text after `issue failed:`.

## `codespar verification-status`

Opens a throwaway session and reads where an asynchronous KYC inquiry landed for one tool call, once by default, or holding an event stream open until the terminal frame with `--stream`.

<Endpoint method="GET" path="/v1/tool-calls/{id}/verification-status" base="https://api.codespar.dev" />

The streaming form reads `/v1/tool-calls/{id}/verification-status/stream` instead. Both are in the published document; see [Tool calls](/docs/api/reference/tool-calls#get-v1tool-callsidverification-status) for the response schema, and [`verificationStatus`](/docs/api/sdk/status#verificationstatus) for the typed wrapper this command is built on.

### Arguments and flags

| Name | Type | Required | What it does |
|---|---|---|---|
| `<tool-call-id>` | `string` | yes | The tool call whose inquiry you are following. |
| `--stream` | `boolean` | no | Holds the stream open and prints each update until the terminal frame. |
| `--timeout <ms>` | `integer` | no | Bound on the stream. Defaults to `600000`, a constant in the command rather than a Commander default, so it does not appear as `(default: ...)` in `--help`. |
| `-u, --user <id>` | `string` | no | Session user. Defaults to the literal `cli-user`. |

`--timeout` must be a positive number and is checked before the session opens. It applies to the stream; the one-shot read does not use it.

### Example

<Split min={380}>
<SplitPane label="One read">

```bash
codespar verification-status tcl_kyc_9a1
```

</SplitPane>
<SplitPane label="Output (stdout is empty)">

```text title="stderr"
✓ verification_status: approved
ℹ tool_call_id: tcl_kyc_9a1
ℹ original_status: pending
ℹ idempotency_key: idem_5c2e
ℹ hosted_url: https://verify.codespar.dev/i/inq_44a2
ℹ events: 2
```

</SplitPane>
</Split>

As with `issue`, the human mode leaves stdout empty. `idempotency_key` falls back to `-` when absent; `hosted_url` is the one line printed only when the field is there, and apart from the label on the first line it is the only thing that separates this rendering from its settlement twin `codespar payment-status`, which prints the same four remaining lines and has no `hosted_url` field at all. `events` is the count, not the list: to see the events themselves, use `--json`, which puts the whole result object on stdout unchanged.

```json title="--json, stdout"
{
  "tool_call_id": "tcl_kyc_9a1",
  "verification_status": "approved",
  "original_status": "pending",
  "idempotency_key": "idem_5c2e",
  "hosted_url": "https://verify.codespar.dev/i/inq_44a2",
  "events": [
    { "at": "2026-09-12T18:02:11Z", "status": "pending" },
    { "at": "2026-09-12T18:04:39Z", "status": "approved" }
  ]
}
```

<Callout type="warn">
Under `--stream`, each update prints as `update #N: verification_status=<s>  events=<n>` on stderr. With `--stream --json`, each update is its own complete JSON document on stdout, followed by the final result — several documents in one stream, not one, so read them as a sequence rather than piping straight into a single-document parser. Ctrl+C aborts the stream and is reported as an abort, not as a result.
</Callout>

### Refusals

| Trigger | What is printed on stderr | Exit |
|---|---|---|
| no API key resolvable | ``✗ Not logged in. Run `codespar login` or set CODESPAR_API_KEY.`` | `1` |
| no tool-call id | `error: missing required argument 'tool-call-id'` | `1` |
| `--timeout` not a positive number | `✗ --timeout must be a positive integer (milliseconds).` | `1` |
| the read fails, for any reason | `✗ internal error:` and a stack trace reading `CodesparApiError: verificationStatus failed: <status> <body>` | `2` |

The last row was measured against a stub answering `403 insufficient_scope`. The same caution as `issue` applies: `2` here is a refused call, not a crash.

