Inside a workspace, key material nests three levels deep. Each level exists for a reason, and knowing which one you are addressing removes most of the guesswork from the endpoint list.
Accounts hold the secret
An account is created with POST /v1/accounts and comes in two kinds.
| Kind | Secret | Derivation |
|---|---|---|
HD |
A BIP-39 mnemonic, generated when you do not supply one | Many wallets from one mnemonic |
PK |
A single imported private key | No derivation |
The kind field defaults to HD. Supply secret to import an existing
mnemonic (HD) or private key (PK); omit it and an HD account generates its own.
A PK account has nothing to generate, so secret is required there —
omitting it is 400 with invalid_body.
Creating an account of either kind produces its first wallet immediately,
and that wallet counts against the tenant’s limits.maxWallets allowance. An
account is never empty, and account creation is never quota-free.
hasOwnPassword and accountPassword are a pair: sending either without the
other is refused, because the flag without a distinct password would leave the
account “separately locked” by the very secret that opened the session. The two
directions answer differently — hasOwnPassword alone is 400 invalid_body,
while an accountPassword alone is checked for strength first, so a weak one
comes back 422 weak_password and only a strong one reaches the 400. An
account password is held to the same strength minimum as a workspace password,
though unlike a workspace password it is also capped at 256 characters. Omit both and the account shares the workspace password, which is the
ordinary case.
The account slug is generated by the service, not chosen by you. Read it from the create response — it is the identifier every later call uses.
displayName is normalized before use and must be 3–64 characters afterwards.
It must also still contain a Latin letter, because the slug is projected from it
with accents stripped; a name that projects to nothing is refused with 400 and
invalid_parameter. The slug is truncated to leave room for a numeric
collision suffix, so two accounts sharing a display name get distinct slugs.
Wallets are derived
POST /v1/accounts/{slug}/wallets derives more wallets on an HD account, in
bulk:
{ "count": 5 }The response reports the count on both sides of the operation, which makes the
call safe to retry and easy to reconcile. Because creation already made one, a
brand-new account starts from 1:
{ "before": 1, "after": 6 }Deriving on a PK account is refused with 422 and unsupported_for_kind. A
PK account instead takes POST /v1/accounts/{slug}/wallets/import with a
privateKey, which adds exactly one wallet on top of the one its creation key
already produced — and importing into an HD account is the mirrored 422.
Derivation is bounded: count must be a positive integer up to 500, and the
tenant’s limits.maxWallets caps the total. Crossing it returns 409 with
quota_wallets_exceeded and derives nothing.
Tags
Wallets carry a list of up to 32 string tags, replaced wholesale with
PUT /v1/accounts/{slug}/wallets/{id}/tags. The service attaches no meaning to
them, but it does not store them verbatim either: each tag is trimmed and
Unicode-normalized, and the list is de-duplicated after that. Read the tags back
from the response rather than assuming you got what you sent.
Individual tags have rules, and breaking them is refused rather than repaired: a
tag must be 1–64 characters after normalization and must not contain control
characters, zero-width characters, or BIDI overrides, or the request comes back
400 invalid_parameter. Zero-width and BIDI characters are refused because a
tag is displayed to people, and either can make two different tags render
identically. The 32 cap is a
separate check on the array you sent, applied before de-duplication and
reported as 400 invalid_body — so 33 tags that would collapse to 5 are still
refused. De-duplication is case-sensitive: prod and Prod both survive.
Addresses are what you sign with
Each wallet exposes one address per VM — an evm one and an svm one — and
each address reports the single network it is on:
{ "publicKey": "0xabc…", "vm": "evm", "network": "ethereum", "chainId": 1 }Read vm, network and chainId off the address itself. They are the
authoritative answer to “what chain will this sign for”; the account’s
defaultNetwork is a creation-time input, not a per-address guarantee, and a
wallet’s svm address is necessarily on a Solana network however the account
was configured.
The signing endpoints take a public key directly rather than
an account/wallet path. You do not walk the tree to sign. Resolution is scoped
to your own workspace, so one tenant can never reach another’s key — and an
unknown public key returns 404 with account_not_found, the same as a public
key that exists but belongs elsewhere.
Related
- Manage accounts and wallets — the task walkthrough
- Accounts endpoints — full request and response shapes
- Scopes and permissions — which scope each operation needs