> The index of these docs, for agents: https://traktor.fun/llms.txt

# Errors

Every error code, over HTTP and on both sockets, with the shape each arrives in.

An error carries a stable `code`, and the code is the only thing to branch on.

## The error envelope

Every REST route answers an error with an HTTP status and this body, except the routes the next section names:

```json
{
  "error": {
    "code": "invalid_parameter",
    "message": "polygon is not a chain",
    "requestId": "req_01m40t5ceg7k2q9xv4ra",
    "details": { "parameter": "chains", "value": "polygon" },
    "docs": "https://traktor.fun/docs/errors#invalid_parameter"
  }
}
```

- `code` names the failure; the tables below list every one.
- `message` is for a person, and may change.
- `requestId` is also the `X-Request-Id` header. Send your own `X-Request-Id` of 1 to 128 letters, digits, `_` and `-`, and the answer carries it in both places.
- `details` holds the code's specifics, such as `parameter`, `value`, `conflictsWith` or `allowed`, and is `{}` when there are none.
- `docs` is the code's row on this page, `https://traktor.fun/docs/errors#<code>`.

An error is never a `200`.

## Where the envelope is shorter

`GET /v1/public/market` and `GET /v1/public/search` answer an error with `code` and `message` only, and no `requestId`, `details` or `docs`:

```json
{ "error": { "code": "invalid_query", "message": "This market query is not supported." } }
```

Stripe's webhook, and a path outside `/v1/` that no route serves, answer the same shape.

A request body over 2 MiB, to any path, is refused with `413` and no body before any route reads it: the status is the answer. A body under that but over a route's own limit gets `body_too_large`.

The sockets refuse an upgrade in shapes of their own; each socket's endpoint block lists its refusals.

- The market stream refuses a handshake that does not offer `traktor.market.v1`, or that carries a query string, with `400` and `{ "error": { "code": "invalid_market_handshake", "protocol": "traktor.market.v1" } }`, which has no `message`. Under a key, its refusals carry an `error` holding `code`, `message` and, where there are any, `details`, at the status its endpoint block lists: a refused key, a spent allowance, a full set of the account's sockets, and `try_later` at `503`, with `Retry-After`, when the key could not be checked. Its `429`, and its `400` for a request that offers the subprotocol, has no query string and is not an upgrade, have no body: the status is the answer.
- The trade stream's refusals hold the code as a string, such as `{ "error": "try_later" }`. Its refusal of an unsupported subprotocol adds `supported`, the subprotocols it speaks.

## HTTP codes

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `invalid_parameter` | 400 | A parameter's value is not one this route accepts, a chain or platform is outside the vocabulary (`details.value`), or one filter key came under both its spellings (`details.conflictsWith`); `details.parameter` names it. | Fix the value, or send one spelling; `details` says what was expected. |
| `unknown_parameter` | 400 | The query names a key this route does not read. | Remove it; `details.parameter` names it. On the market routes `details.accepts` lists the keys they read, and a token-paging key sent without `views=tokens` carries `details.requires` instead. |
| `screen_conflict` | 400 | A floor sits above its own ceiling, such as `minUsd` over `maxUsd`. | Swap or correct the two bounds. |
| `window_out_of_range` | 400 | `windowMinutes` is not a window this route serves. | Use a window from the route's parameter table. |
| `limit_out_of_range` | 400 | `limit` is not an integer from 1 to the route's maximum. | Send an integer from 1 to `details.limit`. |
| `too_many_addresses` | 400 | A list names more addresses or tickers than the grammar allows. | Split the list across requests or subscriptions; in an alert rule, across alerts. |
| `filter_version_unsupported` | 400 | The filter declares a grammar version this API does not speak. | Send `v=1`, or leave `v` out. |
| `invalid_cursor` | 400 | The cursor is malformed, or its signature does not match. | Start again without a cursor. |
| `cursor_query_mismatch` | 400 | The cursor was minted for another filter or another order. | Send the filter and order the cursor came from. |
| `key_in_query` | 400 | A credential-shaped query parameter was sent, such as `key`, `api_key` or `token`. | Send the key in `Authorization: Bearer`, and rotate it: it may now be in proxy logs. |
| `invalid_api_key` | 400 | The body's `apiKey` is missing or is not a string of 1 to 256 characters. | Send `{ "apiKey": "tk_live_…" }`. |
| `invalid_json` | 400 | The body is not valid JSON. | Send a JSON object. |
| `invalid_body` | 400 | The body is JSON but not an object. | Send a JSON object. |
| `unknown_field` | 400 | The body carries a field this route does not take; `details.pointer` names it. | Remove the field. |
| `invalid_alert_name` | 400 | The alert's name is not a string of 1 to 80 characters. | Send a shorter, non-empty name. |
| `invalid_alert_enabled` | 400 | `enabled` is not a boolean. | Send `true` or `false`. |
| `invalid_alert_patch` | 400 | The PATCH names none of `name`, `rule` or `enabled`; one that repeats their current values is accepted and bumps the revision. | Send `name`, `rule` or `enabled` with the `revision`. |
| `invalid_alert_revision` | 400 | `revision` is not a positive integer. | Send the revision you last read. |
| `alert_rule_unsupported` | 400 | The stored rule uses a window, field or match alerts no longer support, so it cannot be enabled as it is. | Edit the rule, then enable it. |
| `invalid_notification_ids` | 400 | `ids` must hold 1 to 100 notification ids. | Send between 1 and 100 ids, or `all: true`. |
| `invalid_notification_bound` | 400 | `through` must be a notification's `createdAt` and `id`, and goes only with `all: true`. | Send `through` from a listed notification, beside `all: true`. |
| `invalid_webhook_url` | 400 | `url` is not an `https://` address on port 443, without credentials, whose host is on the public internet. | Send the endpoint's public https URL. |
| `invalid_unread_filter` | 400 | `unread` is neither `true` nor `false`. | Send `unread=true`, `unread=false`, or leave it out. |
| `invalid_alert` | 400 | `alert` is not an alert id. | Send the `id` of one of the account's alerts, or leave it out. |
| `duplicate_query_parameter` | 400 | A query parameter appears more than once; `details.parameter` names it. | Send each parameter once, several values comma-separated in one: `?chains=bnb,base`. |
| `unknown_query_parameter` | 400 | The query names a parameter this route does not read; `details.parameter` names it. | Remove the parameter. |
| `invalid_limit` | 400 | `limit` is not an integer from 1 to 100. | Send a limit from 1 to 100. |
| `invalid_query` | 400 | The public market or search query is malformed or names a key its resource does not take. | Fix the query against the resource's keys. |
| `invalid_rule` | 400 | The alert rule, or a part of it, is not an object; `details.pointer` names it. | Fix that part so it is a JSON object. |
| `invalid_scope` | 400 | The rule's scope names something the alert grammar does not take; `details.pointer` names it. | Fix the scope against `GET /v1/alert-capabilities`. |
| `invalid_condition` | 400 | A condition is malformed: a missing metric, an unsupported operator, a bound that is not a number. | Fix the condition at `details.pointer`. |
| `unknown_metric` | 400 | A condition names a metric the alert grammar does not know. | Use a metric from `GET /v1/alert-capabilities`. |
| `unknown_signal` | 400 | A signal condition names a signal the feed does not emit. | Use a signal from `GET /v1/alert-capabilities`. |
| `metric_source_mismatch` | 400 | The metric exists, but not on this rule's source. | Use a metric the rule's source offers. |
| `invalid_trigger` | 400 | The trigger's mode, cooldown or hysteresis is outside what alerts support. | Fix the trigger at `details.pointer`. |
| `invalid_source` | 400 | `source` is not `flow`, `token_window` or `signal`. | Send one of the three sources. |
| `invalid_window` | 400 | The rule's `windowMinutes` is not an alert window, or a signal rule names one. | Use a window from `GET /v1/alert-capabilities`; leave it out on a signal rule. |
| `invalid_match` | 400 | `match` needs `op` of `all` or `any` and at least one condition, and a signal rule's `all` names one signal. | Fix `match`. |
| `alert_version_unsupported` | 400 | The rule's `v` is missing or is not 1. | Send `v: 1`. |
| `too_many_conditions` | 400 | The rule holds more conditions than alerts take; `details.limit` says how many. | Split the rule into two alerts. |
| `missing_credentials` | 401 | No bearer token was sent. | Send `Authorization: Bearer $TRAKTOR_API_KEY`. |
| `invalid_key` | 401 | The key is malformed or unknown (the two answer alike), or it is valid but the caller's IP has spent its refused-key budget and the key is not cached yet. | Check the key; if it is right, wait a minute and retry. |
| `key_revoked` | 401 | The key was revoked or has expired, or its account was closed. | Use another key. |
| `session_required` | 401 | This route takes a browser session (`trk_session_…`), and another credential was sent. | Exchange a key at `POST /v1/browser-sessions/exchange` and send that session. |
| `session_expired` | 401 | The browser session is unknown, expired or revoked, or what opened it is gone: its key was revoked or expired, its Telegram unlinked, or its account made inactive. | Exchange a key for a new session. |
| `sign_in_required` | 401 | The Traktor sign-in is unknown, expired or revoked, or its account is suspended or closed. While market data needs a sign-in, which it does not yet, a public market read or a keyless market socket with no sign-in answers it too. | Sign in to Traktor again; a suspended or closed account stays refused. By code, read the market with an API key on the paid routes. |
| `subscription_inactive` | 402 | The account is suspended. | Nothing in the API lifts a suspension. |
| `plan_forbids` | 403 | The request reaches further back than the plan reads; `details.maxHistoryMinutes` says how far it can. | Shorten `windowMinutes`, or move to a plan with more history. |
| `account_inactive` | 403 | The account is suspended or closed, so it cannot open a browser session. | Nothing in the API lifts a suspension or a closure. |
| `web_sign_in_required` | 403 | This credential may not do this: only Traktor's own sign-in on the web may link a group, manage API keys, read the account's usage or billing, open checkout or the billing portal, or connect a Telegram, and only Traktor's own sign-in, on the web or in the app, may read or change the account's saved tokens or the phones its alerts are pushed to. | Do it from traktor.fun or the Traktor app, signed in. |
| `insufficient_scope` | 403 | An AI agent's connection was not granted the scope this route reads, such as `market:read`. | Connect the agent again and grant the scope. |
| `route_not_found` | 404 | No route lives at this path. | Check the path against the reference. |
| `alert_not_found` | 404 | No alert with this id on your account. | List your alerts for their ids. |
| `webhook_not_found` | 404 | No webhook with this id on your account. | List your webhooks for their ids. |
| `telegram_link_not_found` | 404 | This Telegram chat is not linked to your account. | List your links for their chat ids. |
| `method_not_allowed` | 405 | The route does not serve this method; the `Allow` header lists those it does, except on the keyless public market and search routes, which send none and read with `GET` and `HEAD`. | Use a method the route serves. |
| `alert_limit_reached` | 409 | The account holds as many alerts as it may keep. | Delete an alert first. |
| `alert_revision_conflict` | 409 | The alert changed since you read it. | Read it again and resend with its new revision. |
| `alert_once_completed` | 409 | This one-shot alert has already fired. | Duplicate it, or change its rule to arm it again. |
| `webhook_limit_reached` | 409 | The account holds as many webhooks as it may keep. | Delete a webhook first. |
| `cursor_expired` | 410 | The cursor is past its life: `details.reason` is `age`. | Start again without a cursor. |
| `body_too_large` | 413 | The body is over the route's size limit; `details.maxBytes` says it. | Send a smaller body. |
| `filter_too_broad` | 422 | A balance floor would select too many wallets for the views to read. | Raise the floor: `minWalletBalanceUsd` on the metered routes, where `details` holds the numbers and `details.parameter` names it; `minBalance` on the public market. |
| `rate_limited` | 429 | A budget is spent: a per-minute one (the plan's, an IP's or the account's), or, on the keyless public market and search, the server's cap on reads or searches in progress at once or on the IPs it tracks. | Wait for `Retry-After`, then retry. |
| `quota_exhausted` | 429 | The month's REST units are spent, or fewer remain than this request costs; waiting within the period will not help. | Wait for `details.resetsAt`, or move to a larger plan. |
| `preview_rate_limited` | 429 | Too many alert tests in a short time. | Wait for `Retry-After`, then retry. |
| `stream_capacity` | 429 | The public market gateway is at capacity. | Retry after `Retry-After`. |
| `internal_error` | 500 | Something failed on Traktor's side. | Retry; quote `requestId` if it persists. |
| `data_unavailable` | 503 | The data behind the answer is warming up or could not be built; on the metered routes `details.reason` says which. | Retry after `Retry-After`. |
| `market_busy` | 503 | The server is already holding as many market reads waiting on a build, or running as many builds, as it allows. | Retry shortly. |
| `market_fold_pending` | 503 | A committed batch of trades is still being folded into the market read. | Retry shortly, backing off if it persists. |
| `market_warming` | 503 | The public market view has not been built yet. | Retry after `Retry-After`. |
| `market_unavailable` | 503 | The public market read failed, or its resource is switched off on this service. | Retry after `Retry-After`; a switched-off resource sends none, and a retry will not change that. |
| `server_draining` | 503 | The server that took this public market read is shutting down for an update, and it closes the connection with this answer. | Retry after `Retry-After`, on a new connection. |
| `rate_limit_unavailable` | 503 | The rate limiter could not be asked. | Retry after `Retry-After`. |
| `sign_in_unavailable` | 503 | While market data needs a sign-in, the sign-in could not be checked: a store behind the check did not answer. | Retry after `Retry-After`. |
| `telegram_not_configured` | 503 | This service runs without a Telegram bot. | Telegram delivery is unavailable; there is nothing to change on your side. |

## Codes Traktor's own apps answer with

These come only from the profile's key routes, Traktor's Telegram Mini App, the alert worker and Stripe's webhook, so a request made with an API key never meets them.

| Code | Status | Meaning | Where it comes from |
| --- | --- | --- | --- |
| `invalid_alert_batch` | 400 | An alert worker batch failed its own validation. | Raised inside the alert worker, which no client calls. |
| `invalid_saved_tokens` | 400 | `add` or `remove` is not a list of at most 200 strings; `details.pointer` names it. | Only Traktor's own site and app call the route that raises it, to keep the account's saved tokens in step. |
| `invalid_followed_traders` | 400 | `add` or `remove` is not a list of at most 100 strings, or `merge` is not a boolean; `details.pointer` names it. | Only Traktor's own site and app call the route that raises it, to keep the traders the account follows in step. |
| `invalid_push_token` | 400 | `token` is not an Expo push token. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `invalid_push_platform` | 400 | `platform` is neither `ios` nor `android`. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `invalid_push_install` | 400 | `install` is not the key an install of the app makes for itself: 64 lowercase hex characters. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `invalid_key_name` | 400 | A key's name is not a string of 1 to 64 characters, or it holds a control character. | Only the API tab on traktor.fun calls the routes that raise it. |
| `invalid_settings` | 400 | A bot setting is outside what the bot reads, or the change names none. | Only Traktor's Telegram Mini App calls the routes that raise it. |
| `invalid_init_data` | 400, 401 | Telegram's signed init data is missing, expired or not signed for this bot. | Only Traktor's Telegram Mini App calls the routes that raise it. |
| `invalid_agent_client` | 400 | `client_id` names no client registered at `POST /oauth/register`. | Only Traktor's consent page for connecting an AI agent, traktor.fun/connect, calls the route that raises it. |
| `invalid_agent_redirect` | 400 | `redirect_uri` is not one the client registered, so the page sends nobody there. | Only Traktor's consent page for connecting an AI agent, traktor.fun/connect, calls the route that raises it. |
| `invalid_agent_request` | 400 | The authorization request is malformed; `details.error` is the OAuth error the page sends back to the agent, at `details.redirectTo`. | Only Traktor's consent page for connecting an AI agent, traktor.fun/connect, calls the route that raises it. |
| `invalid_signature` | 400 | The `Stripe-Signature` header is missing, malformed, outside its time tolerance, or does not verify against the endpoint's signing secret. | Raised only by Stripe's subscription webhook, `POST /v1/stripe/webhook`, which no client calls. |
| `telegram_not_linked` | 403 | This Telegram is not linked to a Traktor account. | Only Traktor's Telegram Mini App calls the routes that raise it. |
| `telegram_reader_not_found` | 404 | The bot has no chat with this Telegram user yet. | Only Traktor's Telegram Mini App calls the routes that raise it. |
| `agent_connection_not_found` | 404 | No live connection with this id on the account. | Only the connected apps list in Traktor's profile calls the routes that raise it. |
| `key_not_found` | 404 | No live key with this id on the account. | Only the API tab on traktor.fun calls the routes that raise it. |
| `push_device_not_found` | 404 | No phone with this id takes the account's push notifications. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `alert_checkpoint_conflict` | 409 | The worker's checkpoint moved under a batch. | Raised inside the alert worker, which no client calls. |
| `alert_continuation_pending` | 409 | The worker has a fan-out to finish first. | Raised inside the alert worker, which no client calls. |
| `key_name_taken` | 409 | A live key on the account already has this name. | Only the API tab on traktor.fun calls the routes that raise it. |
| `agent_limit_reached` | 409 | The account holds as many connected apps as it may, ten. | Only Traktor's consent page for connecting an AI agent, traktor.fun/connect, calls the route that raises it. |
| `key_limit_reached` | 409 | The account holds as many keys as its plan allows. | Only the API tab on traktor.fun calls the routes that raise it. |
| `already_subscribed` | 409 | The account is on a paid plan or holds a subscription, so checkout has nothing to sell it. | Only the API tab on traktor.fun calls the routes that raise it. |
| `billing_not_started` | 409 | The account has never been through checkout, so the billing portal has nothing to show. | Only the API tab on traktor.fun calls the routes that raise it. |
| `push_token_taken` | 409 | Another account takes this phone's push notifications, and the request came from another install of the app than the one that turned them on. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `agent_connect_off` | 503 | Connecting AI agents is switched off on this server. | Only Traktor's consent page for connecting an AI agent, traktor.fun/connect, calls the route that raises it. |
| `billing_unavailable` | 503 | Checkout and the billing portal are not open on this server, or Stripe could not be reached. | Only the API tab on traktor.fun calls the routes that raise it. |
| `push_not_configured` | 503 | This server pushes no alerts to phones yet. | Only the Traktor app calls the routes that raise it, to turn alert push notifications on or off for a phone. |
| `alert_evaluation_batch_too_large` | 503 | A batch would evaluate more rules than the worker's safety bound. | Raised inside the alert worker, which no client calls. |

## Stream codes

An `error` frame on [the stream](https://traktor.fun/docs/stream.md) answers one frame and never closes the socket. A fault that closes the socket is a close code instead, listed under [close codes](https://traktor.fun/docs/stream.md#close-codes).

| Code | Retryable | Meaning | What to do |
| --- | --- | --- | --- |
| `bad_json` |  | The frame is not JSON. | Send one JSON object per frame. |
| `bad_frame` |  | The frame is not a JSON object, or carries an unknown field, a bad value, or a `from` this service did not mint or one past the newest event in the feed; `detail.pointer` names the field where there is one. | Fix the frame; for `/from`, subscribe again without it. |
| `missing_id` |  | The frame has no `id` of 1 to 64 characters from `A-Z a-z 0-9 _ . : -`. | Give every frame an id. |
| `unknown_op` |  | The op is not one of the client ops. | Use a client op. |
| `not_authenticated` | retryable | A frame other than `auth` or `bye` arrived before `auth`. | Send `auth` first. |
| `already_authenticated` |  | `auth` arrived on a connection that already has an account. | Send `auth` once per connection. |
| `unknown_sub` |  | `unsubscribe` named a subscription the connection does not hold; nothing was removed. | List subscriptions with `subs`. |
| `sub_exists` |  | A subscription with this name is already open. | Choose another name, or unsubscribe first. |
| `sub_limit` |  | The connection holds as many subscriptions as the plan allows; `detail` has `limit` and `inUse`. | Close a subscription, or open another socket. |
| `filter_invalid` |  | The filter fails the flow grammar: an unknown field, a value out of bounds or outside its vocabulary, one key under both its spellings, or a minimum above its maximum; `detail.pointer` names the field, except that a minimum above its maximum leaves it a bare `/filter/`. | Fix the filter; send one spelling of each key. |
| `filter_too_large` |  | A `tokens` or `wallets` list within the grammar's bound is longer than the plan's `wsAllowListMax`; `detail` has `limit` and `got`. | Shorten the list. |
| `filter_too_broad` |  | The connection's filters would score past the plan's `wsFilterScoreMax`; `detail` has `score`, `inUse` and `limit`. | Narrow a filter, or close a subscription. |
| `channel_denied` |  | The frame asks for a channel other than `flows`. | Leave `channel` out, or send `flows`. |
| `rate_limited` | retryable | Frames arrived faster than the plan's inbound rate, or while a burst of them was still waiting on a key check or a replay; `detail.retryAfterMs` says when to send again. | Send fewer frames; persistent flooding closes the socket with 4007. |
| `internal` | retryable | The server failed while handling the frame. | Retry the frame; reconnect if it repeats. |

## Market stream codes

An `error` frame on [the market stream](https://traktor.fun/docs/market-stream.md) answers one command, with its `code` and, when the command carried a valid one, its `id`, except a `stream_capacity` naming a subscription the server closed to make room, which answers none. The socket's closes are listed under [close codes](https://traktor.fun/docs/market-stream.md#close-codes).

| Code | Retryable | Meaning | What to do |
| --- | --- | --- | --- |
| `bad_frame` |  | The frame is not a JSON object, names an op this socket does not have, or carries a field its op does not take. | Send `{ op, id, … }` with that op's fields only. |
| `invalid_id` |  | `id` is not 1 to 64 characters from `A-Z a-z 0-9 _ . : -`. | Give the frame a valid id. |
| `unknown_subscription` |  | `unsubscribe` named a subscription the connection does not hold. | Check the `sub`. |
| `subscription_exists` |  | A subscription with this id is already open. | Use a new id. |
| `subscription_limit` |  | The connection holds as many subscriptions as it may, the server's cap or under a key the plan's. Without a key, also: your IP's connections without a key already read their share of the server's queries, and this one would count toward it. | Unsubscribe from one first. |
| `invalid_query` |  | The query is not a `{ resource, params }` object, or names a resource, a key or a value the public market does not take, or repeats a key. Under a key it is also a resource the paid API does not read, or a chain, app, DEX signal, launchpad or signal type name the grammar does not know, which the keyless socket drops. While market data needs a sign-in, which it does not yet, a socket with no API key, ticket or sign-in is also refused anything but the front door's three reads (`FRONT_DOOR_MARKET_READS`). | Fix the query against the resource's keys. |
| `market_unavailable` |  | The resource is switched off on this service. | Use another resource. |
| `stream_capacity` | retryable | The gateway holds as many market queries as it can. Sent for a subscription you did not just open, it closed that subscription to make room for another reader's new query: your IP, or under a key your account, read at least as many queries as that reader allowed: its whole share for an HTTP read, two for a metered REST read under a key, two more than another IP's or account's connection. A query only you read goes first; a query others read too is closed only when every reader of it reads that many, and then for every one of them. | Retry shortly. |
