Skip to main content

API conventions

Read this once; it applies to every endpoint.

Base URL

EnvironmentBase URL
Staginghttps://api-staging.zenvault.io/api
Productionhttps://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 itemsnextCursor 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.

StatusWhen
400Malformed request, validation failure, or business rule violated (e.g. asset not allowlisted on the target wallet)
401Missing or invalid API key
403Valid key but missing the required scope — or the movement was blocked by policy / screening
404Resource not found in your workspace
409Conflict (e.g. duplicate account/wallet label)
429Rate 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.