Reference

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 response
{
  "error": "Bad signature"
}

A 403 for a missing scope carries one extra field naming the scope that was required:

403 insufficient_scope
{
  "error": "insufficient_scope",
  "required_scope": "trade:read"
}
Match on the HTTP status first and the 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.

StatuserrorCauseWhat to doRetry
400Account 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<query-specific validation message>A query parameter failed validation. GET /api/v1/trading/payouts answers "limit must be an integer between 1 and 200" for a limit that is not a positive integer, and "invalid cursor" for a cursor that was not returned by the API.Send limit as a plain positive integer, and pass nextCursor back exactly as you received it.Retry after fixing
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 providedExactly 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
401Bad signatureThe 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
401Invalid keyNo 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
401Invalid timestampX-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
401Missing credentialsOne 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
401Timestamp invalidThe 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
403insufficient_scopeThe 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. GET /api/v1/keys/self lists the scopes a key carries.Do not retry
403<personal limit message>The account's owner set personal limits on it in the trading terminal (a daily loss limit, a cap on new entries per day, or a maximum order size), and one of them forbids this order, which would open a position or add to one. The message names the limit, for example "Locked until 00:00 UTC — your personal daily loss limit of $500 was reached. You can still close and reduce.", "You've reached your personal limit of 5 new entries today. It resets at 00:00 UTC. You can still close and reduce." or "This order is larger than your personal maximum order size of $10,000.". Only orders that add exposure are refused: closes, reductions, FLAT_ALL, LIMIT_CANCEL and BRACKET (TP/SL) orders pass, as does a LIMIT_EDIT of a position's TP/SL legs, with two exceptions. A reduction of a position opened or added to since the account's latest snapshot reads as an opener on that snapshot, so for up to 15 seconds after the snapshot is saved, while the network is slow or unreachable, it can be refused; a FLAT of that position always passes. And a close cancels only TP/SL orders, so a resting LIMIT can still fill after its position is gone: a LIMIT against the position counts as an opener when, with the other resting orders on its side, it is larger than the position. A LIMIT sized by value on a forex pair is not measured against the position and passes. An edit of a resting LIMIT (a LIMIT_EDIT with its order_uuid) that keeps its side and sends a quantity no larger than the one resting, such as a price change, passes; an edit that grows the order or turns it to the other side is judged like a new LIMIT in its place.Close or reduce instead (if a reduction is refused, send FLAT, or retry once the network answers), send a smaller order for the size limit, or wait for 00:00 UTC, when the loss lock ends and the entry count resets. Neither ends early. A limit the owner loosens takes effect at the next 00:00 UTC; one they tighten applies at once. Each refusal counts toward the too_many_failed_requests cooldown, so do not resend the same order in a loop.Retry after fixing
403This API key is only valid for a different accountEach 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
404Account not foundThe 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
409NonceThis 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
429rate_limitedThe 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
429too_many_failed_requestsThe 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
500Decrypt failedThe 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
503Account snapshot temporarily unavailable (PTN validator unreachable); retry shortlyThe 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
503Trade pairs temporarily unavailable (validator unreachable); retry shortlyGET /api/v1/trading/trade-pairs could not read the network's pair catalog, or the catalog came back without an allow-list. The endpoint fails closed: it never answers an empty list in place of an outage.Retry with backoff. Keep the last list you read until a new one arrives; do not treat it as an account that may trade nothing.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:

HeaderMeaning
Retry-AfterSeconds until the window resets. Wait at least this long.
X-RateLimit-LimitRequests permitted per window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time, in seconds, at which the window resets.

These headers only appear on a 429

Successful responses do not currently carry the rate-limit headers, so you cannot read your remaining budget from a 200. Track your own request rate rather than relying on the server to tell you when you are close.

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.