Backend API Reference
The backend is a minimal Fastify service with exactly three endpoints. Request/response bodies are validated against strict JSON Schemas.
Strict by default
Schema validation runs with additionalProperties: false. An accidental stray
field — like { accountId, pin } — is rejected with a loud 400, not silently
stripped. This is defense in depth against a client bug that might otherwise leak an extra
field without anyone noticing.
GET /healthz
Liveness probe. Carries no accountId and reveals nothing about wallet state — not rate
limited.
| Status | Body |
|---|---|
| 200 | { "status": "ok" } |
POST /v1/provision
Idempotently gets-or-creates the salt for an account. Safe to call on every unlock, not just first-time setup.
Body: { "accountId": "<64-char lowercase hex>" } — pattern ^[0-9a-f]{64}$.
| Status | Body |
|---|---|
| 200 | { "accountId", "salt" (base64), "created": boolean } |
| 400 | { "error": "invalid_account_id" } |
| 429 | { "error": "rate_limited", "retryAfterSeconds" } + Retry-After header |
GET /v1/salt/:accountId
Fetch-only variant of the above, for a stricter unlock-only flow.
| Status | Body |
|---|---|
| 200 | { "salt": "<base64>" } |
| 404 | { "error": "not_found" } — never provisioned |
| 429 | { "error": "rate_limited", "retryAfterSeconds" } + Retry-After header |
Shared rate-limit bucket
POST /v1/provision and GET /v1/salt/:accountId share exactly one
rate-limit bucket per accountId. Hammering one endpoint locks out the other too
— see Rate Limiting Policy for the exact formula.
Data minimization
The server persists only a hashed accountId, a random salt, and rate-limit counters. It
never stores the raw UID, the PIN, the mnemonic, the seed, any private key, or any wallet
address.