Scopes, Errors & Exit Codes
All 18 API key scopes, the scope error codes, and the tvault CLI exit codes.
Every tvkey_… API key and scoped tvagent_… agent carries a list of scopes. A call without the
needed scope fails with 403 SCOPE_DENIED and names the scope in missingScope. For the
concepts, see API Keys & Scopes.
Scopes
| Scope | Allows |
|---|---|
credentials:read | Fetch credentials the principal has grants for |
mcp:use | Call MCP proxies the principal has grants for |
tokens:list | List token metadata (no values) |
tokens:create | Create tokens. No read-back |
tokens:create-read | Create tokens and receive a read grant on them at creation time. Actually reading the token also needs credentials:read. Not retroactive. Removing the scope stops new auto-grants; existing grants stay |
tokens:update | Update any token. Human-grant-only |
tokens:delete | Delete any token. Human-grant-only |
agents:read | List and show agents |
agents:create | Create agents. Children get a subset of the creator's scopes |
agents:manage | Edit, suspend, resume, and end sessions of any agent. Human-grant-only |
agents:delete | Delete any agent, singly or in bulk. Human-grant-only |
grants:write | Grant tokens to agents, only tokens the granter can itself read, so it also needs credentials:read. Auto read grants from tokens:create-read can be passed on, but every copy lapses, for good, when the token is re-created, replaced, rebound or deleted. A grant passed on can't outlive the granter's own. Human-grant-only |
proxies:read | List and show MCP proxies |
proxies:write | Create and change MCP proxies. Changing a webhook proxy's upstream, headers or service also needs credentials:read and a grant on that service; a session proxy's upstream is human-only. Human-grant-only |
policies:read | List and show policies |
policies:write | Create, change, and attach policies. Human-grant-only |
keys:manage | Create, list, edit, suspend, and resume keys. Human-grant-only |
keys:revoke | Revoke (delete) any key. Human-grant-only |
Human-grant-only scopes can only be put on a key or agent by a signed-in human. A key or agent that
tries to pass one on gets 403 SCOPE_NOT_DELEGABLE, even if it holds the scope. The check covers newly added
scopes only; narrowing a key that already has them is fine. Every scope covers the
whole account, whoever holds it. Other scopes can be passed down as a subset of the holder's own.
Classic agents hold credentials:read and mcp:use implicitly, bounded by their grants.
Humans hold every scope.
Inspect what a principal has:
tvault whoamicurl https://api.tokenvault.uk/api/agents/whoami -H "Authorization: Bearer $TVAULT_KEY"API
| Method + path | Purpose |
|---|---|
POST /api/keys | Create a key: {name, scopes, expiresAt}. Returns key once. Humans need a sign-in within 5 minutes (401 REAUTH_REQUIRED) |
GET /api/keys | List keys |
GET /api/keys/{id} | One key, with its grants |
PATCH /api/keys/{id} | {name?, status?, scopes?}. Affects only this key. A scopes edit needs an unlocked vault |
DELETE /api/keys/{id} | Revoke this key only (needs keys:revoke). Keys and agents it created keep working until you revoke them |
POST /api/keys/{id}/rotate | New key value. A key may rotate itself; rotating another principal's key is human-only. A lost race returns 409 ROTATION_CONFLICT |
POST /api/keys/{id}/grants | {serviceName, expiresInHours?, refreshPolicy?} |
DELETE /api/keys/{id}/grants/{serviceName} | Remove a grant |
POST /api/agents | Add kind: "classic" | "scoped" and scopes |
POST /api/agents/{id}/rotate-key | New agent key. Same rule as key rotation |
DELETE /api/agents/{id} | Delete an agent. Needs agents:delete (bulk delete too) |
Errors
{"detail": {"code": "SCOPE_DENIED", "message": "Missing scope tokens:create", "missingScope": "tokens:create"}}| Code | HTTP | CLI exit | Meaning |
|---|---|---|---|
SCOPE_DENIED | 403 | 8 | The key lacks the scope in missingScope |
HUMAN_ONLY | 403 | 9 | Only a signed-in human can do this |
KEY_EXPIRED | 401 | 10 | The key is past its expiry. Rotate or create a new one |
KEY_SUSPENDED | 403 | 11 | The key itself is suspended |
INVALID_KEY | 401 (403 on GET /api/agents/credentials, kept for existing agents) | 12 | Unknown, revoked, or malformed key |
SCOPE_NOT_DELEGABLE | 403 | 1 | A key or agent tried to give a human-grant-only scope to another key or agent. scopes lists the offenders |
SELF_CHANGE_FORBIDDEN | 403 | 1 | A key or agent tried to loosen its own controls: its scopes, its MCP access, resuming itself, its attached policies, or its grants. Suspending, renaming, adding a policy, and rotating its own key are allowed |
ROTATION_CONFLICT | 409 | 1 | Two rotations of the same key raced and this one lost. No new key was issued |
GRANT_REQUIRED | 403 | You need a live grant of your own on the token to pass it on or bind a proxy to it. Also returned by /api/proxy/mcp when a proxy was bound from a tokens:create-read grant and the token's creator no longer owns the token | |
MANUAL_GRANT_IN_PLACE | 409 | 1 | A key or agent tried to replace a target's regular grant with an auto-derived copy. A human can |
AUTO_GRANT_IN_PLACE | 409 | 1 | A key or agent tried to replace a target's auto-derived grant with a regular one. A human can |
HEADERS_REQUIRED | 400 | 1 | A key changed a proxy's upstream URL without sending new headers in the same edit |
NO_GRANT | 404 | No grant for that service | |
UNKNOWN_SCOPE | Scope name not in the table above | ||
INVALID_EXPIRY | Expiry in the past. An expiry later than the creator's is not an error: it is silently clamped to the creator's expiry at creation | ||
TOKEN_EXISTENCE_CHECK_UNAVAILABLE | 503 | 1 | Could not confirm the token exists (webhook unreachable). Retry |
VALUE_NOT_ALLOWED | 400 | 1 | A key tried to send a credential value through Token Vault. Request a store ticket and send the value straight to your webhook |
SELF_GRANT_FORBIDDEN | 403 | 1 | A key or agent tried to grant itself access |
TOKEN_OWNED | 409 | 1 | That token name belongs to someone else, or still has proxies bound to it; a key can't take it over or re-create it. Also returned to a key retrying a store ticket for its own new name when references from an older claim (even its own) still exist |
TOKEN_OWNERSHIP_UNAVAILABLE | 503 | 1 | Couldn't verify who owns the token. Nothing was stored; retry. Also returned when: a key or agent overwrites a token and the creator record can't be read; a take-over finds the creator changed meanwhile; a delete can't write its cleanup mark (refused before anything is deleted); an overwrite hits a token whose earlier delete cleanup is unfinished and still fails; or a webhook bind or rebind can't reset key-created grants and proxies. In every case nothing is stored; retry |
POLICY_DENIED | 403 / 429 | An attached policy said no |
Every key and agent is independent. A key's effective scopes are its own scopes, and its access depends only on its own status, expiry, grants, and attached policies. Revoking, suspending, or narrowing one key never affects another, including keys it created. Revoking a compromised key does not revoke what it created: use the "Created by" information to find those and revoke them yourself. A creator can't give a child scopes, grants, or an expiry beyond its own, but that check happens only at creation.
POST /api/vault/credential-ticket and the TOTP code endpoint are human-only. Keys and agents read
credentials through GET /api/agents/credentials?service=<name>.
A locked vault returns 423 VAULT_LOCKED for every key.
Branch on the exit code in scripts:
tvault tokens create --service stripe --value "$V"
case $? in
0) echo ok ;;
8) echo "key needs tokens:create" ;;
10) echo "key expired, rotate it" ;;
11) echo "key suspended" ;;
12) echo "bad key" ;;
esac