Skip to navigation

Data guide

The conventions every data endpoint follows: authentication, values, lists, symbols, freshness and errors

The conventions every resource follows. The reference documents each resource; this page covers what they have in common.

Authentication

Every request carries a bearer token: Authorization: Bearer <token>. A missing, expired or foreign token is 401 UNAUTHENTICATED, with a WWW-Authenticate header naming the problem.

Values

  • Ids are opaque strings, named after their entity: venueId, instrumentId. An id that does not exist is 404 NOT_FOUND, whatever its shape.
  • Numbers — prices, quantities, amounts — are exact decimal strings such as "0.0000001115". Parse them as decimals, never as floating point. The number of digits carries no meaning.
  • Times are epoch-nanosecond strings named …AtNs or …TsNs.
  • Absent values are always present, as null.
  • Enums are UPPER_SNAKE and only ever grow. Tolerate values you don’t know.

Lists

Every collection returns one page at a time:

{
"items": [],
"nextCursor": null,
"prevCursor": null,
"total": null,
"asOfSeqTsNs": "1758640000123456789"
}
  • limit sets the page size: 1 to 100, default 50. Larger values are reduced to 100, never refused.
  • To move, send nextCursor or prevCursor back as cursor, with the same filters and sort. A cursor is bound to the query it came from.
  • A cursor the API cannot use — edited, expired by a key rotation, or sent with a different query — is 400 INVALID_CURSOR. Drop it and fetch the first page again.
  • sort takes comma-separated properties; a leading - sorts descending. The order is always complete: ties are broken by the id, and absent values sort last, in both directions. On instruments, a statistic is absent for an instrument that has never traded, a 24-hour one while its window is still warming, and any the projection cannot state, such as a turnover it has no price to value or a USD turnover with no current rate. volume24h is in base units and turnover24h in quote units, so only turnoverUsd24h compares markets of different assets: sort=-turnoverUsd24h lists the biggest markets first.
  • include opts into extras, comma-separated. Every collection offers total, the exact count of matching items (null otherwise). Instruments also offer facets and ticker. facets counts the matching instruments per venue, type, base asset and quote asset, each dimension under your filters minus that dimension’s own (venueId, venue and venueId.notIn are the venue dimension’s; search, status and symbol apply to every facet), so a filter panel keeps showing its alternatives: with venueId=300&type=SPOT, venues counts the spot markets of every venue and types every type on venue 300, options included. The count of what you chose is its own entry: SPOT in types, 300 in venues. Each value is what the matching filter takes, with the display alias as label (null for types); values with no matches are omitted; within a dimension the values sort by count, most first, then by value, ids as numbers and type names as text. ticker is each row’s latest statistics, rendered from the same snapshot the statistics sorts read, so a page is ordered by the values it shows; null for an instrument that has never traded.
  • Filters are plain query parameters. Repeat a parameter, comma-separate its values, or both, for “any of”: venueId=300&venueId=301 is venueId=300,301. Different parameters narrow together: type=SPOT&venueId=300 is the spot markets of venue 300. A parameter the resource does not accept is 400 MALFORMED_REQUEST, so a misspelt filter never silently returns the unfiltered list. A filter value that matches nothing is an empty page: an unknown code or symbol, or an id that is not exactly one. A filter that takes an alias in place of an id (venue=okx, baseAsset=BTC) ignores case. An enum filter refuses a value it has never heard of (type=CRYPTO) with 400 MALFORMED_REQUEST. It accepts every documented value and every value its rows carry, removed rows included, so a value you were served stays valid: enums only grow. Its schema says so: each item is anyOf the documented values and any string.
  • The .notIn suffix excludes instead: venueId.notIn=10 matches every venue but that one. Where a resource offers ranges it documents .gte and .lte the same way.
  • search is case-insensitive and resource-specific. Instruments match word prefixes: every word you send must be the start of one of the instrument’s own words, which are the words of its symbol (OKX@BTC/USDT:USDT has OKX, BTC, USDT), of the venue’s own symbol (BTC-USDT-SWAP adds SWAP), of its type (PERPETUAL_SWAP has PERPETUAL and SWAP), its id, and, for a dated future, its expiry as a month, a year, a day and the month with a two-digit year (JUN, 2027, 25, JUN27). So btc usdt finds OKX@BTC/USDT:USDT, okx btc perp the OKX perpetual, 1000 the 1000PEPE markets and every id starting 1000, and one character is enough. Words are runs of ASCII letters and digits: anything else separates them and is otherwise ignored, so btc/usdt is btc usdt, and a search with no ASCII letter or digit is no search at all. Every match is listed; nothing is truncated. Venues match a substring of the code or display name; assets match a substring of the symbol.

Symbols

Every instrument has one canonical symbol, byte-identical to the symbol on the market-data stream, in the grammar VENUE@BASE/QUOTE[:SETTLE[-DDMmmYY[-STRIKE-C|P]]]:

ShapeExample
SpotOKX@BTC/USDT
Perpetual swapOKX@BTC/USDT:USDT (linear), OKX@BTC/USD:BTC (inverse)
Dated futureBYBIT@BTC/USDT:USDT-25Jun27
OptionBYBIT@BTC/USDC:USDC-26Sep26-100000-C (a call; -P is a put)

VENUE is the venue code upper-cased; SETTLE is the asset the contract settles in; the date is the day, the month’s three-letter name and a two-digit year; STRIKE is the strike price. A lookup by symbol is case-insensitive; the symbol served always uses this spelling.

GET /instruments?symbol=OKX@BTC/USDT&symbol=okx@btc/usdt:usdt-25JUN27 looks up to 100 symbols in one request; send limit=100 to get a whole batch in one page. A symbol that names no instrument is simply absent from the page. More than 100 is 400 MALFORMED_REQUEST, a symbol sent twice counting twice: split a longer list into batches. The request line and headers may run to 16 KB, room for a batch of the longest symbols and a bearer token.

A dated product’s maturity is DDMmmYY and nothing else. A name stored with the older YYYYMMDD maturity, such as OKX@BTC/USDT:USDT-20270625 for OKX@BTC/USDT:USDT-25Jun27, finds nothing: convert it before looking it up.

Freshness

Reads carry the position of the event stream they reflect: the X-Immix-As-Of-Seq-Ts header, and asOfSeqTsNs in list pages. It is a sequencer timestamp in epoch nanoseconds, the same clock as seqTs on stream frames: every stream message at or before it is reflected in the response, so you can apply stream updates after it without gaps or repeats. A response built from several sources reports the oldest: an instruments page, which joins the catalogue with the latest statistics, reports the older of the two. When the position is unknown, the header is omitted and asOfSeqTsNs is null.

Errors

Every error uses one envelope:

{
"error": {
"code": "INVALID_CURSOR",
"message": "The cursor is not valid for this request; drop it and fetch the first page again.",
"retryable": false,
"requestId": "5f0c6c1e-8a2b-4f7e-9c3d-2b1a0e9f8d7c"
}
}

Branch on code, never on message. retryable says whether sending the identical request again, after Retry-After or a short pause, can succeed with nothing else changed. Quote requestId (also the X-Request-Id header) when you contact support. To name a request yourself, send X-Request-Id with 8 to 128 of [A-Za-z0-9._-]: it becomes the request’s id, and a value off that grammar is replaced with a fresh one. A conditional write’s 412 VERSION_CONFLICT adds currentVersion beside error; no operation here is one yet.

CodeStatusMeaning
MALFORMED_REQUEST400A parameter is unknown, repeated or out of range
INVALID_CURSOR400The cursor cannot be used with this request
UNAUTHENTICATED401The bearer token is missing or not valid
NOT_FOUND404No such resource: the id names nothing served here
ROUTE_NOT_FOUND404No route serves the path: check it against the reference
METHOD_NOT_ALLOWED405The resource does not support this method
NOT_ACCEPTABLE406Responses are application/json
RATE_LIMITED429Slow down; Retry-After says when
INTERNAL_ERROR500Something went wrong on our side
NOT_IMPLEMENTED501The operation is published but not served yet
PROJECTION_UNAVAILABLE503Reference data is briefly unavailable; retry

Codes are append-only: new ones will appear, and existing ones never change meaning.