Sumvin documentation
Your agent acts for you. Sumvin lets it prove who it acts for, and stay inside the limits you sign.
One path, five steps
Agent-readable
These docs are written for your agent as much as for you. Point it at /llms.txt for the index of every page, or at /skill.md for what it can do here.
New here? Start with what Sumvin is and how it works.
sumvin errand list
sumvin errand get
sumvin errand events
sumvin errand clarify --answer 1=
sumvin errand approve
sumvin errand reject
sumvin errand cancel
```
**Private preview:** this is rolling out and may not be available on your account yet.
**Coming soon:** payments arrive in a fast-follow release.
`errand create` is available to accounts in the early-access group. The other verbs — `list`, `get`, `events`, `clarify`, `cancel` — work for any account that already has errands.
## `sumvin errand create`
Submits a plain-language request, with the most it may spend and the currency.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand create "Order two boxes of A4 paper" --max 200 --currency USD
```
Creating an errand authorises nothing. The errand then waits at `pending_approval` for your signature before it searches for anything — see `sumvin errand approve` below.
**Flags**
| Flag | Effect |
| - | - |
| `` (required) | Plain-language description of what you want done. |
| `--max ` (required) | The most the errand may spend, as a positive amount in major units — for example `200` or `180.50`. |
| `--currency ` (required) | The currency of `--max`, as a three-letter code — for example `GBP` or `USD`. |
| `--intent-type` | `product`, `service`, or `composite`. Defaults to `product`. |
| `--idempotency-key ` | Lets you retry safely after a dropped connection without creating a duplicate — the CLI prints one automatically for you to reuse. |
**Output states**
* Created: the errand's ID and its starting status.
* Retried with the same idempotency key: the same errand, reported as already existing rather than duplicated.
* Not in the early-access group: the command refuses and tells you so.
* Missing or malformed limit: if `--max` or `--currency` is missing or isn't in the right shape, the CLI refuses before contacting Sumvin: `An errand needs the most it may spend and the currency it is in. Set it with --max and --currency , e.g. --max 200 --currency GBP.`
* Limit refused by Sumvin: an unsupported currency, or an amount finer than the currency holds, is refused — the CLI shows the reason with the same hint.
## `sumvin errand list` and `sumvin errand get`
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand list --status pending_approval
sumvin errand get --expand mandate
```
`list` filters by status, intent type, and sort order. `get` shows one errand in full, and can expand its mandate or spend details inline.
**Errand statuses**, in lifecycle order: `qualifying`, `pending_clarification`, `pending_approval`, `approved`, `searching`, `validating`, `monitoring`, `executing`, `completed`, `failed`, `expired`, `cancelled`. At `pending_approval`, nothing has been searched yet.
## `sumvin errand events`
Lists the activity log for one errand — every status change and clarification, in order.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand events
```
## `sumvin errand clarify`
Answers a question the agent asked before it can continue. Questions come before you sign.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand clarify --answer 1="White, 80gsm"
```
Each `--answer N=` answers question `N` from `sumvin errand get`. Repeat the flag to answer more than one question in the same call.
## `sumvin errand approve` and `sumvin errand reject`
After you create an errand, and answer any question it has, it waits for your signature before it searches for anything. `approve` signs the errand's bounds: what you asked for, the most it may spend and the currency, any conditions, and when the authorisation lapses. Approving needs a local signing key — see [`sumvin keys`](/reference/cli/keys) — because your approval has to be provably yours, not something Sumvin's own systems could produce on your behalf.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand approve
```
The CLI shows you the mandate before asking for confirmation:
```text theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
Errand: ipa_01HQ8ZK3M4N5P6R7S8T9V0W1X2
You are signing this errand's bounds, not a chosen product. Nothing has been
searched for yet; a match found within these bounds is bought without asking again.
Authorise a purchase: “Order two boxes of A4 paper”, spending up to 200.00 USD. The purchase is paid with your Visa card ending 4242. It proceeds only while all of the following hold:
- the price is at most 180 USD
- availability is one of: in_stock
A match found within these bounds is bought without asking again. This authorisation lapses on 12 Aug 2026, 07:30 UTC.
Ceiling: Up to USD 200.00
Expires: 2026-08-12T07:30:00.000Z (6:59:12)
Sign this errand's bounds? A match within them will be bought without asking you again.
```
You're signing bounds, not a chosen product. Once you sign, a match found within them is bought without asking you again — there's no second look at the pick.
Read the ceiling and the conditions as carefully as the statement — they are what decides whether a match is bought.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand reject
```
Rejecting is terminal: the errand is cancelled and its reservation released. Nothing is signed, searched, or spent. It needs no key, because it authorises nothing.
**Flags (both commands)**
| Flag | Effect |
| - | - |
| `--yes` | Skip the confirmation prompt. Required in any non-interactive session. Never give it to an unattended agent: it approves without you. |
**Output states**
* Approved: the errand moves on to search, checks the merchant, watches for your conditions, and settles the purchase — all within the bounds you signed.
* Rejected: the errand is cancelled.
* "Not awaiting approval": only an errand in `pending_approval` can take a decision.
* "Mandate has expired": approving is refused once the deadline passes — reject it and start a new errand.
## `sumvin errand cancel`
Stops an errand that's still in progress.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin errand cancel
```
## Exit codes
Every verb uses `0` for success and `1` for failure. Cancelling an errand that's already in a terminal status still exits `0` — the end state you wanted already holds.
**Try asking your agent:** "Check whether my last errand needs anything from me."
# The --json contract
Source: https://docs.sumvin.com/reference/cli/json-output
How every sumvin command speaks to a script or an agent.
**The CLI is for developers.** It's mainly for people building and experimenting on Sumvin. The recommended way to use Sumvin is the [MCP connector](/get-started/connect), or the API if you're building (docs coming).
An agent running in your terminal can approve Stamped Mandates that the CLI signs for you. Never give an unattended agent `--yes`.
Every `sumvin` command accepts `--json`. Instead of formatted text, it prints exactly one JSON document to standard output and nothing else — no progress messages, no partial output, nothing on standard error to interleave with it.
## The envelope
**Success:**
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": true,
"exit_code": 0,
"command": "pint get",
"data": { "…": "…" },
"_links": { "…": "…" },
"next": [ { "command": "…", "reason": "…" } ]
}
```
**Error:**
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": false,
"exit_code": 1,
"command": "pint get",
"error": {
"code": "PINT-404-001",
"title": "Mandate not found. Check the ID and try again.",
"detail": "No mandate with that id.",
"status": 404,
"trace_id": "abc-123"
},
"next": [ { "command": "sumvin pint list", "reason": "See your mandates." } ]
}
```
`data` is present only when `ok` is `true`; `error` is present only when `ok` is `false`. `error`'s fields are always present — `null` rather than omitted — so a script never has to check for a missing key before reading one.
## `ok` is not the verdict
`ok` means the command completed — it reached your account, got an answer, and didn't crash. It does **not** mean "the answer is yes."
The clearest example: verifying a Stamped Mandate that turns out to be invalid is a completely successful run whose answer is no.
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": true,
"exit_code": 2,
"command": "pint verify",
"data": { "valid": false }
}
```
**Always read `exit_code` (or the command's own verdict field, such as `data.valid`) for check-style commands. Never branch on `ok` alone.**
## Exit codes
| Code | Meaning |
| - | - |
| `0` | Success. |
| `1` | The run itself failed and decided nothing. |
| `2` | The run succeeded, and the answer is no. Only two commands use this: `sumvin pint verify` and `sumvin kyc status --wait`. |
Every other command uses only `0` and `1`. A bare `sumvin kyc status` or `sumvin sigil show` reports state rather than judging it, so even an unwelcome result — a rejected verification, a failed Sigil — is still a successful read and exits `0`.
## `_links` vs `next`
* **`_links`** — what can be done to this result, translated into a runnable `sumvin` command. Reflects what's actually available right now: it only lists actions your account can currently take.
* **`next`** — what the CLI suggests you do now. Appears on errors (pointing at how to fix the problem) and on results with an obvious follow-up.
A `command` in either field is always a complete, runnable command with real arguments filled in — never a placeholder.
## Discovery
Running `sumvin --json` with no command returns the entire command surface as one catalog: every command, its flags, its arguments, and what each exit code means. An agent can learn the whole CLI from a single call, without reading text written for humans.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin --json | jq '.data.commands'
```
Running `sumvin --help --json` returns just that command's descriptor the same way.
## The stdout/stderr guarantee
Under `--json`, nothing the CLI writes for a human — error lines, progress narration, `--debug` diagnostics — reaches standard error. Everything is either a field of the envelope or suppressed entirely. `sumvin --json 2>&1` is always safe to parse.
Long-running commands (`sumvin kyc status --wait`, `sumvin keys add-to-safe`) print nothing at all until they finish. Silence under `--json` is expected, not a hang.
## An env var, if you'd rather not repeat the flag
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
SUMVIN_CLI_OUTPUT_JSON=1 sumvin login
```
An explicit `--json` on a single call always wins over the environment variable.
## Credential safety
`--json` never emits a signing key or an access token. `sumvin login --json` returns the token's id and expiry, never the token; `sumvin whoami --json` returns token metadata, never the token; `sumvin keys add-to-safe --json` returns the registered address, never the key.
**Try asking your agent:** "Run `sumvin --json` and tell me what commands are available."
# sumvin keys
Source: https://docs.sumvin.com/reference/cli/keys
Manage the local signing key you use to approve mandates and errands yourself.
**The CLI is for developers.** It's mainly for people building and experimenting on Sumvin. The recommended way to use Sumvin is the [MCP connector](/get-started/connect), or the API if you're building (docs coming).
An agent running in your terminal can approve Stamped Mandates that the CLI signs for you. Never give an unattended agent `--yes`.
This group is entirely optional. Without it, Sumvin's agent signs on your behalf whenever a Stamped Mandate is created. You reach for `sumvin keys` when you'd rather approve things yourself, from a key you hold, on a machine you chose.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin keys generate # create the key
sumvin keys add-to-safe # register your key with your account
sumvin keys show # see the key's address and registration status
```
Registering a key with your account can't be undone yet. There's currently no command to remove a registered key — plan for that before you register one.
## `sumvin keys generate`
Creates a signing key and stores it on this machine.
**Flags**
| Flag | Effect |
| - | - |
| `--force` | Replace an existing key. Not reversible — see below. |
| `--json` | Print a result object instead of the formatted view. |
**Output states**
* Success prints the key's address. The key itself is never printed, by this command or any other — only the address, which is safe to share.
* If a key already exists, the command refuses and tells you to use `--force` or run `sumvin keys show` to see the one you have. If your existing key has already been registered with your account, overwriting it without a way to unregister the old one leaves that old key with standing authority and no copy of it on this machine — `--force` proceeds anyway, deliberately.
## `sumvin keys add-to-safe`
Registers your local key's address with your account and waits for the registration to settle.
**Flags**
| Flag | Effect |
| - | - |
| `--timeout ` | How long to wait for the registration to settle. Defaults to 5 minutes. |
| `--json` | Silence the progress narration and print one result once it settles. |
Only the key's public address ever leaves this machine.
**Output states**
* `pending` while the registration is in progress, then a confirmation once it's active.
* Registration can fail. A failed registration is a failed run, not a state to retry blindly — check `sumvin keys show` before trying again.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin keys add-to-safe --json
```
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": true,
"exit_code": 0,
"command": "keys add-to-safe",
"data": {
"address": "0x1234567890abcdef1234567890abcdef12345678",
"signer_status": "active"
}
}
```
## `sumvin keys show`
Shows your local key's address and whether it's registered with your account.
**Flags**
| Flag | Effect |
| - | - |
| `--json` | Print a result object instead of the formatted view. |
**Output states**
* If you've generated a key, its address and registration status (registered, not registered, or unchecked if you're signed out).
* If no key exists yet, the command says so and points at `sumvin keys generate`.
## Exit codes
All three commands use `0` for success and `1` for failure — a registration that fails to settle is a failed run, not a negative answer.
# sumvin kyc
Source: https://docs.sumvin.com/reference/cli/kyc
Verify your identity from the terminal.
**The CLI is for developers.** It's mainly for people building and experimenting on Sumvin. The recommended way to use Sumvin is the [MCP connector](/get-started/connect), or the API if you're building (docs coming).
An agent running in your terminal can approve Stamped Mandates that the CLI signs for you. Never give an unattended agent `--yes`.
Verification is what unlocks the parts of Sumvin that need a verified person behind them. The CLI drives everything except two steps a human has to do: typing a code that arrives by SMS, and photographing a document in a browser.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin kyc start # verify your phone, then open the browser step
sumvin kyc status # where verification has got to right now
sumvin kyc status --wait # block until there's a result
```
Both commands need a signed-in session — see [`sumvin login`](/reference/cli/auth).
`sumvin kyc start` can't be scripted. It needs a browser and a code sent by SMS. Build automation around `sumvin kyc status --json` instead.
## `sumvin kyc start`
Checks where you already are, verifies your phone number if that hasn't happened yet, then opens a single-use browser link for the document step.
**Flags**
| Flag | Effect |
| - | - |
| `--phone ` | Your phone number, in international format. You're still prompted for the SMS code — there's no flag for that. |
| `--json` | Print a result object instead of the running commentary. |
**Example**
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin kyc start --phone +14155551234
```
**Output states**
* A fresh run prompts for your phone (if needed) and the SMS code, then prints a browser link that expires in about 15 minutes.
* Re-running after an interruption picks up where you left off — it skips a phone step you already completed and reuses your existing verification record.
* Already verified: the command tells you so and mints nothing.
* Previously declined: the command refuses. A declined verification can't be retried with the same documents.
## `sumvin kyc status`
Reports where verification has got to. Always exits successfully — it reports state, it doesn't judge it.
**Flags**
| Flag | Effect |
| - | - |
| `--wait` | Poll until there's a result instead of a snapshot. |
| `--timeout ` | How long `--wait` keeps polling. Defaults to 15 minutes. |
| `--refresh` | Ask the verification provider for a live answer. Ignored together with `--wait`. |
| `--json` | Print the result object instead of the formatted view. |
**Example**
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin kyc status --wait --timeout 30m
```
**The five statuses**
| Status | Meaning |
| - | - |
| `pending` | You haven't started. |
| `in_progress` | A verification is under way and hasn't returned a result. |
| `retry` | More information was requested — resubmit with `sumvin kyc start`. |
| `approved` | Verified. |
| `rejected` | Declined. Terminal — the same documents can't be resubmitted. |
Once approved, `sumvin kyc status` also reports whether your Sigil exists yet. "Not started" is a normal, expected answer here — see [Sigil](/reference/cli/sigil).
**Exit codes**
| Invocation | Result | Exit |
| - | - | - |
| `sumvin kyc status` | any status | `0` |
| `sumvin kyc status --wait` | approved | `0` |
| `sumvin kyc status --wait` | rejected, or more information requested | `2` |
| `sumvin kyc status --wait` | timed out with no answer yet | `1` |
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin kyc status --wait --json
```
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": true,
"exit_code": 0,
"command": "kyc status",
"data": {
"status": "approved",
"verified_at": 1754818264000,
"rejected_at": null,
"reject_reason": null,
"sigil": { "did_mint_status": null, "did_token_id": null }
},
"next": [ { "command": "sumvin sigil show", "reason": "See the full picture." } ]
}
```
# Stamped Mandates (sumvin pint)
Source: https://docs.sumvin.com/reference/cli/pint
Create, list, revoke, and verify Stamped Mandates from the terminal.
**The CLI is for developers.** It's mainly for people building and experimenting on Sumvin. The recommended way to use Sumvin is the [MCP connector](/get-started/connect), or the API if you're building (docs coming).
An agent running in your terminal can approve Stamped Mandates that the CLI signs for you. Never give an unattended agent `--yes`.
A Stamped Mandate is a signed authorisation for what an agent may do. This command group creates and manages the spending kind — how much an agent may spend, and until when — because that's what `pint` supports today. The command group is still named `pint` in the CLI — the rename to match the public name hasn't landed yet.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint create # create a Stamped Mandate
sumvin pint list # list your Stamped Mandates
sumvin pint get # show one in full
sumvin pint revoke # revoke one
sumvin pint verify # verify a Stamped Mandate token independently
```
## `sumvin pint create`
Creates a Stamped Mandate and prints the token.
**Flags**
| Flag | Required | Effect |
| - | - | - |
| `--scope ` | yes, repeatable | What the mandate grants. Your account or agent tells you the exact value to pass. |
| `--statement ` | yes | Human-readable description of what the mandate authorizes. |
| `--expires ` | no (default `24h`) | How long the mandate is valid — `30m`, `24h`, `7d`, and so on. |
| `--max-amount ` | no (default `0`) | Spend cap. `0` authorizes no spend. |
| `--max-amount-token ` | yes | The currency or token the cap is denominated in. |
| `--sign local` | no | Sign the mandate yourself with a local key instead of letting Sumvin's agent sign it — see [`sumvin keys`](/reference/cli/keys). |
| `--json` | no | Print a result object instead of the printed token. |
**Example**
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint create \
--scope \
--statement "Delegate signing for one hour" \
--expires 1h \
--max-amount 0 \
--max-amount-token
```
**Output states**
* Success prints the mandate's identifier, its token, who it's addressed to, its scopes, and its expiry.
* Missing required flags, or a scope your account isn't verified for, are rejected before anything is created — see the errors below.
## `sumvin pint list`
Lists your Stamped Mandates.
**Flags**
| Flag | Effect |
| - | - |
| `--status ` | Filter by status: `pending`, `active`, `expired`, `revoked`. |
| `--limit ` / `--cursor ` | Paginate results. |
| `--sort ` / `--order ` | Sort by creation or expiry, ascending or descending. |
| `--json` | Print a result object instead of the formatted list. |
## `sumvin pint get`
Shows one Stamped Mandate in full.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint get
```
**Flags**
| Flag | Effect |
| - | - |
| `--expand ` | Include related records inline (repeatable). |
| `--json` | Print a result object instead of the formatted view. |
## `sumvin pint revoke`
Revokes a Stamped Mandate. Idempotent — revoking one that's already revoked succeeds without complaint.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint revoke
```
## `sumvin pint verify`
Verifies a Stamped Mandate token independently — its signature, claims, issuer, expiry, audience, scopes, and revocation status. All seven checks are always shown, so a broken signature is never confused with an expired or revoked token.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint verify ""
```
**Flags**
| Flag | Effect |
| - | - |
| `--audience ` | Compare against a specific audience instead of your own account. |
| `--offline` | Skip the network calls entirely. The audience and revocation checks then report as unverified rather than passing or failing. |
| `--json` | Print a result object instead of the formatted checklist. |
**Output states**
* All seven checks pass: the mandate is valid.
* Any check fails: the mandate is not valid, and the failing check is named — this is a completed, successful run whose answer is "no."
## Exit codes
| Command | `0` | `1` | `2` |
| - | - | - | - |
| `create`, `list`, `get`, `revoke` | succeeded | failed | — |
| `verify` | valid | the check itself couldn't complete | not valid |
`sumvin pint verify` completing and reporting an invalid mandate is exit `2`, not `1` — the run succeeded, and the answer is no. Read the exit code, not just whether the command ran.
## Common errors
| Message | Meaning |
| - | - |
| "Couldn't determine your account's spending identity." | Finish onboarding in the Sumvin app before creating a mandate. |
| "Your account isn't verified yet for this scope." | Complete identity verification first — see [`sumvin kyc`](/reference/cli/kyc). |
| "Mandate not found." | Check the ID, or run `sumvin pint list`. |
# sumvin sigil
Source: https://docs.sumvin.com/reference/cli/sigil
Check the state of your Sigil from the terminal.
**The CLI is for developers.** It's mainly for people building and experimenting on Sumvin. The recommended way to use Sumvin is the [MCP connector](/get-started/connect), or the API if you're building (docs coming).
An agent running in your terminal can approve Stamped Mandates that the CLI signs for you. Never give an unattended agent `--yes`.
Shows the state of your Sigil — your verified agent identity. This is a read-only group: a Sigil is issued automatically once verification finishes, so there's nothing to trigger here, only to check.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin sigil show
```
Needs a signed-in session — see [`sumvin login`](/reference/cli/auth).
**Flags**
| Flag | Effect |
| - | - |
| `--json` | Print a result object instead of the formatted view. |
## The six states
| State | Meaning |
| - | - |
| `not_verified` | You haven't completed identity verification yet. |
| `not_started` | You're verified, but nothing has started yet. This is a normal, common answer — it doesn't mean anything is wrong. |
| `in_flight` | Your Sigil is being issued. Check back shortly. |
| `failed` | Issuing didn't complete after repeated attempts. This needs support's attention, not another retry. |
| `minted` | Your Sigil exists. |
| `unknown` | This CLI build doesn't recognise the state your account returned. Update the CLI, or check the Sumvin app. |
**Example**
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin sigil show --json
```
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"ok": true,
"exit_code": 0,
"command": "sigil show",
"data": {
"state": "minted",
"kyc_status": "approved"
}
}
```
`data.state` is the field to branch on — read it rather than the prose.
## Exit codes
`0` for any of the six states above, including `failed` — reading your Sigil's state is a successful read even when the state itself isn't good news. `1` if you're not signed in or the read itself couldn't complete.
See [Sigil](/concepts/sigil) for what a Sigil is and why it matters, and [`sumvin kyc`](/reference/cli/kyc) for the verification a Sigil depends on.
# Errors
Source: https://docs.sumvin.com/reference/errors
Every error the Sumvin MCP connector can return, what it means, and what to do about each one.
Every error Sumvin returns carries a short code, so an agent or a person can tell one failure from
another without parsing prose. This page covers the codes you can meet through the MCP connector
and the account setup it depends on. It isn't the full list Sumvin's systems use internally.
## Reading a code
A code has three parts, and sometimes a fourth: `DOMAIN-STATUS-SEQUENCE`, then `-R` if the failure is
worth retrying.
* `MCP-401-001`: the area (`MCP`), the HTTP status it came with (`401`), and which failure in that
area (`001`). No `-R`, so it's **terminal**: sending the same request again gets the same answer.
Something has to change first: the credential, the input, a setting.
* `MCP-429-001-R`: ends in `-R`, so it's **retryable**: the same request, sent again after a short
wait, can succeed without anything changing. Honour `retry_after` when the error carries one, and
back off between attempts.
That's the whole rule, so a client can decide without a lookup table: retry when the code ends in
`-R`, stop otherwise.
## What an error looks like
A tool call that fails comes back as an error result. Its text leads with a message written for the
agent, followed by the same error as JSON, and the JSON is also the result's structured content:
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"code": "KYC-503-002",
"title": "Verification at daily capacity",
"detail": "Identity verification has reached today's capacity. Wait for the daily allowance to reset at 00:00 UTC.",
"retryable": false,
"retry_after": 5400,
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"_links": {
"docs": {
"href": "https://docs.sumvin.com/reference/errors#kyc-503-002"
},
"sumvin:skill": {
"name": "sumvin-sigil",
"href": "skill://sumvin-sigil/references/errors.md#limits",
"tool": "sumvin_guide",
"args": {
"name": "sumvin-sigil",
"file": "references/errors.md"
},
"why": "Which limits exist and what to tell the person."
},
"next": {
"tool": "kyc_start",
"args": {}
},
"support": {
"href": "https://docs.sumvin.com/support"
}
}
}
```
* `code`, `title` and `detail` say what went wrong. `detail` is specific to this call.
* `retryable` is `true` exactly when the code ends in `-R`. `retry_after` is the number of seconds to
wait when the error names one, and `null` otherwise.
* `trace_id` identifies this call in Sumvin's logs. Quote it with the code if you
[contact support](/support).
* `_links.docs` is this code's entry on this page. `_links["sumvin:skill"]` names the guide to load,
with the `sumvin_guide` call that loads it. `_links.next`, when present, is the call most likely
to move things on. `_links.support` is how to reach Sumvin.
### Refused before the tool runs
Some refusals happen at the connection, before any tool runs, and arrive as an HTTP status with a
problem body rather than as the error above. Each has its entry below:
* [`MCP-401-001`](#mcp-401-001)
* [`MCP-429-001-R`](#mcp-429-001-r)
* [`MCP-429-002-R`](#mcp-429-002-r)
* [`MCP-503-001`](#mcp-503-001)
The rest of this page groups the codes by the kind of fix they call for.
## Retryable
Nothing about the request was wrong. Wait for `retry_after` seconds when it is set, or a few seconds when it is not, then send the same call again. Back off between attempts and stop after a few.
### `KYC-429-003-R`
**Too many verification links this hour.** The person has asked for as many verification links as an hour allows. Have them finish with the latest link if it is still open; otherwise wait for `retry_after`.
### `KYC-503-003-R`
**Verification briefly unavailable.** Identity verification could not confirm its capacity just now. Wait for `retry_after` and call again.
### `MCP-429-001-R`
**Too many requests from this connection.** Wait for the `Retry-After` time, then send the same request again.
### `MCP-429-002-R`
**Too many requests from this network address.** Wait for the `Retry-After` time, then send the same request again.
### `MCP-503-002-R`
**Guide temporarily unavailable.** The guide could not be loaded just now. Try again in a minute, and carry on without it if it is still missing.
### `MCP-503-003-R`
**A service Sumvin depends on is busy.** A service behind this tool timed out or asked Sumvin to slow down. Wait a few seconds and send the same call again.
### `MCR-503-002-R`
**Approval check briefly unavailable.** The approval could not be checked just now. It clears on its own; check the outcome with `mandate_status` in a minute.
### `UST-503-001-R`
**Strategy schedule not updated.** The schedule could not be saved just now. Wait a moment and send the same call again.
### `WAL-409-002-R`
**Primary wallet changed at the same time.** Another change to the primary wallet landed first. Wait a moment and send the same call again.
### `WAL-409-003-R`
**Wallet still being set up.** The smart wallet is still being set up. Wait a minute and send the same call again.
## Auth
This connection has no credential Sumvin accepts. Ask the person to connect their Sumvin account to this app; retrying the call will not change the answer.
### `MCP-401-001`
**Account not connected.** This connection has no Sumvin account behind it. Ask the person to connect their Sumvin account to this app, then carry on with what they asked.
### `MCR-401-001`
**Approval needs a signature.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
### `MCR-401-002`
**Approval signer not recognised.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
### `MCR-401-003`
**Approval signer not registered.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
### `MCR-401-004`
**Approval signer does not own the wallet.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
### `MCR-401-005`
**Approval signature invalid.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
## Limits
A limit was reached. Tell the person when it resets, using `retry_after` when it is set, and do not try again before then.
### `KYC-429-002`
**Daily verification attempts used.** The person has used today's identity-verification attempts. Tell them when it resets, from `retry_after`, and do not try before then.
### `KYC-503-002`
**Verification at daily capacity.** Sumvin has reached today's identity-verification capacity. Tell the person when it resets, from `retry_after`, and do not try before then.
## Verification
The person has to verify something about themselves first, such as their phone number or their identity. Follow `next` when it is set; the guide explains each step.
### `KYC-403-001`
**Identity not verified yet.** This needs the person's identity verified. If they have already submitted, it is being reviewed; check their status rather than asking them to submit again.
Next: call `onboarding_status`.
### `KYC-403-002`
**Identity verification not started.** This needs the person's identity verified, and they have not started. Hand them their verification link.
Next: call `kyc_start`.
### `KYC-403-004`
**Phone number not verified.** Identity verification needs a verified phone number first. `kyc_start` takes the person through both, phone first.
Next: call `kyc_start`.
### `KYC-403-005`
**Identity verification declined.** The reviewer decided not to verify this person, and that decision is final. Tell them, and point them to support if they believe it is wrong.
### `KYC-503-001`
**Verification not set up here.** Identity verification is not configured in this environment. Nothing the person does will change that, so do not retry.
## Permission
The credential behind this connection does not allow this. Do not retry with different arguments; the detail says what is missing and how the person can grant it.
### `MCP-403-001`
**Connection does not allow this.** The person did not grant this connection the access this tool needs. The detail names it; the person grants it by reconnecting this app with that access. Use a tool the connection already covers meanwhile.
### `MCP-403-002`
**Tool not available on this connection.** This connection does not serve that tool. Read the detail: it says whether the person needs an account first, or manages this in the Sumvin app.
### `MCP-403-003`
**Account cannot be served.** The person's account cannot be used for this right now. The detail says why; relay it, and point them to support if they think it is wrong.
### `MCP-403-004`
**No authorization for this action.** Nothing authorizes this agent to take this action for the person. Do not retry; ask the person whether they want to approve it.
### `MCR-403-001`
**Mandate request belongs to someone else.** That request was made to a different person. Do not act on it.
### `PINT-403-001`
**Approval does not cover this.** The person's approval for this agent does not include what this tool needs. Do not retry with other arguments; ask the person to approve the access the detail names.
### `PINT-403-007`
**Asks for more than its parent allows.** This asks for more than the mandate it draws on authorises. Ask for less, or ask the person for a wider mandate.
### `PINT-403-008`
**Parent mandate no longer usable.** The mandate this draws on has expired, been withdrawn or cannot be spent against. Read the person's mandates before going further.
Next: call `pint_query`.
### `WAL-403-006`
**Signing key cannot be primary.** That wallet is the key the account signs with, and cannot also be its primary wallet. Choose another wallet.
## Validation
The arguments were not accepted. Read the detail, correct the arguments it names, and call again. Ask the person when the right value is not yours to guess.
### `BUD-400-003`
**Budget period missing.** A budget of this kind needs a period. Include one and call again.
### `BUD-400-005`
**Budget period too long.** The period is longer than a budget allows. Shorten it and call again.
### `CRD-400-001`
**Card change not possible.** The card cannot move to that state from the one it is in. The detail says why; read the card before trying something else.
### `GEN-400-001`
**Request not valid.** The tool could not use what it was given. The detail says what to change.
### `IPA-422-003`
**Errand needs a spend limit.** An errand needs a spend limit the person has agreed to. Ask them for one, then create the errand with it.
### `MCP-422-001`
**Invalid tool arguments.** The arguments do not match the tool's schema. The detail names each field and what is wrong with it; correct them and call again.
### `MCP-422-002`
**Tool arguments not accepted.** The arguments were well formed but the tool could not use them, for example an unknown category. The detail lists what it accepts.
### `MCR-400-002`
**Approval signature unreadable.** This is answered on the person's approval page, not to you. Tell them what it says and let them finish there; check the outcome with `mandate_status`.
### `MCR-422-001`
**Mandate request not accepted.** The request could not be turned into something the person can approve. The detail says which part; fix it with the person and ask again.
### `MCR-422-002`
**Mandate names the wrong wallet.** The request names a wallet that cannot hold this mandate. Check the person's wallets and ask with the right one.
Next: call `wallet_query`.
### `MCR-422-003`
**Mandate network not supported.** Mandates cannot be issued on that network. Ask with a supported one.
### `PINT-422-003`
**Mandate contradicts how it was signed.** The mandate's contents and the way it was signed disagree. Do not retry it; ask the person to approve a new request.
### `PINT-422-004`
**Mandate cannot be stated.** Sumvin could not state what this mandate would authorise, for example because a limit or currency is missing. The detail says which; fix it and ask again.
### `PINT-422-006`
**Approval has expired.** The approval this draws on has expired, so nothing can be issued under it. Ask the person whether they want to approve a new request.
Next: call `pint_query`.
### `UST-400-001`
**Strategy needs a service connection.** This strategy needs a service connection the person has not set up. They set it up in the Sumvin app first.
### `WAL-400-001`
**Not a wallet address that can be used.** That address cannot be used here; only a wallet the person signs with directly can. Check the address with the person.
### `WAL-400-004`
**Primary network cannot change.** The primary wallet's network is fixed once set. Leave it as it is.
### `WAL-400-005`
**Network not supported for wallets.** A smart wallet cannot be set up on that network. Use a supported one.
### `WAL-400-006`
**Nothing to update.** The call named no change to make. Include the field you mean to change.
### `WAL-400-007`
**Primary wallet cannot be demoted.** A primary wallet stops being primary only when another one is promoted. Promote the wallet the person wants instead.
## Not found
Nothing by that name or identifier is visible to this connection. Check the identifier against a query tool's results before trying again.
### `BUD-404-001`
**Budget not found.** No budget with that identifier is visible here. List the person's budgets to find the right one.
### `CON-404-001`
**Connection to a service not found.** The service connection this needs is not set up for the person. They set it up in the Sumvin app.
### `CRD-404-001`
**Card not found.** No card with that identifier is visible here. List the person's cards to find the right one.
### `IPA-404-001`
**Errand not found.** No errand with that identifier is visible to this connection. List the person's errands to find the right one.
Next: call `errand_query`.
### `MCP-404-001`
**Unknown tool.** No tool has that name. Check it against `tools/list`, which returns everything this connection can call.
### `MCP-404-002`
**Unknown guide.** No guide has that name. The detail lists the guides there are.
Next: call `sumvin_guide` with `{"name": "sumvin-sigil"}`.
### `MCP-404-003`
**Unknown guide file.** That guide has no file by that name. The detail lists its files; omit the file to load the guide itself.
Next: call `sumvin_guide` with `{"name": "sumvin-sigil"}`.
### `MCR-404-001`
**Mandate request not found.** No mandate request with that identifier is visible to this connection. Read the person's requests to find the right one.
Next: call `mandate_status`.
### `PINT-404-001`
**Mandate not found.** No mandate with that identifier is visible to this connection. List the person's mandates to find the right one.
Next: call `pint_query`.
### `RUL-404-001`
**Rule not found.** No rule with that identifier is visible here. List the person's rules to find the right one.
### `RUN-404-001`
**Strategy run not found.** No run with that identifier is visible here. List the strategy's runs to find the right one.
### `STR-404-001`
**Strategy not found.** No strategy with that identifier is visible here. List the strategies to find the right one.
### `WAL-404-001`
**Wallet not found.** No wallet with that identifier is visible to this connection, or the account has no smart wallet yet. List the person's wallets to check.
Next: call `wallet_query`.
## Conflict
The thing you are acting on is not in a state that allows this. Read its current state with the matching query tool, then decide with the person what to do; sending the same call again will not help.
### `MCP-409-001`
**Not ready for this action.** What you are acting on is not waiting for this action right now. Read its current state before deciding with the person what to do.
Next: call `errand_query`.
### `MCP-409-002`
**Cannot be cancelled now.** It has gone too far to cancel. Read its current state and tell the person what that means for them.
### `MCR-409-001`
**Mandate request already answered.** The person has already answered this request. Read what they decided.
Next: call `mandate_status`.
### `MCR-409-002`
**Mandate no longer available.** The mandate behind this request can no longer be issued. Read the request's state and talk to the person about what they want.
Next: call `mandate_status`.
### `MCR-409-003`
**Too many open mandate requests.** The person already has as many requests waiting as are allowed. Read them, and let the person answer one before you ask for another.
Next: call `mandate_status`.
### `MCR-410-001`
**Mandate request expired.** The person did not answer in time, so the request lapsed. Ask them whether they still want it before you request it again.
### `PINT-409-002`
**Mandate already withdrawn.** That mandate was already withdrawn, so there is nothing more to do to it.
Next: call `pint_query`.
### `STR-409-001`
**Strategy already on.** The person already has this strategy. There is nothing more to do.
### `USR-424-002`
**Account setup not finished.** The person's account has no active signing key yet, so it cannot act. Check where their setup stands.
Next: call `onboarding_status`.
### `UST-409-001`
**Strategy not active yet.** The strategy is still being switched on. Read it again shortly.
## Server
Something failed on Sumvin's side and retrying is not expected to help. Tell the person, and quote the `code` and `trace_id` if they contact support.
### `MCP-500-001`
**Tool call failed.** The tool failed for a reason it cannot share. Do not retry it in a loop; tell the person, and quote the code and trace id if they contact support.
### `MCP-500-002`
**Unrecognised refusal.** The tool refused with a reason this connection could not classify. The detail is what the tool said; tell the person, and quote the code and trace id if they contact support.
### `MCP-503-001`
**Connector not set up here.** The connector is not configured in this environment. Nothing you or the person can change will fix it, so do not retry.
### `MCR-500-001`
**Approval checking misconfigured.** Sumvin cannot check approvals because a setting is wrong. Nothing the person does will fix it; tell them.
### `MCR-503-001`
**Mandates not set up here.** The approval page is not configured in this environment, so no mandate can be requested. Do not retry.
### `SYS-503-002`
**Service not configured.** Sumvin could not authorize this request because the service is misconfigured. No argument change will help; tell the person.
# Rate limits
Source: https://docs.sumvin.com/reference/mcp/limits
How the MCP connector limits requests, and how to handle a 429.
The MCP connector limits how many requests it serves in a given window. There are two separate
allowances, and either one can be reached independently:
* **Per network address** — counted before any agent is identified, so a flood from one address is
capped even before it authenticates.
* **Per connected agent** — counted after the agent is identified, so several agents sharing a network
address don't share this allowance, and one agent moving between addresses still has a single
budget.
Both are configured by Sumvin's operators and can change without notice, so this page doesn't publish
exact numbers — they aren't fixed values you can rely on staying the same.
## When you're rate limited
A request over either allowance gets back an HTTP `429` with a `Retry-After` header, given in seconds.
Wait at least that long before trying again — retrying sooner gets the same refusal.
```
HTTP/1.1 429 Too Many Requests
Retry-After: 30
```
The two allowances carry different error codes so an agent can tell which one it hit — see
[Errors](/reference/errors) for `MCP-429-001-R` and `MCP-429-002-R`.
A `429` on this connector is availability protection, not a judgement about the request. Back off for
the stated time and try again — there's nothing else to change about the call itself.
# Refusals
Source: https://docs.sumvin.com/reference/mcp/refusals
What it means when a connected agent's tool call is refused, and what to do next.
A connected agent's tool set is fixed (see [MCP tools](/reference/mcp/tools)), so calling something
outside it always fails the same way: the tool is refused, not silently dropped. Every refusal names
which of a small number of true reasons applies, and what to do about it. This page is for an agent
reading a refusal it got back — it explains what each one means and what to try next.
Most refusals come back as a normal tool result, not as a connection error. Read the result's text for
the reason and the next step, rather than treating any error as the same kind of problem. The same result
carries the [error object](/reference/errors#what-an-error-looks-like): its `code` has its own entry on
the [Errors](/reference/errors) page, and its `_links` name the guide to load and the next call to make.
## The four refusals
A connected agent's tools come from a small, named set. A call outside what this connection can use is
refused for one of four distinct reasons — the reason matters, because "connect the account", "hand
the person this sign-up link" and "ask the account holder to open the app" are different instructions.
The connector has been added but no Sumvin account is connected to it yet. Tools are listed before
anyone signs in, so a client can reach for one first.
**What to do:** nothing in the conversation — the client is prompted to connect the account and
asks the person to sign in or create a Sumvin account. Once they have, call the tool again.
Code: [`MCP-401-001`](/reference/errors#mcp-401-001).
The name called doesn't match anything this server serves, on any connection. Most often a typo.
**What to do:** check the name against `tools/list`, which returns everything this connection can
actually call. Don't guess a corrected name and retry blind.
Code: [`MCP-404-001`](/reference/errors#mcp-404-001).
The tool exists and the account holder can use it, but not from a connected agent — it's held for
the Sumvin app or the CLI. No scope or credential makes it appear here.
**What to do:** don't retry with different arguments. Tell the account holder to do this themselves,
in the app or the CLI. Call `tools/list` to see what this connection does serve instead.
Code: [`MCP-403-002`](/reference/errors#mcp-403-002).
The tool needs an account, and the person being helped doesn't have one. Someone with no account
is listed the same tools as everyone else, so this is the refusal any of them returns.
**What to do:** the refusal carries a sign-up link — hand it to the person to open in their
browser. Once they've signed up they can come back to the same conversation and carry on; the
page they finish on also gives them a prompt to start a new conversation, if they'd rather.
Don't retry tools until they say they've signed up. If the refusal carries no link, it points
them at the Sumvin website instead.
Code: [`MCP-403-002`](/reference/errors#mcp-403-002).
## Verify first
A few tools need more than an account — they need a wallet set up, or the account holder's identity
verified, before they'll do anything. [`mandate_request`](/reference/mcp/tools#mandate_request) is
the clearest example: it checks these before asking the account holder to sign anything, so an
unfinished setup never wastes their time on a request that couldn't be granted.
A Stamped Mandate is granted against a wallet, and this account doesn't have one.
**What to do:** tell the account holder to finish setting up their account, then ask again. Check
[`onboarding_status`](/reference/mcp/tools#onboarding_status) for where they've got to.
Code: [`WAL-404-001`](/reference/errors#wal-404-001).
Spending authority needs the account holder's identity to be verified, and verification hasn't
started.
**What to do:** call [`kyc_start`](/reference/mcp/tools#kyc_start) to get the link, and ask
them to complete it. Then try again.
Code: [`KYC-403-002`](/reference/errors#kyc-403-002).
Verification is underway but hasn't completed yet. Nothing has gone wrong.
**What to do:** check back with [`onboarding_status`](/reference/mcp/tools#onboarding_status) once
the account holder says they're done, then ask again.
Code: [`KYC-403-001`](/reference/errors#kyc-403-001).
## Rate limited
Too many requests in a short window gets a refusal with a wait time attached, not one of the reasons
above. See [Limits](/reference/mcp/limits) for how to read it.
## General principle
A refusal that names a true, specific reason is more useful to an agent than a bare failure — so read
it before deciding what to try next. If a refusal says not to retry, don't: retrying gets the same
answer until the underlying thing (an account, a wallet, a verified identity) changes. Every code an
agent can meet, grouped by the kind of fix it needs, is on the [Errors](/reference/errors) page.
# MCP tools
Source: https://docs.sumvin.com/reference/mcp/tools
Every tool a connected agent gets on the Sumvin MCP connector, grouped by what it's for.
A connected agent doesn't see everything Sumvin can do. It sees a fixed set of tools, chosen for the
account holder's own errands and identity — never budgets, card controls, or anything that isn't the
agent's to touch. The account holder manages those in the Sumvin app or the CLI instead.
These tools are visible before an account is connected, so a client can list them as soon as the
connector is added. Each tool declares the OAuth scope it needs, and a client uses that to ask the
person to connect their Sumvin account the first time a tool needs it. Calling a tool before then
returns a refusal that asks the client to connect the account.
Someone with no Sumvin account yet sees the same tools, but every one of them needs an account first.
Calling any of them returns a refusal that carries a sign-up link to hand to the person — see
[Refusals](/reference/mcp/refusals).
Each tool is either **Read** (returns information, changes nothing) or **Write** (starts or changes
something). Most tools work for anyone with an account; a few need the account holder's identity to
be verified first — those are marked below.
## Getting started
### onboarding\_status
Read
Show where the account holder has got to in setting up their account: which steps are done,
which are next, and how far the key they sign with has got. Reading it changes nothing.
Each stage reads `current` (the one to do next), `pending` (still ahead), `submitted` (handed
in and waiting on a review), `completed`, or `skipped` (waived for this account). The identity
stage can also read `retry`: the reviewer asked for the ID capture again. The account holder
retakes it on the verification page they already have open, and their phone stays confirmed. If
that page is closed, call `kyc_start` for a fresh link.
Or the identity stage can read `rejected`: the reviewer has made a final decision not to verify
the account holder. Trying again won't change it, so your agent doesn't call `kyc_start` for
them. It tells them plainly that their identity couldn't be verified and points them to Sumvin
support. `recorded_at` is when the decision was made, and it stays final unless a reviewer later
approves them.
Each stage also carries `recorded_at`: when it reached its status, in epoch milliseconds. It's
null while the stage is open, or when no time was recorded.
While the account holder still owes a signing key, the answer carries `signing_key_url`: the
browser page where they create and bind that key themselves. Your agent hands it to them to
open; there's nothing it can call to bind the key on their behalf.
**Try asking your agent:** "Where am I up to with Sumvin?"
### kyc\_start
Write
Get the link that starts identity verification, and hand it to the account holder. It's a single
browser trip: they verify a phone number, then their identity, in one sitting. If they're already
verified, this says so instead of handing over a link.
If the reviewer has made a final decision not to verify the account holder, the call is refused
with the code `KYC-403-005` and no link is made. Starting again won't change the decision, so your
agent tells them plainly and points them to Sumvin support instead of retrying.
**Try asking your agent:** "Help me get verified with Sumvin."
## Identity
### sigil\_show
Read
Show the account holder's Sigil — their verified identity. Reports whether it's been issued yet,
is being issued right now, or hasn't started. Not having one yet is a normal answer, not a problem.
**Try asking your agent:** "Show me my Sigil."
### agent\_identity\_show
Read
Show this connection's own identity: its registration status, and whether it's linked to the
account holder's Sigil yet. Answers only for the connection asking — it can't see any other agent
the account holder has connected.
**Try asking your agent:** "What's your identity under my Sigil?"
## Stamped Mandates
A Stamped Mandate is a signed authorisation for what one agent may do. These tools cover the
spending kind — how much an agent may spend, and until when — because that's what the mandate
tools support today. These tools still show their internal name, `pint`, in two places: it's
the same object as a Stamped Mandate, and the rename is in progress.
### mandate\_request
Write · Needs verification
Ask the account holder to authorise spending, and get the link that lets them review and sign it.
This doesn't grant anything by itself — only the account holder can sign, on their own page, and
an agent can't do that for them. Needs a wallet and a verified identity first; if either is
missing, this says which one and where to go next.
**Try asking your agent:** "Set up a spending limit of \$50 for groceries this week."
### mandate\_status
Read
Check what the account holder decided about a mandate that was requested: still waiting, signed,
declined, or expired. There's no notification when they decide — call this again after giving them
time to look.
**Try asking your agent:** "Did I approve that spending limit yet?"
### mandate\_verify
Read
Check a Stamped Mandate someone else has presented: what it permits, up to how much, and whether
it still stands. Use this before acting on a mandate a counterparty shows you — an identifier alone
proves nothing, and a mandate can be withdrawn at any time after it's granted.
**Try asking your agent:** "Check whether this mandate is still valid before we go ahead."
### mandate\_capacity\_query
Read
Read what's left to spend under the Stamped Mandate authorising an errand: its limit, what's
settled, what's held by spending in flight, and what remains.
**Try asking your agent:** "How much is left on the mandate for my grocery errand?"
### pint\_query
Read
List the Stamped Mandates issued to the account holder. Filter by status or by identifier.
**Try asking your agent:** "Show me all my active spending limits."
### pint\_revoke
Write
Permanently withdraw a Stamped Mandate. This can't be undone — anything relying on it to spend
stops being able to, immediately. Find the one to revoke with `pint_query` first.
**Try asking your agent:** "Revoke the spending limit I set up for groceries."
## Payment methods
An errand pays with the card the account holder has added. These tools let an agent check that a
card is ready and, when it isn't, send the account holder to the page where they add one. The card
is always entered on that page, by the account holder — never in the conversation.
### payment\_method\_query
Read
List the cards the account holder has added and whether each is ready. A card can pay for an
errand only once it shows as verified. Card numbers are never shown — only the last four digits.
**Try asking your agent:** "Is my card ready for purchases?"
### payment\_method\_add
Read
Get the link to the page where the account holder adds the card their errands pay with, along
with the cards they already have. The tool changes nothing itself. A card can be added once
identity verification has finished; wait for it to show as verified before starting an errand.
**Try asking your agent:** "I need to add a card before you buy anything for me."
## Errands
An errand is a long-running purchase task the account holder asked their agent to make, run within a Stamped Mandate.
### errand\_create
Write
Start a new errand from what the account holder asked for, in their own words, with the most
they'll spend: `constraints.max_total` (whole purchase) or `constraints.max_price` (per item),
plus `constraints.currency`. Without a spend limit the errand isn't created
(`spend_limit_required`). Starting an errand doesn't authorise any spending on its own: the
account holder asks, signs, and only then does Sumvin search, check the merchant, watch for
their conditions, and settle the purchase.
**Try asking your agent:** "Reorder my usual coffee beans."
### errand\_action
Write
Act on an errand that's already running: answer a question it's waiting on, or cancel it.
Signing an errand isn't available here — only the account holder can sign it, before it
searches, on their own page. A pick found within the bounds they signed is then bought without
asking them again.
**Try asking your agent:** "Cancel that coffee order — I changed my mind."
### errand\_query
Read
Read the account holder's errands: what they are, and where each one stands. Filter by status or
by identifier.
**Try asking your agent:** "What's the status of my coffee order?"
### errand\_events\_query
Read
Read one errand's history: what happened, and when, oldest first.
**Try asking your agent:** "What's happened so far with my coffee order?"
## Account
### account\_query
Read
Read the account holder's linked accounts.
**Try asking your agent:** "What accounts do I have linked?"
### wallet\_query
Read
Read the account holder's wallets.
**Try asking your agent:** "Show me my wallets."
### transaction\_query
Read
Read the account holder's transactions: what they spent, where, and when.
**Try asking your agent:** "What have I spent this week?"
### asset\_query
Read
Read reference data for the currencies and assets Sumvin supports. Not scoped to the account
holder — this is general information, not a personal record.
**Try asking your agent:** "What currencies does Sumvin support?"
## Server
### server\_info
Read · No account needed
Report the build identity of the Sumvin server this connection has reached. Useful for confirming
the connection is live before trying anything else.
**Try asking your agent:** "Are you connected to Sumvin?"
### sumvin\_guide
Read · No account needed
Load a Sumvin guide: what Sumvin is, how to use its tools safely, and the reference file for each
task. Pass `name` (`sumvin-sigil`, or `sumvin-sigil-onboard` for someone without an account yet),
and `file` for one of its reference files. It's the same content a host that supports skills
installs from `skill://sumvin-sigil`, so a host that only calls tools still gets it.
**Try asking your agent:** "How do you use Sumvin?"
## In the Sumvin app and CLI
These tools answer only when the account holder's own Sumvin app or the Sumvin CLI calls the
server. A connected agent never sees them: calling one from a connector is refused, and the refusal
says to manage it in the app or the CLI instead.
### allocations\_query
Read
List what has been spent against an errand's mandate, newest first, and whether each spend has
settled.
### budget\_query
Read
Read the account holder's budgets, by name, type, or the kind of spending they cover.
### budget\_upsert
Write
Create a budget, or replace one in full.
### budget\_delete
Write
Delete a budget. It stops counting spending; its history is kept.
### card\_query
Read
Read the account holder's cards and their current status.
### card\_action
Write
Freeze a card, or unfreeze it. Neither cancels the card or touches payments already made.
### rule\_query
Read
Read the account holder's rules.
### rule\_upsert
Write
Create a rule, or replace one in full.
### rule\_delete
Write
Delete a rule. It stops applying from then on.
### strategy\_list
Read
List the strategies the account holder can enrol in.
### user\_strategy\_query
Read
Read the strategies the account holder is enrolled in.
### user\_strategy\_create
Write
Enrol the account holder in a strategy, after checking that whatever it needs is in place.
### user\_strategy\_update
Write
Pause or restart an enrolment, or adjust its settings.
### user\_agent\_task\_query
Read
Read the runs queued or completed for the account holder's strategies.
### user\_agent\_task\_manage
Write
Queue a strategy run, or cancel one that hasn't started.
### wallet\_update
Write
Rename a wallet, or make it the account holder's main one.
# Status labels
Source: https://docs.sumvin.com/reference/status-labels
What Live, Private preview and Coming soon mean on a documentation page.
Every page in this documentation carries one of three labels, so you always know what to expect before
you rely on it.
Working today, as documented. You can connect to it, call it, or follow it right now.
Rolling out. The page describes what it does or will do, but it may not be available on your
account yet, and details can still change before it's generally available.
Not part of this preview. The page describes what's on the way so you can plan for it. Payments are
coming soon in a fast-follow release, with its own waitlist. An errand goes ask, sign, search, check
the merchant, watch, settle: signing from the connector and the settle step arrive with payments.
A page with no label anywhere on it is Live — the label only appears where it changes what you should
expect.
If a Private preview page doesn't work the way you'd expect on your account, that's the label doing
its job, not a bug to report yet.
# List and disconnect agents
Source: https://docs.sumvin.com/sdk/agents/agent-identities
List the agents connected under a person's Sigil and disconnect one.
**Private preview:** this is rolling out and may not be available on your account yet.
**First-party only today.** These calls need a personal access token, which isn't yet open to third-party apps. See [Sign in](/sdk/agents/sign-in).
Each agent a person connects gets its own [agent identity](/concepts/agent-identities) under their Sigil. The person can see every one and disconnect any of them.
Only the person can do this. A request made on their behalf, such as one carrying a Stamped Mandate token, is refused, so one agent can't discover or disconnect another, or itself.
## List connected agents
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { listAgentIdentities, unwrap } from "@sumvin/sdk";
const { data } = unwrap(await listAgentIdentities({ client }));
for (const agent of data) {
console.log(agent.id, agent.client_id, agent.status, agent.created_at);
}
```
| Field | Means |
| - | - |
| `id` | The agent identity's identifier. Use it to disconnect. |
| `client_id` | The application the agent was connected through. |
| `status` | `pending` while it is being set up, `active` when live, `failed` if setup didn't finish, `retired` once disconnected. |
| `generation` | How many times this application has been connected. It goes up each time it is connected again after a disconnect. |
| `created_at` | When it was connected, in epoch milliseconds. |
## Disconnect an agent
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { revokeAgentIdentity } from "@sumvin/sdk";
const { error } = await revokeAgentIdentity({ client, path: { external_id: agent.id } });
```
The agent stops being honoured straight away, including any access it already held. Disconnecting an agent that is already disconnected changes nothing.
# Errands
Source: https://docs.sumvin.com/sdk/agents/errands
Start and follow purchase errands from the SDK. Not part of this preview.
**Coming soon:** payments arrive in a fast-follow release.
An [errand](/concepts/errands) is a purchase an agent carries out under a Stamped Mandate. The SDK already contains the errand operations, under the older wire name `ipa`: `createIpa`, `listIpas`, `getIpa`, `getIpaEvents`, `clarifyIpa` and `cancelIpa`.
This page will document them when payments arrive. Until then, run errands through the [MCP connector](/get-started/first/errand).
# Sign in as a person
Source: https://docs.sumvin.com/sdk/agents/sign-in
Sign a person in through their browser, get a personal access token, and authenticate SDK calls with it.
**Private preview:** this is rolling out and may not be available on your account yet.
**First-party only today.** Personal access tokens are issued to Sumvin's own apps and approved clients. They are not yet open to third-party apps, so an outside developer can't complete this flow. To give your own agent access to Sumvin, use the [MCP connector](/get-started/connect) or the [CLI](/get-started/cli/install).
Acting for a person takes one credential: a **personal access token**. The person approves it in their own browser; your code never sees their password.
`deviceLogin` needs an unauthenticated client. It calls `onUserCode` once, with the address and short code to show the person, then waits for them to approve.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createSumvinClient, deviceLogin } from "@sumvin/sdk";
const signInClient = createSumvinClient({ baseUrl: "https://api.sumvin.com" });
const credential = await deviceLogin({
client: signInClient,
onUserCode: ({ verificationUri, userCode }) => {
console.log(`Open ${verificationUri} and enter ${userCode}`);
},
});
```
Show the address and code before you do anything else, such as opening a browser, so a person on a remote terminal can still sign in.
`deviceLogin` returns the token and never stores it. Keep `credential.token` wherever you keep secrets.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createSumvinClient, sumvinPat } from "@sumvin/sdk";
const client = createSumvinClient({
baseUrl: "https://api.sumvin.com",
auth: [sumvinPat(() => readStoredToken())],
});
```
`sumvinPat` takes the token, or a function that returns it.
In production, personal access tokens only work from approved Sumvin clients, identified by their user-agent. A request that carries the token from any other client, or with no user-agent, is refused with `403` and code `CLI-403-001`.
## When sign-in fails
Each failure is its own error class, and all of them extend `DeviceLoginError`.
| Error | Means |
| - | - |
| `DeviceLoginExpiredError` | The sign-in request expired, was declined, or was already used. Start again. |
| `DeviceLoginNotFoundError` | The API doesn't recognise this sign-in request. |
| `DeviceLoginConflictError` | Another process already finished this sign-in. |
| `DeviceLoginTimeoutError` | Nobody approved it before it expired. |
To stop waiting, pass an `AbortSignal` as `signal`.
## Carrying a Stamped Mandate
An agent that holds a [Stamped Mandate](/concepts/stamped-mandates) token sends it with `pintToken`. It always travels **alongside** the personal access token, never instead of it: a request with only a mandate token is refused.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createSumvinClient, pintToken, sumvinPat } from "@sumvin/sdk";
const client = createSumvinClient({
baseUrl: "https://api.sumvin.com",
auth: [sumvinPat(() => readStoredToken()), pintToken(() => currentMandateToken())],
});
```
A mandate token only ever narrows what the request may do. It never grants more than the person's own access.
# Read and revoke Stamped Mandates
Source: https://docs.sumvin.com/sdk/agents/stamped-mandates
List a person's Stamped Mandates, read one in full, see who it was issued to, and revoke it.
**Private preview:** this is rolling out and may not be available on your account yet.
**First-party only today.** These calls need a personal access token, which isn't yet open to third-party apps. See [Sign in](/sdk/agents/sign-in).
A [Stamped Mandate](/concepts/stamped-mandates) is a signed authorisation for what an agent may do. The SDK's functions still carry the older wire name, `pint`: `listPints` lists Stamped Mandates.
Every call below reads or changes the signed-in person's own mandates. Nobody else's are visible.
## List mandates
Mandates are listed per wallet, so `listPints` needs the address of the wallet the person's mandates belong to. Get it from `listWallets`: it is the person's primary account wallet.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { listPints, listWallets, unwrap } from "@sumvin/sdk";
const { wallets } = unwrap(await listWallets({ client, query: { is_eoa: false } }));
const wallet = wallets.find((w) => w.is_primary);
if (!wallet) throw new Error("This account has no primary wallet yet.");
const page = unwrap(await listPints({ client, query: { wallet: wallet.address } }));
for (const { pint } of page.data) {
console.log(pint.id, pint.status, pint.scopes);
}
```
A person's account wallet is created after they finish [verification](/get-started/first/verify). Until then, `listWallets` succeeds but returns no account wallet, so they have no mandates to list. The wallet is created in the background, so it may not appear straight after verification; check again later.
| Query | Default | Values |
| - | - | - |
| `wallet` | required | The wallet address. |
| `status` | `active` | `pending`, `active`, `consumed`, `revoked`, `expired`. See [States](/concepts/mandates/verification#states). |
| `sort` | `created_at` | `created_at` or `expires_at`. |
| `order` | `desc` | `asc` or `desc`. |
| `limit` | `20` | 1 to 50. |
| `cursor` | none | The `cursor` from the previous page, when `has_more` is `true`. |
## Read one mandate
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { getPint } from "@sumvin/sdk";
const { pint } = unwrap(await getPint({ client, path: { pint_id: mandateId } }));
```
`mandateId` is the mandate identifier, such as `sr:us:pint:3f9c2a7e5b1d4c8e9a6f0b2d7e4c1a58`. You can only read your own.
To show what a mandate allows in plain numbers, use the [ceiling reader](/sdk/verify/reading-scopes). The scopes themselves are described in [Scopes](/concepts/mandates/scopes).
## See who it was issued to
A mandate can be issued as a token to more than one party. `listPintTokens` lists each one: who it is addressed to, when it was issued, when it expires, and whether it was revoked.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { listPintTokens } from "@sumvin/sdk";
const tokens = unwrap(await listPintTokens({ client, path: { pint_id: mandateId } }));
```
## Revoke a mandate
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { revokePint } from "@sumvin/sdk";
const { error } = await revokePint({ client, path: { pint_id: mandateId } });
```
Revoking is immediate and can't be undone. Every token issued against the mandate stops working with it. Revoking one that is already revoked returns `409` with its current state.
## Requesting a new mandate
A new mandate is always approved by the person, with their passkey, in their own browser. Ask for one through the [MCP connector](/guides/set-limits) or the [CLI](/reference/cli/pint).
# Make a client
Source: https://docs.sumvin.com/sdk/client
Create a Sumvin API client with createSumvinClient and pass it to every operation.
**Private preview:** this is rolling out and may not be available on your account yet.
Every call goes through a client you create once with `createSumvinClient`. Each operation takes that client as `{ client }`.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createSumvinClient, listAgentIdentities } from "@sumvin/sdk";
const client = createSumvinClient({
baseUrl: "https://api.sumvin.com",
auth: [/* see Sign in */],
});
const { data, error } = await listAgentIdentities({ client });
```
## Options
| Option | Required | What it does |
| - | - | - |
| `baseUrl` | Yes | The API address. |
| `auth` | No | How requests authenticate. Leave it out for an unauthenticated client. See [Sign in](/sdk/agents/sign-in). |
| `fetch` | No | Your own `fetch`, for proxies or runtimes where you don't want the global one. |
| `headers` | No | Headers added to every request. |
| `validation` | No | Response validation settings. Validation is on by default. |
| `timeoutMs` | No | A per-request timeout, in milliseconds. No timeout by default. |
## What a call returns
Each operation returns `{ data, error }`. Check `error` before you use `data`, or pass the result to `unwrap`, which returns `data` or throws. See [Errors](/sdk/errors).
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { unwrap } from "@sumvin/sdk";
const identities = unwrap(await listAgentIdentities({ client }));
```
# Handle errors
Source: https://docs.sumvin.com/sdk/errors
Catch every SDK error in one place and branch on its kind, status or error code.
**Private preview:** this is rolling out and may not be available on your account yet.
Every error the SDK throws or returns extends one base class. Catch with `isSumvinError`, then narrow.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { isApiError, isContractDriftError, isSumvinError, unwrap } from "@sumvin/sdk";
try {
const identities = unwrap(await listAgentIdentities({ client }));
} catch (e) {
if (!isSumvinError(e)) throw e;
if (isApiError(e)) {
console.error(e.kind, e.status, e.errorCode);
} else if (isContractDriftError(e)) {
console.error(e.operationKey, e.reason);
}
}
```
| Guard | Means |
| - | - |
| `isApiError` | The request failed: the API answered with an error, or the network or transport failed. |
| `isContractDriftError` | The API answered, but the response didn't match the shape the SDK expects. |
The two never overlap, so check `isSumvinError` first and then each one.
## Kinds of request failure
`ApiError.kind` is one of:
| Kind | Means |
| - | - |
| `problem` | The API returned an error body with an error code. Read `errorCode` and `status`. |
| `http` | An HTTP error with no error body. Read `status`. |
| `network` | The request never got an answer. |
| `abort` | The request was cancelled or timed out. |
| `redirect-refused` | The API answered with a redirect, which the SDK refuses. |
## Branch on codes, not messages
`message` is a diagnostic for developers. Build what your users see from `errorCode`, `status` and `kind`. The codes are listed in [Errors](/reference/errors).
# Install the SDK
Source: https://docs.sumvin.com/sdk/install
Add @sumvin/sdk to a project, check your runtime, and pick the entry point you need.
**Private preview:** this is rolling out and may not be available on your account yet.
```bash bun theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
bun add @sumvin/sdk
```
```bash npm theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
npm install @sumvin/sdk
```
```bash pnpm theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
pnpm add @sumvin/sdk
```
The package ships ESM and CommonJS builds with type definitions. Its only runtime dependency is `zod`.
## Runtimes
| Runtime | Supported |
| - | - |
| Node.js 20 or later | Yes |
| Evergreen browsers | Yes |
| Cloudflare Workers | Yes |
| Bun | Works, but the SDK's own test suite does not yet run under Bun in CI |
## Entry points
| Import | What's in it | Extra packages |
| - | - | - |
| `@sumvin/sdk` | Every API operation, its types and schemas, plus authentication, errors and the ceiling reader. | None |
| `@sumvin/sdk/react` | Query options for TanStack Query. | `react`, `@tanstack/react-query` |
| `@sumvin/sdk/testing` | `fakeFetch`, for tests only. See [Testing](/sdk/testing). | None |
## Next
Point the SDK at the API and choose how it authenticates.
# TypeScript SDK
Source: https://docs.sumvin.com/sdk/overview
What the Sumvin TypeScript SDK does, the two jobs it covers, and who can use each part today.
**Private preview:** this is rolling out and may not be available on your account yet.
`@sumvin/sdk` is the TypeScript SDK for the Sumvin API. It gives you every API operation as a typed function, plus a small layer on top: authentication, one way to handle errors, and a reader for the limits a [Stamped Mandate](/concepts/stamped-mandates) carries.
The SDK is open source at [github.com/Sumvin/sdk-ts](https://github.com/Sumvin/sdk-ts) and published on npm as [`@sumvin/sdk`](https://www.npmjs.com/package/@sumvin/sdk).
## Two jobs
Sign in as a person and read or revoke their Stamped Mandates and connected agents.
An agent shows you a Stamped Mandate. Check it offline and online before you act on it.
## Who can use what today
| Part | Who can use it today |
| - | - |
| **Act for a person** | Sumvin's own apps and approved clients. Personal access tokens are not yet open to third-party apps. If you are building your own agent, connect it through the [MCP connector](/get-started/connect) or the [CLI](/get-started/cli/install) instead. |
| **Check a mandate** | Any site can check a token addressed to its own website, with no key. Checks addressed to an organisation need a key from the developer dashboard, which is by invitation during the preview. These checks are plain HTTP calls and a standard JWT library; the SDK has no verification function yet. The SDK's [ceiling reader](/sdk/verify/reading-scopes) helps you read what a mandate allows. |
## Next
Add the package and check your runtime.
Get a key, then check a mandate offline and online.
# Test your integration
Source: https://docs.sumvin.com/sdk/testing
Run SDK calls in your tests with no network, using fakeFetch from @sumvin/sdk/testing.
**Private preview:** this is rolling out and may not be available on your account yet.
`fakeFetch` is the scripted transport the SDK's own tests run on. Give it the responses you want, pass its `fetch` to your client, and read back the requests your code sent.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createSumvinClient, listAgentIdentities, sumvinPat } from "@sumvin/sdk";
import { fakeFetch } from "@sumvin/sdk/testing";
const f = fakeFetch([{ status: 200, body: { _links: {}, data: [], offset: 0, limit: 20, total: 0 } }]);
const client = createSumvinClient({
baseUrl: "https://api.test",
fetch: f.fetch,
auth: [sumvinPat("pat_test")],
});
const { error } = await listAgentIdentities({ client });
expect(error).toBeUndefined();
expect(f.last().headers.get("x-sumvin-pat")).toBe("pat_test");
```
Authentication, response validation, error handling and link following all run as they would against the API, so a test can check them end to end.
`@sumvin/sdk/testing` is for tests only. It is kept out of the main entry point so it can't end up in a production bundle; don't import it from production code.
# Get a key from the developer dashboard
Source: https://docs.sumvin.com/sdk/verify/api-key
Get an API key from the Sumvin developer dashboard, which is by invitation during the preview, and find your Organisation ID.
**Private preview:** this is rolling out and may not be available on your account yet.
A token an agent addresses to your website needs no key: you check it offline. A key is only for a service that already holds the signed request a person approved, for example for its own app. To [exchange that request](/sdk/verify/quickstart#services-that-already-hold-the-signed-request) and check it online, you need two things from the developer dashboard: an **API key** and your **Organisation ID**.
**Developer dashboard access is by invitation during the preview.** You can't sign up yourself yet.
Once you're invited, sign in at [dash.sis.sumvin.com](https://dash.sis.sumvin.com/sign-in). Your organisation has its own identifier.
Open **Environments** and create one, for example `development`. Every key belongs to an environment, so you need at least one.
Open **API Keys** and select **Create key**. Choose **Standard SIS Key**, pick your environment, and name the key after where it will run, such as `local-dev`.
Copy the key when it is shown. It is shown once; if you lose it, revoke it and create another.
A Standard key can both exchange a Stamped Mandate for a token addressed to you and check it online. A **Public RPC Key** can do neither.
Open **Organisation → Settings**. The **Organisation ID** there is the audience your tokens are addressed to. You pass it to every offline check of a token addressed to your organisation.
Keep the key on your server. Anyone holding it can act as your organisation. Never put it in a browser, a mobile app or source control.
## Use the key
Send it as a bearer token:
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
curl "https://sis.sumvin.com/v0/sis/pint/sr%3Aus%3Apint%3Atest/status" \
-H "Authorization: Bearer $SUMVIN_API_KEY"
```
A `404` with code `PINT-404-001` here means the key works: no mandate with that identifier has a token addressed to your organisation. A `401` or `403` means the key or its permissions are wrong; see [Checks and errors](/sdk/verify/checks#errors).
## Next
Check a Stamped Mandate, and exchange a signed request you already hold.
# Checks and errors
Source: https://docs.sumvin.com/sdk/verify/checks
Every step of checking a Stamped Mandate as a verifier: the offline check, and for services that hold the signed request, the exchange and online check, with their responses and error codes.
**Private preview:** this is rolling out and may not be available on your account yet.
This page is the reference for the [verifier quickstart](/sdk/verify/quickstart).
An agent presents a Stamped Mandate as a token in the `x-sumvin-pint-token` request header, with no `Bearer` prefix, addressed to your site's hostname. You check it offline, on your own server, with no key.
The exchange and the online check are only for a service that already holds the signed request a person approved, for example for the service's own app. An agent never sends a site the signed request. Both calls authenticate with your API key as `Authorization: Bearer `.
| Step | Where | Key permission |
| - | - | - |
| Read the token | The `x-sumvin-pint-token` header on the agent's request | None |
| Offline check | Your server, with `https://jwks.sumvin.com/.well-known/jwks.json` | None |
| Exchange (signed request holders only) | `POST https://sis.sumvin.com/v0/sis/token/pint` | Exchange, included in a Standard key |
| Online check (signed request holders only) | `GET https://sis.sumvin.com/v0/sis/pint/{id}/status` | Read mandates, included in a Standard key |
## Offline check
Verify the token with any standard JWT library against:
| Setting | Value |
| - | - |
| Key set | `https://jwks.sumvin.com/.well-known/jwks.json` |
| Algorithm | `ES256` |
| Issuer | `https://sis.sumvin.com` |
| Audience | Your hostname, such as `reservations.example.com`, or your Organisation ID for a token from the exchange |
Pin the algorithm to `ES256`, the only one Sumvin signs with, so a token signed any other way is refused rather than trusted. Pick the key by the identifier in the token's header, and let the library refresh the key set: keys rotate, so never copy one into your code. A token that fails any part of the check is a refusal.
**Coming soon:** an online check for tokens addressed to a website. Until then, the offline check is the whole check: it tells you the token is genuine, addressed to you and unexpired, but not whether the person has revoked it since.
### From the CLI
The [CLI](/reference/cli/pint) runs the same check:
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sumvin pint verify "" --offline --audience ""
```
With `--offline` it makes no Sumvin API calls, so the revocation check reports as unverified. An unverified check never counts as a pass, so the overall answer is no and the command exits `2`; read the signature, issuer, expiry and audience lines individually. The CLI's own revocation check only answers for mandates you own. The CLI is itself in [Private preview](/get-started/cli/install).
## Exchange
For a service that already holds the person's signed request.
Send the signed request the person approved, unchanged, with `audience` set to your Organisation ID. Any other audience is refused. During the preview, some mandates can't be exchanged yet; those return `PINT-403-009`.
| Answer | Means |
| - | - |
| `201` | A token addressed to your organisation was issued. |
| `208` | This request was already exchanged for your organisation. The body is the same token. |
The body carries `sig` (the token), `id` (the mandate identifier), `scopes` and `expires_at` (epoch milliseconds).
One signed request can be exchanged by up to ten parties, each getting its own token addressed to itself. When the mandate expires or the person revokes it, every token issued against it stops working together.
The person's verification is checked again at every exchange. A request signed while they were verified yields nothing new if their verification has lapsed since.
## Online check
For a token from the [exchange](#exchange), addressed to your organisation.
```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
curl "https://sis.sumvin.com/v0/sis/pint/$(printf %s "$MANDATE_ID" | jq -sRr @uri)/status" \
-H "Authorization: Bearer $SUMVIN_API_KEY"
```
URL-encode the mandate identifier: it contains colons.
```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
"_links": {
"self": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3A3f9c2a7e5b1d4c8e9a6f0b2d7e4c1a58/status" },
"pint": { "href": "/v0/sis/pint/sr%3Aus%3Apint%3A3f9c2a7e5b1d4c8e9a6f0b2d7e4c1a58" }
},
"id": "sr:us:pint:3f9c2a7e5b1d4c8e9a6f0b2d7e4c1a58",
"valid": false,
"reason": "revoked",
"revoked_at": 1758620000000
}
```
| Field | Means |
| - | - |
| `valid` | `true` when the mandate is neither revoked nor expired. **Act only on `true`.** |
| `reason` | `null` when valid; otherwise `revoked` or `expired`. |
| `revoked_at` | When it was revoked, in epoch milliseconds. Only set when revoked. |
Rules of the online check:
* **It answers only for mandates addressed to you.** A mandate with no token addressed to your organisation returns `404`, whether or not it exists. This is how one verifier is kept from probing another's mandates.
* **Answers can be up to 30 seconds old.** The answer for each mandate is cached per organisation for 30 seconds.
* **`valid` is this mandate's own state.** The fuller *stands* answer in [What a verifier checks](/concepts/mandates/verification#stands-not-status) also accounts for any larger mandate this one was narrowed from. That answer is available through the connector's `mandate_verify` tool; see [Check a mandate](/guides/check-a-mandate).
To read the mandate's scopes, status and expiry, call `GET https://sis.sumvin.com/v0/sis/pint/{id}` with the same key. It follows the same rule: `404` unless a token for the mandate is addressed to your organisation.
## Errors
Errors come back as a problem document with an error `code`, except a missing `Authorization` header, which returns `401` with `{"detail": "Not authenticated"}`.
| Status | Code | Means | What to do |
| - | - | - | - |
| `401` | `SIS-401-001` | The key is invalid, expired or revoked. | Check the key, or create a new one. |
| `401` | `SIS-401-002` | The key isn't attached to an organisation. | Create the key from the developer dashboard. |
| `403` | `SIS-403-003` | The key lacks the permission this call needs. | Use a Standard key. |
| `404` | `PINT-404-001` | No mandate with this identifier has a token addressed to your organisation. | Don't act. |
| `400` | `PINT-400-002` | Exchange: `audience` isn't your Organisation ID. | Set it to your Organisation ID. |
| `403` | `PINT-403-009` | Exchange: during the preview, some mandates can't be exchanged yet, and this is one of them. | Don't act. |
| `409` | `PINT-409-002` | Exchange: the mandate has been revoked. | Don't act. |
| `409` | `PINT-409-007` | Exchange: the signed request has already reached as many parties as it can. | Ask for a new mandate. |
| `410` | `PINT-410-001` | Exchange: the mandate has expired. | Don't act. |
More codes are listed in [Errors](/reference/errors).
# Check a Stamped Mandate
Source: https://docs.sumvin.com/sdk/verify/quickstart
Before you act for an agent, check the Stamped Mandate token it sends your site, offline on your own server.
**Private preview:** this is rolling out and may not be available on your account yet.
An agent asks your site to do something and shows you a [Stamped Mandate](/concepts/stamped-mandates) as proof that a verified person allowed it. The agent sends it as a token addressed to your site's hostname. You check it on your own server, offline, against Sumvin's published keys: the token never leaves your server, and you need no API key.
A passing check tells you the token is genuine, addressed to you and unexpired, and that the person approved exactly its [scopes](/concepts/mandates/scopes). A token addressed to a hostname carries identity attestations only: proof of personhood, verification status, over 18. It never tells you the person's name or contact details. See [What a verifier checks](/concepts/mandates/verification).
These checks use plain HTTP and a standard JWT library. The SDK has no verification function yet.
## What the agent sends
The agent sends the token in the `x-sumvin-pint-token` request header. The value is the bare token, with no `Bearer` prefix:
```http theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
x-sumvin-pint-token:
```
The token is addressed to one hostname, such as `reservations.example.com`. Accept only a token addressed to your own.
## Before you start
* A JWT library. This page uses [`jose`](https://github.com/panva/jose): `bun add jose` or `npm install jose`.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const token = request.headers.get("x-sumvin-pint-token");
if (!token) throw new Error("No Stamped Mandate presented");
```
A request without the header carries no mandate. Treat it as any other unauthenticated request.
Verify the token against Sumvin's published key set, with your hostname as the audience. The keys rotate, so let your library fetch and cache them rather than copying them.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { createRemoteJWKSet, jwtVerify } from "jose";
const sumvinKeys = createRemoteJWKSet(
new URL("https://jwks.sumvin.com/.well-known/jwks.json"),
);
await jwtVerify(token, sumvinKeys, {
algorithms: ["ES256"],
issuer: "https://sis.sumvin.com",
audience: "reservations.example.com",
});
```
Pin the algorithm to `ES256`, the only one Sumvin signs with, so a token signed any other way is refused rather than trusted.
`jwtVerify` throws if the token was altered, wasn't issued by Sumvin, isn't addressed to you, or has expired. Treat any throw as a refusal.
**Coming soon:** an online check for tokens addressed to a website. Until then, the offline check is the whole check: it tells you the token is genuine, addressed to you and unexpired, but not whether the person has revoked it since.
A token that passes its check still permits only what its scopes say. Compare them with what the agent is asking for, and act on nothing more.
Any refusal means don't act.
## Services that already hold the signed request
This section is for a service that already holds the signed request a person approved, for example because the person approved it for the service's own app. It is not how an agent presents a mandate to a site: an agent sends the hostname token above. A service in this position can exchange the signed request for a token addressed to its organisation, then check online whether the mandate still stands.
It needs an API key and your Organisation ID. See [Get a key](/sdk/verify/api-key).
**Developer dashboard access is by invitation during the preview.** You can't sign up yourself yet.
Send the signed request the person approved, unchanged, with `audience` set to your Organisation ID. You get back a token addressed only to your organisation.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const SIS = "https://sis.sumvin.com";
const res = await fetch(`${SIS}/v0/sis/token/pint`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUMVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...signedRequest, audience: process.env.SUMVIN_ORG_ID }),
});
if (!res.ok) throw new Error(`Exchange refused: ${res.status}`);
const { sig: token, id: mandateId, scopes, expires_at } = await res.json();
```
`201` means a new token was issued. `208` means this request was already exchanged for your organisation, and you get the same token back. Keep `mandateId`: you need it for the online check.
During the preview, some mandates can't be exchanged yet; the exchange refuses them with `PINT-403-009`. See [Checks and errors](/sdk/verify/checks#errors).
Run the same offline check as above, with your Organisation ID as the audience:
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
await jwtVerify(token, sumvinKeys, {
algorithms: ["ES256"],
issuer: "https://sis.sumvin.com",
audience: process.env.SUMVIN_ORG_ID,
});
```
Ask Sumvin whether the mandate is still valid:
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const statusRes = await fetch(
`${SIS}/v0/sis/pint/${encodeURIComponent(mandateId)}/status`,
{ headers: { Authorization: `Bearer ${process.env.SUMVIN_API_KEY}` } },
);
if (!statusRes.ok) throw new Error(`Online check refused: ${statusRes.status}`);
const status = await statusRes.json();
if (!status.valid) {
throw new Error(`Mandate no longer stands: ${status.reason}`);
}
```
`valid` is `true` when the mandate is neither revoked nor expired. Otherwise `reason` says which. See [the online check](/sdk/verify/checks#online-check).
Compare the `scopes` with what is being asked for, and read any spending ceiling with [`readScopeCeiling`](/sdk/verify/reading-scopes).
Any refusal, from any step, means don't act.
## Next
Every response, error code and limit of the two checks.
Turn scopes into amounts you can compare.
# Read a mandate's scopes
Source: https://docs.sumvin.com/sdk/verify/reading-scopes
Turn the scopes on a Stamped Mandate into ceilings you can compare against a request, with readScopeCeiling.
**Private preview:** this is rolling out and may not be available on your account yet.
A Stamped Mandate permits only what its [scopes](/concepts/mandates/scopes) say. Before you act, check that the request in front of you is inside them. The scope language, parameters and limits are described in [The mandate language](/concepts/mandates/language) and [Limits and narrowing](/concepts/mandates/limits); this page covers reading them in code.
## Read a spending ceiling
`readScopeCeiling` reads the ceiling one scope states. It works offline on the scope string alone, so you can use it on the `scopes` from the [exchange](/sdk/verify/checks#exchange).
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import { isScopeCeilingError, readScopeCeiling } from "@sumvin/sdk";
readScopeCeiling("sr:us:pint:spend:visa_checkout?max=25¤cy=USD");
// { kind: "fiat", amount: "25.00", currency: "USD", decimals: 2 }
readScopeCeiling("sr:us:pint:sigil:proof_of_personhood");
// null: this scope states no ceiling
```
`max` is always in the display unit of the currency: `max=25¤cy=USD` is 25.00 US dollars, never 25 cents. `amount` comes back as an exact decimal string, padded to the currency's precision. Compare it as a decimal, never as a floating-point number.
Spending scopes belong to payments, which arrive in a fast-follow release. Identity scopes such as `sr:us:pint:sigil:proof_of_personhood` state no ceiling.
## Read every scope
The reader handles one scope at a time. Read every scope on the mandate, not only the first.
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
const ceilings = scopes
.map((scope) => ({ scope, ceiling: readScopeCeiling(scope) }))
.filter(({ ceiling }) => ceiling !== null);
```
## When it refuses
The reader refuses rather than guesses. It throws a `ScopeCeilingError` whose `reason` is one of:
| Reason | Means |
| - | - |
| `malformed-scope` | The scope, or its parameters, aren't written the way the language allows. |
| `invalid-amount` | `max` isn't a plain positive decimal. |
| `missing-denomination` | `max` names no currency or asset. |
| `ambiguous-denomination` | It names both a currency and an asset. |
| `unknown-denomination` | The currency or asset isn't one the SDK knows. |
| `over-precise` | `max` has more decimal places than the currency allows, such as `25.001` USD. |
```ts theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
try {
readScopeCeiling(scope);
} catch (e) {
if (isScopeCeilingError(e)) {
// Unreadable: refuse the request.
}
}
```
**Treat a refusal as unreadable, never as unlimited.** Refuse the request.
`errand:search` takes an optional `max` but names no currency, and that `max` is not enforced (see [Scopes](/concepts/mandates/scopes)). A `max` written there has no currency to read, so the reader refuses it as `missing-denomination`. The ceiling that counts is on `spend:visa_checkout`.
# Contact support
Source: https://docs.sumvin.com/support
How to reach Sumvin when something goes wrong, and what to include so we can find it.
## What to include
Every error your agent receives from Sumvin carries two things that let us find exactly what
happened. Include both:
* **The error code**, such as `KYC-403-001`. It is the `code` in the error your agent received.
* **The trace id**, a 32-character string such as `0af7651916cd43dd8448eb211c80319c`. It is the
`trace_id` in the error your agent received. Ask your agent for it if you can't see it.
Tell us roughly when it happened and what you were trying to do. Don't send passwords, passkeys,
recovery codes or full card numbers: we don't need them to look into an error.
## Before you write
Most errors say what to do next. Look the code up on the [Errors](/reference/errors) page first: a
code ending in `-R` usually clears if you wait and try again.