API conventions
Read this once; it applies to every endpoint.
Base URL
| Environment | Base URL |
|---|---|
| Staging | https://api-staging.zenvault.io/api |
| Production | https://api.zenvault.io/api |
All endpoints are under /v1. Health check (unauthenticated): GET /api/health.
Authentication
Bearer key on every /v1 request; see Workspaces & keys.
Authorization: Bearer zv_<prefix>_<secret>
Pagination
List endpoints are cursor-paginated:
{ "items": [ ... ], "nextCursor": "<id-of-last-item>" }
Pass ?limit=<n> (default 50, max 200) and ?cursor=<nextCursor> for the
next page. Stop when a page returns fewer than limit items — nextCursor
is simply the last item's id, and is only null when the page is empty.
:::note Exception
GET /v1/accounts/{id}/balances returns a plain array (one entry per activated
asset), not a paginated envelope.
:::
Idempotency
All transfer-creation endpoints accept an Idempotency-Key header. Send a
unique key per distinct transfer and reuse the same key to safely retry —
ZenVault returns the original transfer instead of creating a duplicate (it does
not error on replay). Keys are scoped to your workspace and remembered for 24h.
Idempotency-Key: 7c3e6b8a-2f4d-4a9e-9c1b-0e7a5d2f1b9c
If you omit the header, ZenVault generates one server-side — which means a retried request with no key will create a second transfer. Always send one.
Errors
Errors use standard HTTP status codes with this body:
{ "statusCode": 400, "message": "human-readable reason", "error": "Bad Request" }
Validation failures return message as an array of field-level strings.
| Status | When |
|---|---|
400 | Malformed request, validation failure, or business rule violated (e.g. asset not allowlisted on the target wallet) |
401 | Missing or invalid API key |
403 | Valid key but missing the required scope — or the movement was blocked by policy / screening |
404 | Resource not found in your workspace |
409 | Conflict (e.g. duplicate account/wallet label) |
429 | Rate limit exceeded (per-workspace budget) |
Blocked-by-policy and blocked-by-screening responses are ZenVault's own message
("Transfer blocked by policy: …" / "… by KYT screening: …"), never a
passed-through provider error.
Data types
- IDs are UUIDs.
- Amounts are decimal strings (
"125.50"), never floats. - Timestamps are ISO-8601 UTC (
"2026-05-27T10:00:00.000Z"). - Asset canonical keys are
<SYMBOL>_<NETWORK>; see Assets.