Using the API
API overview
Base URL, authentication, amount format, errors, rate limits and pagination for every Zanora API route.
Base URL
https://api.zanora.devThe API is HTTPS and JSON only. A request that reaches the gateway over plain HTTP is refused with 403 INSECURE_TRANSPORT before authentication, so a key sent over HTTP isn't accepted.
Authentication
Send your API key in the x-api-key header on every request:
curl -s https://api.zanora.dev/v1/whoami -H "x-api-key: zk.akey_….…"These routes need no key: GET /health, the four signup routes, and the three account-recovery routes (login, password/forgot, password/reset).
| Status | Means |
|---|---|
401 UNAUTHORIZED | missing, mistyped, expired or revoked key |
403 FORBIDDEN | the key lacks the route's scope (the message names it), or the record belongs to another tenant |
Each route below lists the scope it needs. See Accounts and API keys for the full scope table.
Tenancy comes from the key
Routes work out your providerId or workspaceId from your key. Don't send them in request bodies. They're ignored for tenant keys, and a query that names another tenant changes nothing. List routes return your records only.
Amounts
Every amount is a string of integer minor units (cents): "priceMinor": "100" is $1.00. A JSON number is rejected with 400 VALIDATION_FAILED, never rounded. Responses use the same format. See Money and amounts.
Request bodies
- Send
content-type: application/jsononly when there's a body. An empty body with that header is rejected. - Routes whose body is optional accept no body at all.
- Unknown enum values are rejected, not ignored.
Errors
{ "error": { "code": "VALIDATION_FAILED", "message": "priceMinor must be a string of integer minor units", "details": {} } }Handle errors by code. An unexpected error returns 500 with "internal error" and an errorId. Quote the errorId when you contact support@zanora.dev. Every code is listed in Errors and refusals.
Rate limits
Requests are limited per key (or per IP without one), 50 per second by default. Over the limit, you get 429 RATE_LIMITED with a retry-after header. Signup and password-recovery routes have a separate, stricter per-IP limit. If you have many workers, give each its own key rather than sharing one.
Pagination
Lists that grow over time use a cursor, not an offset:
curl -s "https://api.zanora.dev/v1/receipts?limit=50" -H "x-api-key: $KEY"
# → { "receipts": [ … ], "nextCursor": "rcp_…" }
curl -s "https://api.zanora.dev/v1/receipts?limit=50&after=rcp_…" -H "x-api-key: $KEY"nextCursor is present only when the page came back full. An unknown cursor is refused rather than starting again from the top.
Route index
| Area | Routes |
|---|---|
| Signup and accounts | POST /v1/signup, /verify, /resend, GET /v1/signup/:id, /v1/accounts/* |
| Keys and identity | GET /v1/whoami, /v1/auth/self, /v1/auth/scopes, /v1/auth/keys… |
| Discovery | POST /v1/discovery/search |
| Capabilities | POST/GET /v1/capabilities, GET/POST /v1/capabilities/:id, …/deprecate |
| Providers | GET /v1/providers, /:id, …/verification-request, …/rate, …/payout-destination(s) |
| Wallets | /v1/wallets…: create, list, balance, ledger, top-up, deposit address, freeze |
| Policies and approvals | /v1/policies…, /v1/approvals… |
| Receipts | GET /v1/receipts, /:id, /v1/transactions/:id/receipts |
| Payouts and rails | POST /v1/payouts, GET /v1/rail/providers, /v1/rail/transactions |
| Platform | GET /health, /v1/platform/public-key, /v1/platform/receipt-keys/:id |
| x402 protocol | POST /v1/x402/challenge, /authorize, /complete, /fail, for middleware authors |