Skip to content

Errors

Every failure answers in the same shape: a list, because one request can be wrong in several places at once, and each entry says what is wrong, where, and what to do instead.

{
"errors": [
{
"code": "DQL_UNKNOWN_FIELD",
"message": "unknown field 'fundng' at 1:18; did you mean 'funding'?",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 6
},
"hint": "did you mean 'funding'?"
}
]
}

Branch on code. It is stable. message explains and may be reworded between releases, and hint is advice, not a contract. position is present whenever the fault has a place in the query text: line and column are 1-based, offset is 0-based, and length covers the offending token, so an editor can underline it exactly.

Each of these was produced by running the query beside it through the parser. The messages are the server’s own.

The text is not a query. The position is where reading stopped and the message names what was expected there.

funding >
{
"errors": [
{
"code": "DQL_SYNTAX",
"message": "unexpected end of input at 1:11; expected an expression",
"position": {
"line": 1,
"column": 11,
"offset": 10,
"length": 0
},
"hint": "expected an expression",
"next": "example of a valid query: any(venues where funding > 0)"
}
]
}

Underline the position. POST /v2/dql/parse answers this without running anything, which is what an editor should call on every keystroke.

A name that is not a field. The message suggests the nearest one it knows.

any(venues where fundng > 0)
{
"errors": [
{
"code": "DQL_UNKNOWN_FIELD",
"message": "unknown field 'fundng' at 1:18; did you mean 'funding'?",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 6
},
"hint": "did you mean 'funding'?"
}
]
}

Read GET /v2/dql/catalogue for the fields that exist rather than carrying a list in your client.

A name that is not a function.

any(venues where emma(close, 20) > 0)
{
"errors": [
{
"code": "DQL_UNKNOWN_FUNCTION",
"message": "unknown function 'emma' at 1:18; did you mean 'ema'?",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 15
},
"hint": "did you mean 'ema'?"
}
]
}

The catalogue lists every function with its signature.

A venue code the platform does not have. The message lists the ones it does and the hint names the nearest. Common names (BINANCE-PERP, BINANCE-FUTURES, OKX-SWAP and the like) are not errors: they resolve to the registry code, and the response says so under resolved_from.

any(venues where venue = BINANCE-USDT)
{
"errors": [
{
"code": "DQL_UNKNOWN_VENUE",
"message": "unknown venue 'BINANCE-USDT' at 1:26; known: BYBIT-PERP, HYPERLIQUID, KRAKEN-FUTURES, OKX-PERP",
"position": {
"line": 1,
"column": 26,
"offset": 25,
"length": 12
},
"hint": "known: BYBIT-PERP, HYPERLIQUID, KRAKEN-FUTURES, OKX-PERP"
}
]
}

Read GET /v2/venues. A venue the platform is still onboarding is not queryable until it appears there.

A quote currency that exists on no listing.

any(venues where quote = USDD)
{
"errors": [
{
"code": "DQL_UNKNOWN_CURRENCY",
"message": "unknown currency 'USDD'",
"position": {
"line": 1,
"column": 26,
"offset": 25,
"length": 4
},
"hint": "did you mean 'USD'?"
}
]
}

The catalogue’s currencies is the whole set. DQL never converts between them, so the spelling has to match.

A base asset the platform does not have.

base = BITCOIN
{
"errors": [
{
"code": "DQL_UNKNOWN_ASSET",
"message": "unknown asset 'BITCOIN'",
"position": {
"line": 1,
"column": 8,
"offset": 7,
"length": 7
},
"hint": "known: BTC, ETH, SOL"
}
]
}

The catalogue’s assets is the whole set.

An instrument code that does not exist — often the base asset written where the instrument belongs.

instrument = BTC
{
"errors": [
{
"code": "DQL_UNKNOWN_INSTRUMENT",
"message": "unknown instrument 'BTC'",
"position": {
"line": 1,
"column": 14,
"offset": 13,
"length": 3
},
"hint": "known: BTC-PERP, ETH-PERP, SOL-PERP"
}
]
}

Instruments are BTC-PERP, not BTC. The catalogue’s instruments lists them.

A venue-scoped field used as though it belonged to the instrument. Funding is a fact about one venue’s listing; an instrument has several.

funding > 0
{
"errors": [
{
"code": "DQL_SCOPE",
"message": "'funding' is venue-scoped; wrap it in any(venues where …), all(…), count(…) or an aggregate",
"position": {
"line": 1,
"column": 1,
"offset": 0,
"length": 7
},
"hint": "wrap in any(venues where ...) / all(...) / count(...) or an aggregate"
}
]
}

Say which venues you mean: any(venues where funding > 0), or all, count, or an aggregate.

A quantifier inside a quantifier. There is one level of venues, so the inner one would have nothing to range over.

any(venues where any(venues where funding > 0))
{
"errors": [
{
"code": "DQL_NESTING",
"message": "quantifiers cannot be nested",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 29
}
}
]
}

Flatten the condition.

An operation on a value of the wrong kind. Summing prices across venues is a category error, not a small inaccuracy — the result would be a number with no meaning.

sum(venues, mark) > 0
{
"errors": [
{
"code": "DQL_TYPE",
"message": "sum requires an additive field; mark is a price",
"position": {
"line": 1,
"column": 1,
"offset": 0,
"length": 17
},
"hint": "additive: oi, volume_24h, turnover_24h, bid_size, ask_size, volume, the depth functions, or arithmetic over those"
}
]
}

Sum only additive fields; the catalogue’s additive says which. For a price across venues, use max, min or an average of your own.

A series function with no period to read at. A moving average is a different number on every timeframe, so the platform will not choose one for you.

any(venues where ema(close, 20) > close)
{
"errors": [
{
"code": "DQL_TIMEFRAME_REQUIRED",
"message": "series function 'ema' needs a timeframe: add 'timeframe 15m' or a trailing argument",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 14
}
}
]
}

Add timeframe 1h to the query, or give the function its own: ema(close, 20, 1h).

A period the platform does not store. The message lists the ones it does.

any(venues where sma(close, 3) > 0) timeframe 7m
{
"errors": [
{
"code": "DQL_TIMEFRAME_UNSUPPORTED",
"message": "timeframe '7m' is not supported; supported: 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d",
"position": {
"line": 1,
"column": 37,
"offset": 36,
"length": 9
}
}
]
}

Use a period from the catalogue’s timeframes.

Two series of different periods compared as though they lined up. They do not: one bar of the first is not one bar of the second.

any(venues where crossover(ema(close, 20, 1h), ema(close, 50, 4h)))
{
"errors": [
{
"code": "DQL_TIMEFRAME_MISMATCH",
"message": "series timeframes differ (1h vs 4h)",
"position": {
"line": 1,
"column": 18,
"offset": 17,
"length": 49
}
}
]
}

Read both at the same period.

An argument outside what the function accepts. The message states the range.

any(venues where ema(close, 0) > 0) timeframe 1h
{
"errors": [
{
"code": "DQL_ARGUMENT",
"message": "argument 'n' of ema must be an integer in 1…500",
"position": {
"line": 1,
"column": 29,
"offset": 28,
"length": 1
}
}
]
}

Use a window inside the stated range.

A clause given twice. The platform will not guess which one you meant.

venues_listed > 0 timeframe 1h timeframe 4h
{
"errors": [
{
"code": "DQL_DUPLICATE_CLAUSE",
"message": "clause 'timeframe' given twice",
"position": {
"line": 1,
"column": 32,
"offset": 31,
"length": 9
},
"hint": "each clause may appear at most once"
}
]
}

Give it once.

An instant outside what can be answered — in the future, or before the earliest data retained.

any(venues where funding > 0) as of 2030-01-01T00:00:00Z
{
"errors": [
{
"code": "DQL_AS_OF_RANGE",
"message": "as_of is in the future",
"position": {
"line": 1,
"column": 31,
"offset": 30,
"length": 2
},
"hint": "the server clock is 2026-09-23T12:00:05Z"
}
]
}

The catalogue’s earliest_retained is the floor; the present is the ceiling.

These are about the request rather than about the query, so no query produces them and none is shown.

code status what it means
DQL_JSON_SCHEMA 400 The request body is not the shape the endpoint takes. path points at the offending member, as a JSON pointer.
DQL_VERSION 400 A language version other than 1 was asked for. There is only one.
DQL_LIMIT 400 The request is within the grammar but past a batch limit — too many listings, too many rows, or a response too large. The message states both the figure and the limit.
DQL_COMPLEXITY 400 The query is within the grammar but costs more than the limits allow. POST /v2/dql/parse returns a query’s cost, so this can be seen before it is hit.
AUTH_REQUIRED 401 The endpoint needs a key and none was sent.
AUTH_INVALID 401 The key is unknown, revoked, expired, or the header is not Bearer <key>.
ACCOUNT_DISABLED 403 The key belongs to a disabled account.
KEY_IN_URL 400 A key was sent as a query parameter. Query strings are written to logs and browser history, so the key should be treated as compromised and replaced.
PLAN_LIMIT 403 The request is valid but reaches past what the plan includes — usually further back in history. Carries next with the earliest instant the plan reads for that request and the plan that reads further and, for POST /v2/candles, earliest_from and retry_with (the same request with from moved there). Free, Mini and Standard read every timeframe, each as deep as its timeframe allows (Free 1m 48 hours, 5m and 15m 7 days, 1h 30 days, 4h and 1d 90 days; Mini 7, 30, 90 and 180 days; Standard a year); everything without a timeframe reaches 7 days on Free, 30 on Mini.
PAYMENT_REQUIRED 402 A payable endpoint was called with no credentials, or with an x402 payment that was not accepted. The body carries what to pay (accepts) and, beside it, the way out with a key; error says what was wrong with a payment that was sent. Only when the operator has switched x402 on. You pay for what we recorded, not for what we missed. A key whose plan’s archive files for the month are spent (or that has none) is also answered 402, in the same code and without accepts: limit is archive_files_per_month and price_usd the price of the file.
PAYMENT_MALFORMED 400 The PAYMENT-SIGNATURE header is not a payment: not base64, not JSON, not an x402 version 2 payment of the exact scheme, or a field of it cannot be what it says. Nothing was asked of the facilitator.
NOT_FOUND 404 There is nothing at this path to give: an archive file that does not exist (not archived yet, or a day on which nothing was observed, or a layer the archive does not hold), or any archive path while the archive is off.
RATE_LIMITED 429 A limit of the plan stopped the request; limit in the error names it (requests_per_second, requests_per_day, dql_per_minute, stream_connections, or x402_challenges_per_minute and x402_unpaid_in_flight_per_payer for a request without a key). Retry-After says when to come back.
UNAVAILABLE 503 The platform cannot answer yet; retry in a minute.
ANCHOR_NOT_FOUND 404 GET /v2/anchors/... asked about a day with no anchor (a day is anchored within 24 hours of its end), a listing with no history, or a row that was never stored or that retention has dropped (the day’s root stays).
ANCHOR_NOT_YET 404 GET /v2/anchors/proof asked about a day whose root is not on the chain yet (not computed, computed, submitted, or in conflict). Answered at once, before any history is read; the message names the first anchored day of the network, or says none is yet.
ANCHOR_CHANGED 409 A proof was asked for a row whose part of the day no longer matches what was anchored: history was changed after the anchor. The day’s root and the proofs made before the change still stand.

A 400 from POST /v2/dql/parse is not a failure of the API: it is the answer you asked for.