Errors & rate limits
Failures return a JSON object with an error field and a 4xx or 5xx status. This page lists every one, what actually causes it, and whether retrying will help.
Error shape
One consistent envelope, whatever went wrong.
{
"error": "Bad signature"
}A 403 for a missing scope carries one extra field naming the scope that was required:
{
"error": "insufficient_scope",
"required_scope": "trade:read"
}error string only when you need to tell two causes of the same status apart. Wording may be clarified over time; statuses will not change under v1.Every error
Grouped by status, in ascending order.
| Status | error | Cause | What to do | Retry |
|---|---|---|---|---|
| 400 | Account is not ready for trading (missing subaccount_id) | The account exists but has not finished provisioning on the trading network, so no orders can be routed to it. | Wait for provisioning to complete. The account will show as tradable in the dashboard, and you can retry then. | Retry with backoff |
| 400 | <field-specific validation message> | The order body failed validation. The message names the first problem found, such as an unknown execution_type, a malformed accountId, or a wrong field type. | Check the payload against the Placing orders reference. Unrecognised fields are ignored rather than rejected, so a typo'd field name shows up as a missing-value error. | Retry after fixing |
| 400 | 'leverage', 'value', or 'quantity' must be provided | Exactly one sizing field is required on MARKET and LIMIT orders. Sending none, or sending two, is rejected. | Pick a single sizing field per order. | Retry after fixing |
| 401 | Bad signature | The HMAC did not match. The credential is recognised, so the fault is in the canonical string, the body hash, or the secret. | Compare your signer against the worked test vectors on the Authentication page. They isolate which of the three is wrong. | Retry after fixing |
| 401 | Invalid key | No active key matches X-Vanta-Key-Id: the key id is wrong, or the key has been revoked or deleted. | Check the key id, and confirm the key is still listed as active in Settings → Key Management. | Do not retry |
| 401 | Invalid timestamp | X-Vanta-Timestamp is not a positive finite number. It is most often seconds since the epoch, an ISO date string, or a float. | Send whole milliseconds since the Unix epoch as a decimal string. | Retry after fixing |
| 401 | Missing credentials | One or more of the four auth headers is absent or empty. Note that all four are required on every request, including GETs. | Send X-Vanta-Key-Id, X-Vanta-Timestamp, X-Vanta-Nonce and X-Vanta-Signature together. | Retry after fixing |
| 401 | Timestamp invalid | The timestamp is more than 5 minutes away from server time in either direction. Usually a clock drift problem on the caller's machine, or a request that sat in a queue before being sent. | Sync your clock (NTP) and sign immediately before sending. Never reuse a signature from an earlier batch. | Retry after fixing |
| 403 | insufficient_scope | The key does not carry the scope the endpoint requires. The response also includes a required_scope field naming it. | Create a new key. Scopes are fixed at creation time and cannot be added to an existing key. | Do not retry |
| 403 | This API key is only valid for a different account | Each key is bound to exactly one prop account. Either the ?accountId= query parameter or the accountId in the request body names a different one. | Omit accountId on reads (the key already identifies the account), or send the bound account's id on writes. Use one key per account. | Do not retry |
| 404 | Account not found | The bound account no longer resolves for the key's owner. It was deleted, transferred, or never fully provisioned. | Confirm the account still exists in the dashboard, and re-issue the key against a current account. | Do not retry |
| 409 | Nonce | This nonce has already been used by this key. Nonces are stored permanently and are single use for the life of the key. This is the replay protection. | Generate a fresh random nonce per request (16 random bytes hex is ample). Never retry a failed request with the same nonce. | Retry after fixing |
| 429 | rate_limited | The per-key read budget for the current minute is exhausted. The response carries Retry-After plus X-RateLimit-Limit / -Remaining / -Reset. | Back off until Retry-After elapses. Poll on a fixed schedule rather than in a tight loop. | Retry with backoff |
| 429 | too_many_failed_requests | The key accumulated 30 rejected requests within 10 minutes, so order placement is paused for a 15-minute cooldown. Retrying the same failing request in a loop is what triggers this. | Fix the request that keeps being rejected — the error you were receiving before the cooldown names the problem. Any successful request resets the failure count. Wait out Retry-After before trying again. | Retry after fixing |
| 500 | Decrypt failed | The stored key secret could not be decrypted server-side. This is a platform fault, not a problem with your request. | Contact support with your key id (never the secret). | Retry with backoff |
| 502 | <upstream error message> | The upstream trade-history service answered with an error rather than data. | Retry with backoff; if it persists, contact support with the timestamp of the call. | Retry with backoff |
| 503 | Account snapshot temporarily unavailable (PTN validator unreachable); retry shortly | The upstream validator could not be reached. The endpoint fails closed on purpose: it will not return placeholder balances that a polling client would record as real account history. | Skip this cycle and retry with backoff. Do not treat it as a zeroed account. | Retry with backoff |
Rate limits
Read endpoints are capped at 120 requests per minute per key.
The window is fixed rather than sliding: the budget refills at the top of each window rather than gradually. When it is exhausted the response is 429 with these headers:
| Header | Meaning |
|---|---|
| Retry-After | Seconds until the window resets. Wait at least this long. |
| X-RateLimit-Limit | Requests permitted per window. |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix time, in seconds, at which the window resets. |
These headers only appear on a 429
The limit is per key, so separate strategies on separate keys get separate budgets. Order placement does not consume the read budget and is not rate-limited on volume — but it is guarded against sustained failure: a key that accumulates 30 rejected requests within 10 minutes is answered with 429 and a Retry-After header for a 15-minute cooldown. A single successful request resets the failure count, so a working integration never sees this — fix the failing request rather than retrying it in a loop, and handle 429 on any endpoint.
Retrying safely
Two rules that matter more here than on a typical REST API.
- Re-sign every retry with a fresh nonce and timestamp. Replaying an identical signed request returns
409, never a duplicate order. That is protection, not an error to code around. Build the request again from scratch on each attempt. - Order placement is not idempotent. There is no idempotency key. A request that times out may still have been accepted, so before retrying a write, read GET /positions or GET /orders and check whether it landed. Blind retries open double positions.
A 503 is not a flat account
503 on a read means the upstream validator was unreachable, so the endpoint refuses to answer rather than return placeholder numbers. A polling client that treats it as real data will record a zeroed account and may act on it. Skip the cycle and retry.