Every error from every route has one shape:
{
"error": {
"code": "account_not_found",
"message": "…",
"status": 404,
"requestId": "…"
}
}Branch on error.code. Messages are written for humans and may change;
codes are the contract. Keep requestId when reporting a problem — it comes
back as the x-request-id response header too, and it is what your operator
will ask for.
Some errors carry a fifth field, error.details, naming the specific thing
that failed:
{
"error": {
"code": "scope_denied",
"message": "…",
"status": 403,
"requestId": "…",
"details": { "required": ["sign"] }
}
}| Code | details |
|---|---|
scope_denied |
required — an array of every scope the route wanted, not just the one you lack |
invalid_slug |
expected — the grammar it wanted, and the reserved names when that is why it was refused |
account_locked |
account |
limit_not_raised |
limit — which ceiling you named — plus current and requested |
session_capacity_exceeded |
scope and limit — which ceiling you hit, and what it is |
mint_rate_limited, account_unlock_rate_limited, workspace_creation_rate_limited, workspace_recreation_cooldown, admin_rate_limited |
retryAfterSec |
Treat details as present-or-absent: no other code carries it, and nothing
outside this table is exposed.
Wait exactly as long as you are told
Every 429 that knows its own deadline sets a Retry-After response
header, in seconds. That covers the five rate-limit codes above. Read the
header rather than guessing at a backoff — it is the authoritative answer, and
for workspace_recreation_cooldown a guess will be badly wrong.
The capacity code session_capacity_exceeded does not set it: its ceiling
clears when other work finishes, not on a clock. That is the one to back off
exponentially.
400 — the request was malformed
| Code | Meaning and fix |
|---|---|
invalid_body |
The JSON shape or field combination is wrong. The message names the expected shape and the fields that failed — … — "count" is required, … — unexpected field "tag". Field names only; your values are never echoed back. |
invalid_slug |
A workspace or account identifier the service will not accept. Usually syntax — lowercase letters, digits, hyphens, starting alphanumeric. Workspace slugs additionally refuse four names reserved as directory names — con, prn, aux, nul — even though they satisfy that rule; account slugs do not, because they are generated rather than chosen. The details.expected says which of the two you hit. |
invalid_parameter |
A path or query selector is malformed — for example ?force= or ?vm= with an unaccepted value. Also returned when the signing engine itself rejects a payload the service forwarded, such as typed data whose field types do not check out, or an address that fails its checksum. |
chain_id_mismatch |
A typed-data domain.chainId names a different chain than the one being signed for. The message gives both numbers. See Sign. |
invalid_mnemonic |
The supplied mnemonic could not be parsed. |
invalid_private_key |
The supplied private key could not be parsed. |
unsupported_network |
The network is not one the workspace supports. Returned both when the signing engine rejects a network and when a route’s ?network= selector (for example GET /v1/workspace/assets) names a well-formed network the workspace lacks. |
401 — you are not authenticated
| Code | Meaning and fix |
|---|---|
bad_api_key |
X-Api-Key or X-Api-Secret is missing or wrong. |
bad_password |
A password was wrong — the workspace password when minting a token, or the account password when unlocking an account. Branch carefully: the same code covers both. |
session_expired |
The idle or absolute clock elapsed. Mint a new token and retry — this is routine, not exceptional. Also returned for a missing or malformed Authorization header, and after a service restart, so it does not by itself mean a clock ran out. |
admin_denied |
The super-admin X-Admin-Key is missing or invalid. |
403 — authenticated, but not allowed
| Code | Meaning and fix |
|---|---|
scope_denied |
The token is valid but lacks the scope. Re-mint with the scope you need. |
permission_denied |
The operation is not permitted for this caller. |
404 — no such thing
| Code | Meaning and fix |
|---|---|
workspace_not_found |
No workspace with that slug for this tenant. |
account_not_found |
No such account, wallet, or address in this workspace. |
not_found |
The route or resource does not exist. |
tenant_not_found |
A super-admin route named a tenant id that does not exist. |
409 — conflicts with current state
| Code | Meaning and fix |
|---|---|
workspace_exists |
That slug is already in use. |
workspace_in_use |
The workspace is held. On delete that means live token leases, or a workspace caught mid-open or mid-close; ?force=true overrides the refusal in every one of those forms. It is also raised on create, when that slug is already being held — POST /v1/workspaces takes no force, so there retrying once it settles is the only option. |
quota_workspaces_exceeded |
Tenant workspace limit reached. |
quota_wallets_exceeded |
Tenant wallet limit reached. |
duplicate_identity |
An add collided with an identity the workspace already holds — most often re-importing a private key that is already present. Distinct from invalid_parameter, which is for malformed input. |
wallet_disabled |
A signing request named a wallet whose derivation slot is disabled. Disabling is a reversible marker the engine supports; this service does not currently expose a route to toggle it, so in practice you will not see this. |
limit_not_raised |
A super-admin limit change named a value below the current ceiling. Limits move up only; a value equal to the current ceiling is accepted and changes nothing. details carries the field, its current value, and what you asked for. |
422 — well formed, but cannot be done
| Code | Meaning and fix |
|---|---|
unsupported_for_kind |
The account, wallet, or chain type cannot perform this otherwise valid operation — typed data on a non-EVM address, a mnemonic from a PK account, a VM the wallet lacks. |
weak_password |
The password is shorter than 12 characters. Length is the only rule — no character-class requirement applies. Raised when creating a workspace or an own-password account, never when minting a token. |
423 — locked
| Code | Meaning and fix |
|---|---|
workspace_locked |
The workspace is not open. Mint a token. |
account_locked |
The account is not currently open. Call the unlock route with the account’s password — or, for an account that shares the workspace password, with the workspace password. Any account reaches this state once its custody window expires, not only one created with hasOwnPassword. |
record_locked |
A record is held by another operation. Retry shortly. |
429 — slow down, or you are at capacity
| Code | Meaning and fix |
|---|---|
mint_rate_limited |
Too many token mints for this tenant. Back off. Account unlock spends the same tenant budget, so this also answers an unlock. |
account_unlock_rate_limited |
Too many failed account-unlock attempts. |
workspace_creation_rate_limited |
Creating workspaces too quickly. |
workspace_recreation_cooldown |
That slug was recently deleted and is still cooling down. |
session_capacity_exceeded |
A capacity ceiling was hit. details.scope says which: tenant (workspaces open at once), process, or workspace (live token leases on one workspace, default 64). Revoke tokens you no longer need — a client that mints per request without revoking reaches the workspace ceiling with only one workspace open. |
admin_rate_limited |
Too many super-admin requests. Back off; details.retryAfterSec says how long. |
500 — the service failed
| Code | Meaning and fix |
|---|---|
storage_error |
A storage operation failed. |
encrypt_failed |
Encryption failed. |
decrypt_failed |
Decryption failed. |
algorithm_irreversible |
The requested transformation cannot be reversed. |
storage_not_durable |
A write could not be confirmed durable. Do not assume it landed — re-read before retrying anything that mutates. |
These are not your fault and not fixable by retrying the same request. Capture
requestId and report it.
501 — not implemented
| Code | Meaning and fix |
|---|---|
unsupported_operation |
The operation is not supported by the underlying engine — most often a /sign/message encoding the address cannot perform, such as ed25519 on an EVM address or personal_sign on a Solana one. See Sign. |
507 — capacity
| Status | Code | Meaning and fix |
|---|---|---|
| 507 | disk_full |
The service is out of storage. |
Retry guidance
| Situation | Retry? |
|---|---|
429 with a Retry-After header |
Yes, after exactly that long |
429 without one |
Yes, with exponential backoff |
session_expired |
Yes, after minting a new token |
record_locked |
Yes, shortly |
Any 400, 403, 404, 422 |
No — fix the request |
500-class |
No — report with requestId |
Codes from outside the catalog
The codes above are the service’s own, and each one names a specific condition. A few others come from the layer in front of it, before a request ever reaches a route:
| Status | Code | When |
|---|---|---|
| 400 | bad_request |
The JSON body could not be parsed at all |
| 401 | unauthorized |
A rejection with no more specific code |
| 403 | forbidden |
A rejection with no more specific code |
| 404 | not_found |
No such route. Note this is the same code a missing record produces, so the two are not distinguishable on the wire |
| 413 | payload_too_large |
The request body exceeded the parser’s limit, roughly 100 KB |
| 500 | internal_error |
An unhandled failure. Capture requestId and report it |
| — | request_failed |
A 4xx with no more specific code |
They carry the same envelope as everything else, so branching on error.code
still works — but they are generic by construction, and error.details is
never present on them.
Related
- API conventions — the error shape and request tracing
- Safe usage