Errors & rate limits
The envelope
{ "error": { "code": 4001, "slug": "insufficient_funds",
"message": "Required 1300000 SUN, available 420000 SUN",
"field": null, "retryable": true,
"details": { "required_sun": 1300000, "available_sun": 420000 } },
"request_id": "req_01J9Z5P8T3WQ7X" }
| Field | Contract |
|---|---|
code | Stable integer. Never renumbered, never reused for a different meaning. New conditions get new numbers. |
slug | Stable lowercase snake_case name for the same condition. One-to-one with code. |
message | English, human-readable, may change without notice. Show it to a human, never parse it. |
field | The offending request field for validation errors, otherwise null. |
retryable | Whether retrying the identical request could plausibly succeed later. |
details | Optional machine-readable context; shape is specific to the code, documented below where it exists. |
request_id | Also in the X-Request-Id response header. Quote it in support requests. |
Branch on slug or code, never on the HTTP status alone — several distinct conditions share a status (409 covers both an idempotency conflict and an order that cannot be reclaimed, and they call for opposite reactions). Unknown codes will appear: treat one you do not recognise as its status class (4xx “my fault, do not retry blindly”, 5xx “their fault, retry with backoff”). Do not fail closed on an unrecognised slug.
Ranges: 1000–1099 auth/credentials · 1100–1199 rate limiting and quotas · 2000–2099 request validation · 3000–3099 business state · 4000–4099 money · 5000–5099 supply · 9000–9099 internal.
Codes
| Code | Slug | HTTP | When | Retry |
|---|---|---|---|---|
| 1001 | missing_credentials | 401 | One or more of X-API-KEY, X-API-TIMESTAMP, X-API-SIGN absent. A malformed header value (bad base64, wrong length) is 1002, not 1001; 1001 means the header was not sent at all. | No — fix the client. |
| 1002 | invalid_signature | 401 | X-API-SIGN does not match the canonical string the server computed. | No. Re-read the canonical string definition; usual causes are re-encoding the query, signing a pretty-printed body while sending a compact one, or a trailing newline. |
| 1003 | signature_timestamp_skew | 401 | X-API-TIMESTAMP more than 5 s from server time, either direction. details.server_time carries our clock. | Yes, once, after fixing the clock. Run NTP; do not widen the retry loop. |
| 1004 | ip_not_allowed | 401 | Source address not on this key’s allowlist. details.source_ip echoes what we saw. | No. Add it in the dashboard; behind NAT or an egress pool, allowlist the whole block. |
| 1005 | insufficient_scope | 403 | Key valid but lacks the scope this endpoint needs. details.required_scope names it in the one platform permission vocabulary (area.action, e.g. orders.create), documented in openapi.yaml. | No — a key’s scopes are chosen explicitly by the account owner, so widening one is a deliberate act, never an automatic retry. |
| 1006 | key_revoked | 401 | The key was deleted or expired. | No. |
| 1007 | key_inactive | 401 | The key exists but is switched off. | No. |
| 1008 | account_suspended | 403 | The account may read but not order. | No. Contact support. |
| 1009 | replayed_signature | 401 | This exact signature was already used; signatures are single-use within the skew window. | No — generate a fresh timestamp and signature per attempt, including per retry. |
| 1010 | challenge_invalid | 401 | The signup/login challenge could not be accepted: unknown nonce, expired nonce, nonce already used, or a signature that does not recover the address. One code for all four on purpose — a caller who has not proved control of an address learns nothing about which addresses have accounts. details.reason is "invalid" and carries no detail. | Yes, once, after a fresh POST /v1/accounts/challenge. |
| 1011 | bootstrap_token_expired | 401 | The 15-minute bootstrap token from POST /v1/accounts/challenge/verify expired or was used on an operation it does not open (only account creation, the signup deposit address, GET /v1/account and the first key). | Yes — re-run the challenge and verify. |
| 1012 | key_environment_mismatch | 401 | The key’s environment marker does not match the network this host serves: an ak_live_… key sent to the test host, or an ak_test_… key sent to the live one. details.key_environment and details.host_environment name both. Answered before the key is looked up, so it says nothing about whether the key exists. | No — use the key issued for this host. |
| 1013 | session_expired | 401 | No dashboard session cookie, or the session behind it expired or was signed out. | No — sign in again by signing a fresh challenge. |
| 1014 | csrf_token_invalid | 403 | A cookie-authenticated write arrived without an X-CSRF-Token header matching the session’s CSRF cookie. | No — read the CSRF cookie and send it on every write. |
| 1100 | rate_limited | 429 | Per-key or per-IP request budget exceeded. Retry-After says how long; details.scope is key or ip. | Yes — honour Retry-After, then exponential backoff with jitter. Never a tight loop. |
| 1101 | concurrency_limited | 429 | Too many of this account’s orders or batches in flight at once. | Yes, after the in-flight ones settle. Lower your parallelism rather than retrying harder. |
| 1102 | quota_exceeded | 429 | A daily or monthly cap on the account was reached. details.resets_at. | Yes, after resets_at. Retrying before that cannot help. |
| 2000 | malformed_json | 400 | Body is not valid JSON, or the content type is not application/json. | No. |
| 2001 | validation_failed | 400 | A field is missing, of the wrong type, or out of range. field names it; details.constraint describes the rule. Also returned when scopes is omitted from an API key creation — the API does not invent a default permission set. | No. |
| 2002 | empty_patch | 422 | A PATCH with no changeable field in the body. | No. |
| 2003 | tier_unavailable | 422 | Tier syntactically valid but not currently sellable for this resource. details.available_tiers lists what is. Also the energy 1d tier while it is switched off (TIER_1D_ENABLED=false) on POST /v1/orders, batches, quotes and estimates; GET /v1/prices shows that row with available: false. | No. |
| 2004 | quote_mismatch | 422 | quote_id sent alongside explicit order fields that contradict the quote. | No. |
| 2005 | idempotency_key_required | 400 | A batch created with neither client_batch_id nor an Idempotency-Key header. | No. |
| 2006 | duplicate_receiver | 400 | The same address appears twice in one batch. Merge the amounts instead. | No. |
| 2007 | invalid_address | 400 | Not a valid Base58Check TRON address (bad checksum, wrong length, hex form instead of Base58). | No. |
| 2008 | amount_out_of_range | 400 | Below the tier’s minimum or above its maximum. details.min_amount / details.max_amount. | No. |
| 2009 | batch_too_large | 400 | More than 100 receivers, or the total across receivers exceeds the per-batch ceiling. | No. |
| 2010 | invalid_webhook_url | 400 | Not HTTPS, resolves to a private/loopback/link-local address, carries credentials, or exceeds 2048 characters. | No. |
| 2011 | unsupported_contract | 422 | The contract in a transfer estimate is not a TRC-20 token we can simulate. | No. |
| 3001 | order_not_found | 404 | No such order, or it belongs to another account. The two are deliberately not distinguished. | No. |
| 3002 | order_not_cancellable | 409 | A batch cancel (POST /v1/batches/{id}/cancel) arrived after that receiver had been picked up. There is no single-order cancel in v1 — POST /v1/orders charges synchronously, so an order never waits unpaid in a queue and this code cannot arise outside a batch. | No — inspect the order; it is probably already delivered. |
| 3003 | receiver_is_ours | 422 | The receiver is one of the platform’s own addresses. | No. |
| 3004 | receiver_not_activated | 422 | Receiver not activated and activate: false was sent. Nothing was charged. | Yes, with activate: true, or after activating the address yourself. |
| 3005 | quote_expired | 409 | The quote’s expires_at has passed. | Yes — take a fresh quote first. |
| 3006 | price_above_limit | 409 | Live total exceeds max_price_sun. details.quoted_total_sun is what it would have cost. Nothing was charged. | Yes, later — the price moves with the pricing period. Or raise the cap. |
| 3007 | nothing_to_reclaim | 409 | The order never resulted in an active delegation, or it is already expired and returned. | No. |
| 3008 | reclaim_unavailable | 409 | The order was filled from a third-party provider, whose resources we cannot return. details.filled_by says which. | No. |
| 3009 | order_in_terminal_state | 409 | A state change was requested on an order that is already finished. | No. |
| 3010 | idempotency_conflict | 409 | The same client_order_id / client_batch_id / Idempotency-Key reused with a different body. details.original_id points at the first request’s object. | No — a caller-side bug. Either retry verbatim or pick a new key. |
| 3011 | subscription_exists | 409 | This address already has a subscription for this resource. details.subscription_id. | No — patch the existing one. |
| 3012 | subscription_not_active | 409 | An operation needing a live subscription hit a cancelled one. | No. |
| 3013 | webhook_limit_reached | 409 | Two endpoints already registered. | No — delete or patch one. |
| 3014 | webhook_role_taken | 409 | The requested role is occupied. | No — PATCH the other endpoint to swap roles. |
| 3015 | request_in_progress | 409 | An identical request with this idempotency key is still executing. Retry-After suggests when to look again. | Do not retry the create. Wait, then read the object by its client id. A retry risks nothing but wastes a slot. |
| 3016 | account_unfunded | 409 | Unused since 2026-09-26 — no route returns it. It was POST /v1/api-keys on an account with no confirmed deposit; an unfunded account may now create keys and is refused at POST /v1/orders with 4001 instead. Kept so the number is never reused. | — |
| 3017 | api_key_limit_reached | 409 | The account already holds the maximum number of active API keys. A product limit, not an access right. details.limit and details.used. | No — revoke a key you no longer use; the slot is free immediately. |
| 3018 | deposit_rotation_limited | 429 | Operator surface only: POST /v1/admin/accounts/{id}/deposit-address/rotate inside the rotation limits (default 1 per 7 days, 3 per 90 days). details.retry_after (seconds), details.next_allowed_at; Retry-After header. | Yes — after retry_after. |
| 3019 | address_book_full | 409 | POST /v1/account/addresses with a new address on an account whose address book already holds 100 entries. A product limit, not an access right. details.limit and details.used. Re-saving an address already in the book only relabels it and never hits this. | No — delete an entry you no longer use; the slot is free immediately. |
| 3020 | subscription_rule_invalid | 422 | POST / PATCH /v1/subscriptions with a reserve, low and high that break the rule: 131,000 ≤ reserve ≤ 5,240,000, each a multiple of 1,000, low ≥ 65,000, low < high ≤ reserve, high − low ≥ 65,000 (Basic’s low = high = reserve = 131,000 is the one exception). details.violations names each broken rule. | No — fix the numbers or pick a preset. |
| 3021 | subscriptions_unavailable | 409 | Subscriptions are switched off on this deployment (SUBSCRIPTIONS_ENABLED=false): POST /v1/subscriptions, and a PATCH that changes the rule, preset or refill parameters. Reading, pausing, resuming and cancelling existing subscriptions keep working. GET /v1/subscriptions/plans → available says which state is in force. | Yes, later — once available is true. |
| 4001 | insufficient_funds | 402 | Balance below the order total plus activation. details.required_sun, details.available_sun. | Yes — deposit, then retry with the same client_order_id. That is exactly what the idempotency key is for. |
| 4002 | balance_reserved | 402 | Nominal balance sufficient but too much is held against in-flight orders. | Yes, once the in-flight orders settle. |
| 4003 | currency_not_supported | 422 | The brand does not accept this currency for this operation. | No. |
| 4004 | ledger_conflict | 409 | Two concurrent charges raced on the same balance and one lost. Nothing was charged. | Yes, immediately, same idempotency key. Serialise charges per account if you see this often. |
| 5001 | insufficient_supply | 503 | Neither our inventory nor the provider cascade could cover the amount at an acceptable cost. Nothing was charged. | Yes, with backoff — supply returns as rentals expire. For a short tier, falling back to a longer tier often succeeds immediately. |
| 5002 | delegation_failed | 503 | The delegation transaction was built but the chain rejected it or it never confirmed. Any charge is reversed; the order ends failed with refunded_amount_sun set. | Yes — a fresh client_order_id, since the old order is terminal. |
| 5003 | chain_unavailable | 503 | Our TRON node is not answering, so we cannot verify or deliver. | Yes, with backoff. |
| 5004 | provider_unavailable | 503 | Every fallback provider declined or timed out and our own inventory was short. Nothing was charged. | Yes, with backoff. |
| 5005 | receiver_capacity_exceeded | 422 | The receiver cannot accept this much more delegated resource (TRON limits delegations per address). details.max_additional. | No, not as sent — reduce the amount or wait for existing delegations to expire. |
| 5006 | supply_paused | 503 | Sales administratively paused for this resource or tier. details.resource, details.tier. | Yes, later, but not soon — GET /v1/prices will not list a paused tier. |
| 5007 | capacity_unavailable | 503 | Own capacity was short and this order may not overflow to a provider: an on_demand contract, or an on_demand_guaranteed contract past its daily overflow cap. The charge is refunded and the order ends failed. details.supply_policy, details.contract_id. | Yes, with backoff — own capacity returns as rentals expire. |
| 9000 | internal_error | 500 | An unhandled failure. The order’s state is unknown from the response alone. | Yes — with the same client_order_id, the only way to learn whether the first attempt took effect. Never retry a create without one. |
| 9001 | timeout | 504 | The operation exceeded the gateway’s window; outcome unknown. | Yes, same client_order_id. |
| 9002 | not_implemented | 501 | The endpoint exists in this contract but not yet in this deployment (phase 1 does not ship everything). | No. |
Validation codes (2000–2099) are all retryable: false and carry field where a single field is at fault; fix the request, retrying it unchanged fails identically.
The supply guarantee: a 503 from order creation is fail-secure — nothing was stored and nothing was charged, so retrying the identical request with the same client_order_id is always safe. Where a charge happened and delivery then failed (5002), the order exists, is terminal, and the money is credited back: read it and start a new order rather than retrying the old id.
Retry policy
Retry on 429, 5xx, and on 4001/4002/4004 after fixing the cause. Do not retry any 400, 401, 403, 404, or a 409 other than 4004 — those need a changed request. Always send client_order_id on order creation so every retry is free of double-purchase risk. Exponential backoff starting at 250 ms with full jitter, delay capped at 30 s, give up after roughly 60 s of wall-clock time rather than a fixed attempt count — then read the order by its client id to find out what happened. Honour Retry-After whenever present; it overrides your own schedule.
HTTP status map
200 success, also an idempotent replay of a create and a reclaim that had already happened · 201 a new object was created · 202 accepted for asynchronous processing (batches, and a reclaim whose hash had not landed yet — nothing delivered) · 204 success with no body (deletes, cancels) · 400 malformed or invalid request · 401 credentials missing, wrong, stale or from a disallowed IP · 402 not enough money · 403 authenticated but not permitted · 404 no such object for this account · 409 conflicts with current state, including idempotency conflicts · 422 well-formed but semantically impossible · 429 rate or quota limit · 500 unhandled internal failure · 501 contract-defined but not deployed · 503 temporarily unable to serve, fail-secure on creates · 504 upstream timeout, outcome unknown.
Compatibility
The CatFee-, Netts-, TronZap- and FeeSaver-compatible facades translate these codes into each competitor’s numbering; each comparison page under /compare maps them. The native codes above are the source of truth; a facade’s code is a projection and is lossy in places, which each compat document states explicitly.
Rate limits
Per API key, and additionally per source IP. Every response carries
RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Exceeding a limit
returns HTTP 429 with 1100 rate_limited and a Retry-After header.
Default budgets (per key, per second) — the dashboard shows the ones actually in force for your key, and wholesale accounts get higher ones on request:
| Group | Limit |
|---|---|
Order creation (POST /v1/orders, POST /v1/batches) | 30 rps |
Reads (GET on orders, quotes, prices, resources) | 50 rps |
| Account, API keys, webhooks management | 5 rps |