# Errors & rate limits

Source: https://tenergy.me/docs/errors
Last updated: 2026-09-27

## The envelope

```json
{ "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](https://tenergy.me/compare/catfee) 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 |

## Next steps

- [Authentication](https://tenergy.me/docs/authentication) — the `1xxx` codes come from here; sign each attempt afresh.
- [Webhooks](https://tenergy.me/docs/webhooks) — `order.failed` carries the same `failure.code` and `slug`.
- [Orders reference](https://tenergy.me/docs/api/orders) — which endpoint answers which code.
