Every route here uses tenant credentials, never a workspace token.
GET /v1/workspaces
List the tenant’s workspaces. Returns 200.
{
"workspaces": [
{ "slug": "demo", "createdAt": "2025-06-01T09:15:00.000Z", "walletCount": 12 }
]
}GET /v1/quota
Report the tenant’s configured limits and current usage. Returns 200.
curl "$API_URL/quota" -H "X-Api-Key: $API_KEY" -H "X-Api-Secret: $API_SECRET"{
"workspaces": { "used": 3, "limit": 10 },
"wallets": { "used": 128, "limit": 500 }
}Two counters, both tenant-wide: workspaces you have created, and wallets across
all of them. walletCount on each workspace record breaks the second one down.
This route does not report how many workspaces may be open at once. That
ceiling exists — it is what session_capacity_exceeded enforces — but it is
not exposed by any route.
POST /v1/workspaces
Create a workspace. Returns 201.
Request
{ "slug": "demo", "password": "a-strong-workspace-password" }| Field | Type | Required | Notes |
|---|---|---|---|
slug |
string | yes | Lowercase letters, digits, hyphens; starts alphanumeric; ≤63 chars. con, prn, aux and nul are reserved and refused despite fitting that rule |
password |
string | yes | At least 12 characters. Shorter is rejected with 422 and weak_password |
Response
{
"workspace": {
"slug": "demo",
"createdAt": "2025-06-01T09:15:00.000Z",
"walletCount": 0
}
}Errors
| Status | Code |
|---|---|
| 400 | invalid_body, invalid_slug |
| 401 | bad_api_key |
| 409 | workspace_exists, workspace_in_use, quota_workspaces_exceeded |
| 422 | weak_password |
| 429 | workspace_creation_rate_limited, workspace_recreation_cooldown, session_capacity_exceeded |
| 500 | storage_error — the workspace exists in the ledger but could not be opened |
Creating a workspace holds an open handle for as long as it takes, so it
competes for the same ceiling a token mint does: with too many workspaces
already open, create answers 429 with session_capacity_exceeded.
workspace_in_use is the narrower case where that exact slug is already held
open — a create racing another create, or a mint, on the same name.
DELETE /v1/workspaces/{slug}
Delete a workspace and everything in it. Returns 204.
Query parameters
| Parameter | Values | Default | Meaning |
|---|---|---|---|
force |
true or false |
false |
Delete even while live token leases hold the workspace |
force accepts exactly those two strings. Anything else returns 400 with
invalid_parameter — there is no truthy-string coercion.
Without force, deleting a workspace that is still held returns 409 with
workspace_in_use. “Held” means live token leases — an expired session, or one
whose last token was revoked, does not block the delete even though it still
exists. A workspace caught mid-open or mid-close is refused the same way. force
overrides all of it: it is read before the check runs, so it applies to every
form of this refusal on delete.
Create is the exception, and only because it has no force to give: the same
code answers POST /v1/workspaces when that slug is already being held, and
there the only option is to retry once it settles.
Errors
| Status | Code |
|---|---|
| 400 | invalid_slug, invalid_parameter |
| 401 | bad_api_key |
| 403 | permission_denied — the storage refused the delete |
| 404 | workspace_not_found |
| 409 | workspace_in_use |
| 500 | storage_error — a ledger write during removal failed |
?force=true overrides the 409, which is a live-session refusal. It does not
override the 403, which comes from the storage layer and stands regardless.
Related
- Manage workspaces — the task walkthrough