# Errors and rate limits

Production code needs to know which failures to retry, which to fix and how fast it may call. You will read the error envelope, retry the right things with back-off, make writes safe to repeat and stay under the per-IP rate limits. The reference tables are in [Responses & codes](https://docs.rarebl.com/responses-and-codes) and [Rate limits](https://docs.rarebl.com/rate-limits).

## Before you start

- Every reply, success or error, has the same envelope `{status, code, message, data, go, meta}` and the HTTP status matches the code.
- Branch on `code` and `data.reason`, never on `message`: messages can change.
- Every reply carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-Request-Id`. Log the request id (`meta.requestid`, the same value): support will ask for it.

## Step 1 — Read the error

An error has `status: 0`, an `API*` code and often a `data` object with the detail.

```json
{ "status": 0, "code": "API422", "message": "The node rejected the transaction.", "data": { "reason": "node_rejected", "detail": "min relay fee not met" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } }
```

```js JavaScript
const res = await fetch(url, options);
const body = await res.json();
if (body.code !== 'API200') {
  const { reason, param, detail } = body.data ?? {};
  console.warn(body.code, reason ?? param, detail, body.meta?.requestid);
}
```

```python Python
try:
    c.broadcast("btc", hex_tx)
except rarebl.ApiError as e:
    print(e.code, e.reason, e.detail)            # for example API422 node_rejected "min relay fee not met"
```

```php PHP
try {
    $c->broadcast('btc', $hex);
} catch (Rarebl\ApiException $e) {
    error_log($e->apiCode . ' ' . $e->reason . ' ' . $e->detail);
}
```

**You should see** the code, the reason and the node's detail. The SDKs raise typed errors (`ValidationError`, `ConflictError`, `UnprocessableError`, `RateLimitError` and others) that carry the same fields.

## Step 2 — Decide: retry, wait or fix

| Code | Meaning | What to do |
| --- | --- | --- |
| `API429` | Rate limited. | **Wait** `Retry-After` seconds, then retry. |
| `API502` | A node is unavailable, syncing or timed out. | **Retry** with back-off. |
| `API500` | Internal error. | Retry once or twice; report it with the request id. |
| `API409` | Not available yet on this chain, or a precondition is missing (`data.reason` says which). | Not a failure of your request. Start the watch on `not_watched`; otherwise try later. |
| `API422` | Well-formed, but the node or our checks reject it. | **Change the transaction** (fund it, raise the fee, rebuild). Do not retry it unchanged. |
| `API400`, `API403`, `API404`, `API405` | A bad parameter, a policy refusal, a missing object or a wrong method. | **Fix the request.** Do not retry unchanged. |

## Step 3 — Retry with back-off

Retry only what can succeed later: `API429` and `API502` (and `API500` briefly). Honour `Retry-After`, double the delay up to a ceiling and add jitter.

```js JavaScript
async function call(url, options, tries = 4) {
  let delay = 1000;
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url, options);
    const body = await res.json();
    if (body.code === 'API200') return body.data;
    if (body.code === 'API429') delay = Number(res.headers.get('Retry-After') ?? 1) * 1000;
    else if (body.code !== 'API502' && body.code !== 'API500') throw Object.assign(new Error(body.message), { code: body.code, data: body.data });
    await new Promise((r) => setTimeout(r, delay + Math.random() * 250));
    delay = Math.min(delay * 2, 30000);
  }
  throw new Error('gave up');
}
```

```python Python
import random, time

def call(fn, tries=4):
    delay = 1.0
    for _ in range(tries):
        try:
            return fn()
        except rarebl.ApiError as e:
            if e.code not in ("API429", "API502", "API500"):
                raise                                   # fix the request or the transaction
            time.sleep(delay + random.random() * 0.25)
            delay = min(delay * 2, 30)
    raise RuntimeError("gave up")
```

**You should see** transient failures absorbed and permanent ones surfaced at once.

## Step 4 — Make writes safe to repeat

A retried broadcast must never pay twice. Send an `Idempotency-Key` on [Broadcast](https://docs.rarebl.com/api/tx/broadcast): the same key, or the same txid, within 24 hours returns the first result. Use one stable key per payout (for example your payout id), and queue payouts instead of firing them in a burst: the write class allows 6 per minute (burst 3) and 300 per day.

```bash cURL
curl -X POST https://api.rarebl.com/open/tx/broadcast -H "Content-Type: application/json" \
  -H "Idempotency-Key: payout-7781" -d '{"chain":"btc","hex":"0200000001…"}'
```

**You should see** `alreadyKnown: true` (or the same txid) if you send it again.

## Step 5 — Stay under the rate limits

Limits are per IP address, by rate class; each class is its own bucket, so a burst of builds does not use up your reads.

| Class | Endpoints | Limit |
| --- | --- | --- |
| read | v1 reads, UTXOs, nonce, resources, outspends, market, `hooks/get`, `hooks/deliveries` | 5 per second, burst 20 |
| compute | decode, estimate, build, assemble (without broadcast), address validate | 2 per second, burst 10 |
| write | broadcast, assemble with `broadcast: true`, hosted sign, send and new address | 6 per minute, burst 3; 300 per day |
| watch | address watch | 30 per minute; 2,000 addresses per day |
| hooks | create, update, pause, resume, delete, test, replay | 10 per minute; 100 active subscriptions per IP |
| stream | `wss://api.rarebl.com/ws` | 3 connections; 200 channels per connection |

- Prefer [webhooks](https://docs.rarebl.com/guides/webhooks) or the [WebSocket](https://docs.rarebl.com/guides/websocket) to polling, cache what does not change, and poll no faster than the data changes ([Data freshness](https://docs.rarebl.com/data-freshness)).
- Many users behind one address (an office, a carrier, your own backend) share one bucket. Run heavy work from your own server.

## Common errors

| Reason | What it means | What to do |
| --- | --- | --- |
| `API429` | A bucket is empty. | Wait `Retry-After`; slow down or cache. |
| `API409` `not_watched` | BTC, LTC or BCH address not watched and active. | [Watch it](https://docs.rarebl.com/guides/watch-addresses). |
| `API409` `tx_index_pending`, `history_unavailable`, `before_history_start`, `no_mempool_view` | Data that is not available on this chain right now. | See [Coverage and limits](https://docs.rarebl.com/limits-today). |
| `API422` `insufficient_funds`, `dust_output`, `fee_too_low`, `fee_too_high` | The transaction cannot be built or relayed as asked. | Fund it, change the amount or the fee. |
| `API422` `inputs_spent`, `inputs_unknown` | An input is spent or unknown. | Re-list the UTXOs and build again. |
| `API422` `bad_signature`, `nonce_too_low`, `chain_id_mismatch`, `tx_expired`, `tx_too_large` | A signing or chain-rule problem. | Rebuild and sign again. |
| `API422` `node_rejected` | The node refused it; `data.detail` has its message. | Read `detail`. |
| `API403` `risk_not_acknowledged`, `key_mismatch` | Hosted signing: the header is missing, or a key does not match. | See [Hosted signing](https://docs.rarebl.com/guides/hosted-signing). |
| `API502` | A node is unavailable. | Retry with back-off. |

## Next steps

- [Responses & codes](https://docs.rarebl.com/responses-and-codes) and [Rate limits](https://docs.rarebl.com/rate-limits): the full tables.
- [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction): the errors specific to building and broadcasting.
- [Coverage and limits](https://docs.rarebl.com/limits-today): what is not available on which chain.
