> For the complete documentation index, see [llms.txt](https://savefee.gitbook.io/savefee/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://savefee.gitbook.io/savefee/error-codes.md).

# Error codes

## The envelope

Every error uses the same [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807) shape:

```json
{
  "type": "https://docs.savefee.io/errors/wallet-insufficient_balance",
  "title": "wallet.insufficient_balance",
  "status": 402,
  "detail": "Insufficient balance: required 1400000 sun",
  "instance": "/v1/orders",
  "code": "wallet.insufficient_balance",
  "requestId": "…"
}
```

{% hint style="warning" %}
**Branch on `code`, never on `detail` and never on the HTTP status alone.** `422` covers several situations that need opposite handling, and `detail` is written for humans and can change. `code` is the stable contract.
{% endhint %}

Always log `requestId` — it identifies the exact request in SaveFee's logs and is the first thing support will ask for.

## Authentication

| Status | `code`                    | Cause                                                                                | What to do                                                       |
| ------ | ------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `401`  | `api_key.invalid`         | Key missing or malformed.                                                            | Check the `Authorization: Bearer sf_live_…` header.              |
| `401`  | `api_key.revoked`         | The key was revoked in the dashboard.                                                | Create a new key and roll it into your configuration.            |
| `403`  | `api_keys.ip_not_allowed` | The key has an IP allowlist and your source address is not on it. Carries `seenIp`.  | Compare `seenIp` with what you pinned, and update the allowlist. |
| `429`  | `rate_limited`            | Too many requests. Carries a `Retry-After` header, plus `level` and `retryAfterSec`. | Wait `Retry-After` seconds, then retry with exponential backoff. |

{% hint style="info" %}
These apply wherever a key is required. On `POST /v1/broadcasts`, where the key is optional, a header that is not in the `Bearer` scheme is read as no key at all rather than returning `401` — see **POST /v1/broadcasts**.
{% endhint %}

## Validation

| Status | `code`                            | Cause                                                          | What to do                                |
| ------ | --------------------------------- | -------------------------------------------------------------- | ----------------------------------------- |
| `400`  | `validation.invalid_field`        | A field is missing or invalid, or `Idempotency-Key` is absent. | Read `detail` and fix the request.        |
| `413`  | `request.invalid`                 | Body over 1 MiB.                                               | Send fewer items.                         |
| `422`  | `validation.invalid_callback_url` | The callback URL is not a public `https://` URL.               | Use a publicly resolvable HTTPS endpoint. |

Common `detail` strings for `validation.invalid_field`:

| `detail`                                         | Meaning                                  |
| ------------------------------------------------ | ---------------------------------------- |
| `amount must be between 50000 and 200000`        | Outside the allowed range.               |
| `amount must be an integer`                      | A fractional amount was sent.            |
| `amount must be a positive number`               | Missing, zero, negative, or non-numeric. |
| `durationSec must be one of: 60, 300, 900, 3600` | Unsupported duration.                    |
| `toAddress must be a valid Tron address`         | Malformed address or bad checksum.       |
| `resource must be ENERGY or BANDWIDTH`           | Missing or unrecognised resource value.  |
| `Only ENERGY orders are supported`               | `POST /v1/orders` buys ENERGY.           |
| `items[] required …`                             | `items` missing, or not an array.        |
| `Idempotency-Key required`                       | The header was not sent.                 |

## Idempotency

| Status | `code`                    | Cause                                                               | What to do                                 |
| ------ | ------------------------- | ------------------------------------------------------------------- | ------------------------------------------ |
| `409`  | `idempotency.in_progress` | The same key is still being processed. Carries `Retry-After: 2`.    | Retry after the delay; do not switch keys. |
| `422`  | `idempotency.key_reuse`   | The key was reused with a different body after the first completed. | Use a new key for a different order.       |

{% hint style="warning" %}
A completed result is replayed for **24 hours**, and an in-flight key is reserved for **10 minutes**. Past the replay window the same key runs again from scratch and charges again, so cap the age of anything in your retry queue below 24 hours.
{% endhint %}

## Money

| Status | `code`                        | Cause                                                                                | What to do                                              |
| ------ | ----------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `402`  | `wallet.insufficient_balance` | Balance below the total. Carries `requiredSun`.                                      | Top up, then retry **with the same `Idempotency-Key`**. |
| `422`  | `wallet.not_registered`       | A broadcast was sent without an API key from an address that has no SaveFee account. | Send your API key so your account pays.                 |

## Orders

| Status | `code`                         | Cause                                                       | What to do                                                            |
| ------ | ------------------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------- |
| `404`  | `not_found`                    | Unknown id, or an order not created by this key's account.  | Check the id.                                                         |
| `422`  | `order.receiver_undeliverable` | The receiver is not an activated account, or is a contract. | Use an activated, non-contract address, then retry with the same key. |

## Broadcasts

| Status | `code`                             | Cause                                                                          | What to do                                                       |
| ------ | ---------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `400`  | `broadcast.malformed`              | The signed transaction could not be read.                                      | Send the transaction object exactly as your library produced it. |
| `400`  | `broadcast.no_signature`           | The transaction carries no signature.                                          | Sign it before submitting.                                       |
| `409`  | `broadcast.duplicate_tx`           | A broadcast for this `txId` is still being processed, or is already confirmed. | Read the existing broadcast instead of signing a replacement.    |
| `422`  | `broadcast.invalid_tx`             | `txID` does not match the body, or the signature does not match the sender.    | Rebuild and sign again without editing the transaction.          |
| `422`  | `broadcast.tx_would_revert`        | Simulation failed.                                                             | Check the transfer, then sign a new one.                         |
| `422`  | `broadcast.insufficient_usdt`      | The sender holds too little USDT for this transfer.                            | Fund the sender.                                                 |
| `422`  | `broadcast.insufficient_bandwidth` | The sender cannot cover the bandwidth this transaction needs.                  | Fund the sender with a little TRX.                               |
| `422`  | `broadcast.insufficient_time`      | Between 30 and 75 seconds left on the expiration.                              | Extend to \~600 seconds and sign again.                          |
| `422`  | `broadcast.tx_expired`             | Under 30 seconds left, or the expiration has already passed.                   | Sign a fresh transaction.                                        |
| `422`  | `broadcast.unsupported_contract`   | This transaction type cannot be provisioned.                                   | Send a supported transaction — see **Broadcast**.                |
| `503`  | `broadcast.upstream_unavailable`   | A TRON node was unreachable.                                                   | Retry with backoff. Nothing was charged.                         |

Every broadcast error above is raised before the transaction goes to the network, so none of them charge you.

{% hint style="info" %}
`409 broadcast.duplicate_tx` covers an in-flight or confirmed transaction only. Once a broadcast has ended in `failed` or `expired`, the same `txId` is accepted again — that is how you resubmit.
{% endhint %}

## Which errors are safe to retry

|                             |                                                                                                                     |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Retry with the same key** | `402` after topping up, `422 order.receiver_undeliverable` after fixing the address, `409 idempotency.in_progress`. |
| **Retry with backoff**      | `429`, `503`, and network timeouts.                                                                                 |
| **Do not retry unchanged**  | `400`, `401`, `413`, `422`. On `409 broadcast.duplicate_tx`, read the existing broadcast rather than resending.     |
