# Errors

> The error envelope, every error code the Skylit API returns, and what to do about each one.

Every REST error from the Skylit API has the same JSON shape. This includes the
errors the gateway sends itself (`401`, `403`, `429`, `504`):

```json
{ "error": { "code": "symbol_not_found", "message": "Unknown symbol 'ZZZZ'." } }
```

- `code` is stable and machine-readable. Branch on it.
- `message` is for people. Its wording can change, so never parse it.
- `docs_url`, when present, links to this code's entry on this page. It is optional,
  so don't depend on it.
- More fields may be added to `error` later. Ignore fields you don't recognise.

> **Warning:** **A few edge responses don't use the envelope.** Our CDN firewall answers in front
> of the API: a per-IP rate block is `429` with
> `{"error": "Rate limit exceeded", "message": "..."}` (and `Retry-After`), and a
> blocked request is `403` with `{"error": "Forbidden", ...}` or an HTML page. The MCP
> server's `401` is the OAuth shape `{"error": "unauthorized", ...}`. If `error` is a
> string rather than an object, use the HTTP status and treat the string as the code.

> **Note:** **Case.** Heatseeker, Atlas and gateway codes are lower-case (`symbol_not_found`).
> Flowseeker codes are upper-case (`SYMBOL_NOT_FOUND`), except its billing codes
> (`insufficient_credits`, `account_suspended`, `credit_check_failed`), which are
> lower-case on every API. The same condition has the same name in both cases, so
> **compare codes case-insensitively**. Each code below has a stable anchor: the
> lower-case code, for example `/api-reference/errors#symbol_not_found`.

## How to handle an error

1. Branch on the HTTP status first, then on `error.code`.
2. Retry only `429`, `500`, `502`, `503` and `504`. See
   [Rate limits and retries](https://www.skylit.ai/docs/api-reference/rate-limits-and-retries) for the backoff rules.
3. Never retry any other `4xx` unchanged. Fix the request, or stop.
4. Failed calls are free. A `4xx` or `5xx` is refunded, and its
   `X-Credits-Remaining` header already includes the refund.

| Status | Retry? | What it means |
| --- | --- | --- |
| `400` | No | The request is invalid. Fix it using the message. |
| `401` | No | No API key was sent. |
| `402` | No | Out of credits, or the monthly cap is reached. Stop. |
| `403` | No | Bad key, suspended account, or no access to this data. Stop. |
| `404` | No | Unknown symbol, or nothing to return. |
| `405` | No | Wrong HTTP method. The API is read-only, so use `GET`. |
| `422` | No | The request asks for too much data at once. Narrow it. |
| `429` | Yes | Too fast, or too many requests at once. Wait, then retry. |
| `500`, `502` | Yes, with backoff | Our fault. Retry with backoff. |
| `501` | No | That mode isn't built yet. |
| `503` | Yes, with backoff | Temporarily unavailable. Honor `Retry-After` when present. |
| `504` | Yes, with backoff | The request took too long. Retry, or narrow the window. |

## 400 Bad Request

### invalid_parameter
A parameter has a value the API doesn't accept: not one of the documented values,
out of range, badly formatted, or too many symbols. Flowseeker: `INVALID_PARAMETER`.
The message names the parameter and, for enumerations, the allowed values. Fix the
request. Don't retry it unchanged.

### missing_parameter
A required parameter is missing. Flowseeker: `MISSING_PARAMETER`.

### range_too_large
The time range you asked for is wider than the endpoint allows. Split it into
smaller windows. The endpoint's reference page gives the cap.

> **Info:** **Atlas `/v1/history`** follows the TradingView UDF convention. A window wider
> than its cap is a `400` with `{"s": "error", "errmsg": "...", "requested_days": N,
> "max_days": M}` instead of the envelope. Page through the range in windows of
> `max_days` or fewer.

## 401 Unauthorized

### unauthorized
No API key was sent. Flowseeker: `UNAUTHORIZED`. Send `Authorization: Bearer <key>`.
The API doesn't read `X-API-Key` or keys in the query string.

## 402 Payment Required

### insufficient_credits
Your credit balance is lower than this request costs. Stop and tell the user.
`GET /v1/account` (free) shows the balance. The response carries
`X-Credits-Remaining`.

### monthly_cap_reached
The account has reached the monthly spending cap it set for itself. Requests
resume when the period resets or the cap is raised.

## 403 Forbidden

### forbidden
The key is unknown, revoked or expired. Sent by the gateway. Check the key on the
[Developer page](https://app.skylit.ai/developer).

### account_suspended
The account's API access is suspended. Contact support.

### account_blocked
API access for this account is blocked. Contact support.

### not_entitled
This data isn't enabled for your account. For example, Tempest data is in preview.

### opra_agreement_required
This data needs a signed OPRA subscriber agreement. Sign it in the developer
console, then retry.

## 404 Not Found

### symbol_not_found
Skylit has never seen this ticker or contract. Flowseeker: `SYMBOL_NOT_FOUND`.
In a list parameter, unknown symbols are left out of the result, and you get
`404` only when none of them is known. A known symbol with no activity returns
`200` with empty data, not `404`.

### expiration_not_found
None of the requested `expirations` exist for the symbol. List the valid ones first.

### no_data
The symbol is valid but there is nothing stored for the requested instant or
window. Flowseeker: `NO_DATA`.

### not_found
The path doesn't exist. Flowseeker: `NOT_FOUND`. Check the path against the
API reference.

## 405 Method Not Allowed

### method_not_allowed
The public API is read-only. Use `GET`.

## 422 Unprocessable Entity

### request_too_large
The request would read more data than one call may. Narrow the time range or
the number of symbols.

## 429 Too Many Requests

### rate_limited
The key went over its requests-per-minute limit (your plan's limit, in
`X-RateLimit-Limit` and `GET /v1/account`). The gateway's `429`
has `X-RateLimit-Remaining: 0` and `X-RateLimit-Reset` but no `Retry-After`: sleep
until `X-RateLimit-Reset`, plus a little random jitter, then retry. Flowseeker's
own limiter uses `RATE_LIMITED`. If a `429` does carry `Retry-After`, honor it.

### too_many_concurrent_requests
Too many requests from your account are running at the same time (for example,
more than 2 historical replays at once). Carries `Retry-After`. Send fewer
requests in parallel.

### too_many_concurrent_queries
Flowseeker: `TOO_MANY_CONCURRENT_QUERIES`. As above, for Flowseeker's
per-account in-flight cap. Carries `Retry-After`.

### stream_limit_reached
You have hit the limit on open streams or streamed symbols. Carries
`Retry-After`. Close a stream, or put more symbols on one connection.

## 5xx Server errors

### internal_error
`500`. Something failed on our side. Flowseeker: `INTERNAL_ERROR`. Refunded.
Retry with backoff. If it keeps happening, contact support.

### upstream_error
`502`. A service behind the API failed. Refunded. Retry with backoff.

### not_implemented
`501`. The mode you asked for is accepted but not built yet. Don't retry.

### unavailable
`503`. The data source is temporarily unavailable. Flowseeker: `UNAVAILABLE`.
Refunded. Retry with backoff.

### data_unavailable
`503`. The data for this request can't be read right now. Refunded. Retry with backoff.

### warming_up
`503`. The service is loading data after a restart. Carries `Retry-After`. Wait
that long, then retry.

### stream_capacity
`503`. The streaming service is at capacity. Retry the connection with backoff.

### stream_unavailable
`503`. Streaming is temporarily unavailable. Retry the connection with backoff.

### api_paused
`503`. The API is paused for maintenance. Carries `Retry-After`. Wait that long,
then retry.

### credit_check_failed
`503`. Your credit balance couldn't be checked, so the request wasn't served.
Nothing was charged. Safe to retry with backoff.

### gateway_timeout
`504`. The request didn't finish in time (Flowseeker answers within 25 seconds).
Flowseeker: `GATEWAY_TIMEOUT`. Refunded. Retry, or ask for a smaller window.

## Streams

A Server-Sent Events stream that the server ends sends a final `closed` event with
a `reason`. The reasons are `insufficient_credits`, `account_suspended`,
`monthly_cap_reached`, `credit_check_failed`, `key_revoked` and (Tempest
streams) `access_withdrawn`. Reconnect only
after `credit_check_failed`. For the others, fix the cause first.

## MCP

The [MCP server](https://www.skylit.ai/docs/mcp/overview) reports errors in two ways:

- **Tool errors.** The API rejected the call. The tool result has `isError: true`,
  and its text carries the API error (the same codes as above). Failed tool
  calls are free. Apply the same retry rules as for REST.
- **Protocol errors.** The JSON-RPC message itself was bad: `-32700` (parse error),
  `-32600` (invalid request), `-32601` (unknown method) or `-32602` (invalid
  params: an argument has the wrong name or type, for example a list where the
  tool takes a comma-separated string). Fix the call.

A request to the MCP endpoint with no credentials gets `401` with
`{"error": "unauthorized", "error_description": "..."}` and a `WWW-Authenticate`
header that points MCP clients at sign-in. This body follows the OAuth convention,
not the envelope above.
