# Requests and errors

Source: https://www.scanner.net/docs/api/reference/conventions

> Base URL, authentication, response formats, streaming, extra per-IP limits, scan costs and the errors every Scanner API route can return.

This page covers what every route in the Scanner API reference shares. The route pages link back here instead of repeating it. Key setup, scopes, rotation and plan rules are in [API keys](/docs/api/keys).

Base URL and authentication [#base-url-and-authentication]

Every path in the reference is served under `https://api.scanner.net`. Every request carries a secret key in one header, either `Authorization: Bearer sk_live_...` or `x-api-key: sk_live_...`. Call the API from a server, never from a browser.

```bash
curl "https://api.scanner.net/explorer/address/WALLET_ADDRESS" \
  -H "x-api-key: $SCANNER_API_KEY"
```

Don't send a `key` query parameter. A secret key in the URL is refused with `API_KEY_IN_URL`, an `emb_` key together with a secret key is refused with `INVALID_INPUT`, and any other `key` value is checked as an embed key and refused with `403 EMBED_KEY_INVALID`.

A key is accepted by route group, not by exact path: it works under the groups this reference covers. Account and billing paths return `403 API_KEY_ROUTE_NOT_ALLOWED`, and admin routes return `403`. A path inside an allowed group that doesn't exist returns `404 NOT_FOUND`. A route missing from this reference may still accept a key, but only the routes listed here are documented and supported.

Response formats [#response-formats]

Routes return JSON unless the reference says the route streams. There are three JSON shapes, and each route page says which one it uses:

* **Wrapped:** `{ "success": true, "data": { ... } }`. The explorer, holdings and wallet finder routes use it.
* **Flat with `success`:** `{ "success": true, "wallets": [...], "count": 12 }`. The KOL, alpha, identities, cross-chain and `/v1` routes use it.
* **Bare:** the result object itself, with no `success` field. The scan, detect and bundle routes use it.

Timestamps are ISO 8601 strings unless a field says it is unix seconds or milliseconds. Addresses on Robinhood Chain come back lowercased.

Streaming routes [#streaming-routes]

Routes whose path ends in `/stream` answer with server-sent events (`Content-Type: text/event-stream`). Each event is an `event:` line naming it and a `data:` line holding JSON:

```text
event: transactions_progress
data: {"fetched":250}

event: complete
data: {"address":"...","fundedBy":[...],"scanComplete":true}
```

Use `curl -N` (no buffering) or an SSE client. The final result arrives in the `complete` event, and the stream then closes.

* An error found before the stream opens (a bad address, too few scans) is a normal JSON error with an HTTP status.
* An error after the stream has opened arrives as an `error` event with the usual error body, `{ "error", "code", "requestId" }`, and the stream closes. The HTTP status is already `200` by then.
* Some streams send a `heartbeat` event every 15 seconds while work is running. Ignore it.

Errors [#errors]

Errors use a standard body:

```json
{ "error": "Invalid Solana address", "code": "INVALID_ADDRESS", "requestId": "..." }
```

Some errors add fields, listed in the tables. A few routes answer some errors in a different shape. Their route pages say so, so check `code` only when it is present. Quote `requestId` when you contact support.

Errors any route can return [#errors-any-route-can-return]

These come from key checks and limits that run before the route itself. The route pages list only their own errors.

| Status | Code                          | Meaning                                                                                              |
| ------ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| 400    | `API_KEY_IN_URL`              | The key was sent in a URL. Send it in a header, and roll the key.                                    |
| 400    | `INVALID_INPUT`               | The request carried more than one credential.                                                        |
| 401    | `API_KEY_INVALID`             | The key doesn't exist.                                                                               |
| 401    | `API_KEY_REVOKED`             | The key was revoked, it expired, or its rotation grace period ended.                                 |
| 403    | `API_KEY_BROWSER_NOT_ALLOWED` | The request came from a browser.                                                                     |
| 403    | `API_KEY_ROUTE_NOT_ALLOWED`   | The path is outside the route groups a key can call, such as account or billing.                     |
| 403    | `API_KEY_IP_NOT_ALLOWED`      | The request IP isn't on the key's allowlist.                                                         |
| 403    | `API_PLAN_REQUIRED`           | The plan doesn't include API access. The body includes `upgradeUrl`.                                 |
| 403    | `API_KEY_SCOPE_NOT_ALLOWED`   | The key is limited to other endpoint groups. The body lists them in `allowedRouteGroups`.            |
| 403    | `FORBIDDEN`                   | API access is switched off for the account.                                                          |
| 403    | `EMBED_KEY_INVALID`           | The request had a `key` query parameter. Remove it.                                                  |
| 429    | `API_KEY_RATE_LIMITED`        | The key or account per-minute limit was reached. The body includes `retryAfterSeconds` and `limit`.  |
| 429    | `RATE_LIMITED`                | Too many invalid keys from your IP, or one of the [per-IP limits](#per-ip-limits) below.             |
| 500    | `INTERNAL_ERROR`              | Something failed on our side. Retry, and contact support with the `requestId` if it keeps happening. |
| 503    | `INTERNAL_ERROR`              | Key verification is briefly unavailable. Retry after a short delay.                                  |
| 504    | `TIMEOUT`                     | The request ran past the server's time limit, or an upstream data provider timed out.                |

Errors on paid routes [#errors-on-paid-routes]

Routes that spend scans can also return these:

| Status | Code                   | Meaning                                                                                             |
| ------ | ---------------------- | --------------------------------------------------------------------------------------------------- |
| 402    | `INSUFFICIENT_CREDITS` | Not enough scans left. Before the charge the body includes `credits` (your balance) and `required`. |

Robinhood Chain upstream errors [#robinhood-chain-upstream-errors]

Robinhood Chain explorer and scan routes read from an upstream indexer. When it is down they return `502 UPSTREAM_ERROR`. Retry after a short delay.

Rate limits [#rate-limits]

Every request counts toward the key's and the account's per-minute limit, described in [Rate limits](/docs/api/keys#rate-limits). Responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining`.

Per-IP limits [#per-ip-limits]

A few route groups also have a limit per client IP address, on top of the key limit. These limits are counted per IP, not per key, so several services or customers behind one egress IP share them. When one is reached the route returns `429 RATE_LIMITED`, with the standard `RateLimit` headers.

| Routes                                              | Limit per IP                                             |
| --------------------------------------------------- | -------------------------------------------------------- |
| `/explorer/*`                                       | 60 requests per minute                                   |
| `/rh/explorer/*`                                    | 60 requests per minute, counted apart from Solana        |
| `/wallet-finder/search`, `/rh/wallet-finder/search` | 30 requests per minute per chain                         |
| `POST /identities/batch`                            | 60 requests per minute                                   |
| `GET /holdings/evm/{address}`                       | 20 requests per minute                                   |
| `GET /scan/cached`                                  | The scan limit, 2,000 requests per 15 minutes by default |

Live origin traces (`/explorer/address/{address}/origin` and the Robinhood Chain equivalent) and the explorer's oldest-first transaction walk have a small extra budget per IP. See those routes.

Cost [#cost]

Routes spend scans from the plan's allowance at the same price as the dashboard. Everything else is free, but still counts toward the rate limit.

| Route                                                                                                   | Cost                                                               |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [Wallet scan](/docs/api/reference/wallet-scans), Solana or Robinhood Chain                              | 1 scan at depth 0 (Quick), 2 at depth 1 (Deep), 4 at depth 2 (Max) |
| [Token scan](/docs/api/reference/token-scans), Solana or Robinhood Chain                                | 1 scan, free when a cached result is served                        |
| [Multi-token scan](/docs/api/reference/token-scans#multi-token-scan)                                    | 1 scan per token without a cached result                           |
| [Funding chain trace](/docs/api/reference/funding#funding-chain-stream)                                 | 1 scan, free when a trace from the last 24 hours is cached         |
| [Cross-chain lookup](/docs/api/reference/funding#cross-chain-lookup)                                    | 1 scan for a live search, free for `stored=1` and cached answers   |
| Cached scan reads, explorer, detection, identities, holdings, wallet finder, KOL, alpha, bundles, `/v1` | Free                                                               |

Scans are charged before the work starts. A scan that fails is refunded automatically. Each route page gives the exact cache and refund rules.
