> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.immix.xyz/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.immix.xyz/_mcp/server.

# Reference data guide

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:

```json
{
  "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.
* `include` opts into extras, comma-separated. Every collection offers `total`, the exact count
  of matching items (`null` otherwise). Instruments also offer `facets` (counts per venue, type,
  base asset and quote asset, each counted under your filters minus that dimension's own, so a
  filter panel keeps showing its alternatives) and `ticker` (each row's latest statistics).
* 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.
* 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** of
  the symbol's tokens, the venue's own symbol, the type and the id: `btc usdt` finds
  `OKX@BTC/USDT:USDT`, `perp` finds every perpetual swap. 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]]]`:

| Shape          | Example                                                        |
| -------------- | -------------------------------------------------------------- |
| Spot           | `OKX@BTC/USDT`                                                 |
| Perpetual swap | `OKX@BTC/USDT:USDT` (linear), `OKX@BTC/USD:BTC` (inverse)      |
| Dated future   | `BYBIT@BTC/USDT:USDT-25Jun27`                                  |
| Option         | `BYBIT@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:

```json
{
  "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 the same request may succeed
later. Quote `requestId` (also the `X-Request-Id` header) when you contact support.

| Code                     | Status | Meaning                                          |
| ------------------------ | ------ | ------------------------------------------------ |
| `MALFORMED_REQUEST`      | 400    | A parameter is unknown, repeated or out of range |
| `INVALID_CURSOR`         | 400    | The cursor cannot be used with this request      |
| `UNAUTHENTICATED`        | 401    | The bearer token is missing or not valid         |
| `NOT_FOUND`              | 404    | No such resource                                 |
| `METHOD_NOT_ALLOWED`     | 405    | The resource does not support this method        |
| `NOT_ACCEPTABLE`         | 406    | Responses are `application/json`                 |
| `RATE_LIMITED`           | 429    | Slow down; `Retry-After` says when               |
| `INTERNAL_ERROR`         | 500    | Something went wrong on our side                 |
| `NOT_IMPLEMENTED`        | 501    | The operation is published but not served yet    |
| `PROJECTION_UNAVAILABLE` | 503    | Reference data is briefly unavailable; retry     |

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