These routes address the workspace named by the token’s own claim — no
workspace-token route takes the workspace in its path, so a path and a token
can never disagree about which one is in play. (The tenant-tier route
DELETE /v1/workspaces/{slug} does name a workspace in its path; it carries no
token to disagree with.)
Auth: workspace token on every route.
GET /v1/workspace
Current workspace state. Scope: read. Returns 200.
{
"workspace": "demo",
"locked": false,
"accounts": 3,
"damagedAccounts": 0,
"expiresAt": "2026-08-18T12:34:56.000Z"
}| Field | Meaning |
|---|---|
accounts |
Number of accounts in the workspace |
damagedAccounts |
Count of accounts whose records could not be read |
expiresAt |
When the idle clock currently runs out |
GET /v1/accounts
List every account in the workspace. Scope: read. Returns 200.
{
"accounts": [
{
"slug": "treasury-a1b2",
"displayName": "Treasury",
"kind": "HD",
"hasOwnPassword": false,
"locked": false,
"wallets": 4
}
]
}GET /v1/accounts/{slug}
Resolve one account, unlocking it lazily if it shares the workspace password.
Scope: read. Returns 200.
{
"account": {
"slug": "treasury-a1b2",
"displayName": "Treasury",
"kind": "HD",
"hasOwnPassword": false,
"locked": false,
"wallets": 4
}
}An account with its own password returns 423 with account_locked until
explicitly unlocked.
GET /v1/workspace/assets
Assets on one network. Scope: read. Returns 200.
Query parameters
| Parameter | Required | Notes |
|---|---|---|
network |
yes | Network slug |
The network must be named — listing every network’s assets would be an unbounded response shaped by how many networks the tenant has registered.
{
"network": "ethereum",
"assets": [
{
"id": 1,
"symbol": "ETH",
"name": "Ether",
"decimals": 18,
"native": true,
"contractAddress": null
}
]
}POST /v1/accounts/{slug}/unlock
Reopen an account that is locked. Any scope. Returns 204.
{ "accountPassword": "…" }An account created with hasOwnPassword: true needs this before its first
use, with its own password.
Every other account needs it too, eventually. An account sharing the workspace
password opens transparently the first time you touch it, but that opening is a
custody window bounded by ttl.accountAbsoluteSec — five minutes by
default. When it lapses the account locks, and every route touching it answers
423 and account_locked until you call this one. Pass the workspace password
in accountPassword.
The same applies after POST /v1/accounts/{slug}/lock, which locks any account
regardless of how it was created.
Failed attempts are rate-limited — 429 with account_unlock_rate_limited.
POST /v1/accounts/{slug}/lock
Lock an account again. Any scope. Returns 204. No body.
Errors
| Status | Code |
|---|---|
| 400 | invalid_body, invalid_slug, invalid_parameter, unsupported_network |
| 401 | session_expired |
| 403 | scope_denied |
| 404 | account_not_found |
| 423 | account_locked |
| 429 | account_unlock_rate_limited |