> This page is for Trading.

> 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.

# REST API guide

The consumer's reference for the Immix API — the REST API an organisation uses for its users,
venue credentials and accounts, the balances those accounts hold at their venues, addresses,
portfolios and policies, the orders it places, and the algo instances it runs.
The contract itself is the OpenAPI document the gateway serves at `/openapi.json` (rendered
by Swagger UI at `/docs`); this guide is the long form of the rules that document's first
screen states in eight lines, and the [changelog](/trading/changelog/rest-api) records every
change to either. Read `GET /me` first: it tells you who the platform holds you to be and what
you may do.

Contents: [Authentication and admission](#authentication-and-admission) ·
[Principals](#principals) · [Reads](#reads) · [Writes](#writes) ·
[Maker-checker](#maker-checker) · [Money](#money) ·
[Ids, sentinels, enums and sets](#ids-sentinels-enums-and-sets) · [Errors](#errors) ·
[Orders](#orders) · [Balances](#balances) · [Algos](#algos) ·
[The organisation lifecycle](#the-organisation-lifecycle) ·
[WebSocket tickets](#websocket-tickets) · [Versioning](#versioning)

## Authentication and admission

Every request carries a bearer token (`Authorization: Bearer …`). There are two lanes:

* **JWT** (dev and prod): a token from the tenant's identity provider, verified against its
  JWKS with the audience and issuer pinned. The token's `org_id` claim names your
  organisation and its `sub` names you; a machine-to-machine client's `sub` is
  `<client_id>@clients` and binds to a `SERVICE` user.
* **Roster** (local and test): a static per-user token bound to a user by name.

Either way the token only authenticates. Authorisation is the platform's: nothing on a token
is a role, a permission or an organisation — the live organisation and user records decide,
at request time. What the door can answer:

| Status | `error.code`          | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 401    | `UNAUTHENTICATED`     | No token, or one that does not verify.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 403    | `AWAITING_ADMISSION`  | You verified, but the platform holds no active user for you yet. Your first request registers you as a `DISCOVERED` user with no capabilities; until an administrator of your organisation admits you (`PUT /users/{id}` with a capability set), every route but `GET /me` answers this — and `GET /me` serves your row, so a client can say "awaiting admission" with the organisation's name.                                                                          |
| 403    | `ORG_NOT_ACTIVE`      | Your organisation is not `ACTIVE`: suspended, pending activation, retired, or not yet registered. For an organisation the platform has never met, the door registers it from the token's `orgName` and `productProfile` claims, and its first member under it, and the message says it awaits activation by the platform ([the organisation lifecycle](#the-organisation-lifecycle)); a token without those claims is refused with the message naming the missing claim. |
| 403    | `FORBIDDEN_PRINCIPAL` | A token carrying no `org_id`, or an `org_id` or `sub` that is not plain ASCII; a `DISABLED` user; or (roster) a binding with no organisation or user.                                                                                                                                                                                                                                                                                                                    |
| 503    | `NOT_PRIMED`          | This member is still catching up; retry shortly. It is checked first, so a booting member answers nothing else.                                                                                                                                                                                                                                                                                                                                                          |

Admission is checked on every request before anything else about it: a malformed request
from a caller the platform has not admitted is refused 403, not 400.

**Capabilities.** What you may do is your user's `capabilities`: an array of scope strings,
`<resource>:<verb>` — `users:manage`, `credentials:propose`, `credentials:approve`,
`accounts:propose`, `accounts:approve`, `account-addresses:propose`,
`account-addresses:approve`, `portfolios:manage`, `asset-policies:propose`,
`asset-policies:approve`, `chain-policies:propose`, `chain-policies:approve`,
`trading-policies:propose`, `trading-policies:approve`, `orders:trade`, `orgs:propose`,
`orgs:approve`, `algos:manage` — served on every user row and on `GET /me`. Every scope stands alone
(`approve` does not imply `propose`); an empty set is a read-only member; there are no roles.
A write your capabilities do not cover is refused by the record's owner, on the platform's
stream, as 403 `CAPABILITY_DENIED` — the gateway itself gates nothing, save the credential
intake ([Writes](#writes)), which checks `credentials:propose` before it stores any key
material and answers the same code. Granting capabilities takes the scope strings or a
`preset` (`INITIATOR`, `APPROVER`, `ADMIN`, `TRADER`, `TREASURY`, `PLATFORM`) the door
expands to a set; the document publishes each preset's expansion as
`x-immix-preset-capabilities` on the `preset` property.

## Principals

Your organisation and your user are taken from the token and the platform's records, never
from the request:

* No request carries an `orgId` to choose an organisation. Every read is scoped to your
  organisation by construction and no parameter widens it; another organisation's record is
  `404 NOT_FOUND`, indistinguishable from one that does not exist.
* No write carries a `userId`: it is stamped from the admitted caller, and a body that states
  one is refused 400 `MALFORMED_REQUEST`. Rows carry `orgId` and `userId` for reading. An
  order row also carries `algoInstanceId` — the platform's own algo instance that placed it,
  beside a `userId` of `0`; `null` on every order a person placed — and an order write that
  states one is refused the same way: no request through this edge is machine flow.
* `GET /me` is the one read that describes the caller: `userId`, `orgId`, `orgName`, `name`,
  `displayName`, `kind`, `status`, `capabilities`, the organisation's `requiredApprovals` and
  whether it is the platform's own (`isInternalPlatform`). Configure a client from it, and
  from nothing in the token.

## Reads

`GET /{resource}` lists your organisation's rows and `GET /{resource}/{id}` serves one, for
`orgs`, `users`, `credentials`, `accounts`, `account-addresses`, `portfolios`,
`asset-policies`, `chain-policies`, `trading-policies`, `orders` and `executions`;
`GET /account-balances` lists your accounts' latest observed balances (a collection only —
see [Balances](#balances)), and the algos collections are under [Algos](#algos).

Every collection answers a page:

```json
{"globalSequence": "1042", "asOfSeqTsNs": "1759570000123456789",
 "items": [], "nextCursor": "AQAAb3…", "prevCursor": null, "total": null}
```

Every member is on every page, `null` where it does not apply.

* **Order.** Each collection has one order, stated on its operation. `orders`, `executions`
  and an algo instance's orders are feeds, newest first: by descending id, and ids rise with
  creation. Every other collection ascends by its key: the row's id; `accountId` then
  `assetId` for balances; the sequence number for an instance's documents; the pool, then
  each entry's place in its pool's report, for the algo catalogue.
* **`limit`** is the page size, 1 to 100. A larger value is clamped, never refused. Without
  `limit` the whole collection is served as one page.
* **`cursor`** is the `nextCursor` or the `prevCursor` of a page you were served, sent with
  the same filters. It is opaque: send back exactly what you were given. `nextCursor` is
  `null` on the last page and `prevCursor` on a page that starts at the head. A cursor does
  not carry `limit` or `include`; send them with every request that wants them.
* **`include=total`** adds `total`: the exact number of rows matching the filters, the whole
  walk's and not the page's. Without it `total` is `null`.
* **`INVALID_CURSOR`** (400) refuses a cursor that does not decode, or that came from another
  query: another collection, filter, view or organisation. Drop it and fetch the first page.

**Walking a collection.** Pages are read from live state, so a walk is not a snapshot. Each
page states the position it was read at, and a row that changed between two pages is served
as it stood when its page was read: merge pages by the row's key, never append them.

A finished walk holds every row that was in the collection at its first page's position and
stayed in it. A row that joined later is in the walk only if it joined ahead of the walker.
That is always so where rows join in key order, which is every unfiltered list of rows keyed
by an id, on the default view: ids rise with creation, so a new row lands after an ascending
walker, and above the head of a newest-first walk, where fetching the first page again reads
it. It is not so where a row can join anywhere in the order: balances, the algo catalogue,
and any list narrowed by a filter or by `view=approved`, which a row enters by changing. So
take a walk as complete as of its first page's position, and learn what came after from the
stream or from another walk.

Walking back, a page with fewer rows before it than `limit` asks for is the head page — the
first `limit` rows, exactly what a request without a cursor serves — so it can repeat rows
you already hold, which merging by key absorbs. A member below `0.13.0` ignores the three
parameters and serves the whole collection, in its own order, with `globalSequence` and
`items` alone: take a page without a `nextCursor` as the end.

* **Two views.** `?view=latest` (the default) is the review surface: while a proposal is
  pending on a row, the proposed content. `?view=approved` is the enforcement surface: what
  the platform enforces now. On resources without proposals (`users`, `account-addresses`,
  `portfolios`) the two coincide; `orders`, `executions` and `account-balances` have one
  surface and refuse `view=approved` (400).
* **Position.** Every response served from the platform's state — the 200s and the 404s —
  says where this member's view of that state stood, twice. `X-Immix-Global-Sequence` is the
  position of the last change it had applied; 200 bodies restate it as `globalSequence`.
  `X-Immix-As-Of-Seq-Ts` is the platform's timestamp of that same change, in epoch
  nanoseconds; a page restates it as `asOfSeqTsNs`. Both advance on every change the gateway
  sees, records, orders and balance readouts alike: a reader at position S has seen every
  record, every order and execution and every balance reading at or before S. The position
  is the exact order, and it restarts if the platform's stream is ever started afresh; the
  timestamp keeps rising across such a restart, and two changes can share one, which the
  position tells apart. A response holds every change at or before the position it states,
  and may already hold later ones, because its rows are read after its position is. So a
  stream frame at or before that position is already in the response; apply a later one by
  the row's key and version, never as a new row. Until this member has applied its first
  change there is no timestamp to state: the header is absent and `asOfSeqTsNs` is `null`. A write's
  answer carries the position of the fact that answered it and no timestamp. Error bodies
  never carry a position field, and a refusal at the door (400, 401, 403, 503) carries
  neither header.
* **Version.** An entity response carries `ETag: "<version>"`, the row's version — the token
  `If-Match` takes on a write. Executions are immutable and carry no version, so no `ETag`;
  balances are readings that supersede one another and carry none either.
* **Filters.** `GET /users?status=DISCOVERED|ACTIVE|DISABLED` narrows the user list
  (`DISCOVERED` is the admission queue); `GET /executions?orderId=` narrows executions to
  one order's fills (an order outside your organisation yields an empty collection);
  `GET /account-balances?accountId=` narrows balances to one account's pools (the same
  rule for an account outside your organisation). An unknown value, or `?status=` on any
  other collection, refuses 400.
* **No other parameter.** A read takes the query parameters its operation declares and
  refuses any other 400 `MALFORMED_REQUEST`, so a misspelt filter never serves the unfiltered
  collection as if it were the narrowed one. A collection takes its filters and the page's
  three; a single row takes `view` alone. `view=latest` is accepted on every collection and
  row, the one-surface ones included.

## Writes

Three route shapes per resource: `POST /{resource}` creates, `PUT /{resource}/{id}` amends or
restates the whole record, `POST /{resource}/{id}/{verb}` acts (`approve`, `withdraw`,
`retire`, `halt`, …). A body is the record's fields minus what the gateway supplies: no
`userId` (stamped), and the path's id and the version (`If-Match`) may be restated in the body
but never contradicted (400). A create's id must be absent, `null` or `0`. An action's body is
optional: `{}` or an empty body is legal. Every write answers from the platform's stream,
never from the gateway alone:

* **`Idempotency-Key`** is required on every write but one — `POST /v1/ws-tickets`, which
  mints a new single-use ticket on every call and takes no key (400 `MISSING_IDEMPOTENCY_KEY` /
  `INVALID_IDEMPOTENCY_KEY`): a URL-safe string of 1 to 64 characters from `[A-Za-z0-9._~-]`
  (a UUID fits), scoped to you — the same key from two users is two operations. A key names
  one operation on one record, ever: a retry replays the stored outcome with
  `Idempotent-Replay: true` and publishes nothing, a retry of a still-live key attaches to
  it, and reuse of the key on another record or resource refuses 409
  `IDEMPOTENCY_KEY_REUSED`.
* **`If-Match`** on entity routes (`PUT` and the `/{id}/{verb}` actions) carries the `ETag`
  the read minted, quoted or bare; absent or `*` is unconditional; a weak validator (`W/…`)
  refuses 400 `INVALID_IF_MATCH`. A stale value answers 412 `VERSION_CONFLICT` with the
  current version in the envelope (`currentVersion`) and a fresh `ETag`: re-read, reapply,
  retry. **Send it.** Every write is a full-record restatement — the command carries the
  whole record and the answer replaces the row wholesale — so a write with no precondition
  overwrites every field from your copy, including the ones you never meant to touch, and two
  editors working from stale reads silently revert each other, both answered 200. The same
  shape one step less visible: an amend against a row whose proposal is still pending
  replaces that proposal.
* **200** is the record as the platform now holds it — the same shape the read serves, with
  `globalSequence`, the position header and the `ETag`; this member's reads already reflect
  it.
* **202** (`{"status": "PENDING", "key": "<your key>"}`, `Location: /operations/{key}`)
  means the command was accepted, or is being carried, and its answer had not landed inside
  the gateway's deadline — never a fabricated failure. Poll `GET /operations/{key}`: the
  record shows `phase` (`PENDING` → `COMMITTED` → `CONCLUDED`) and, once concluded, the
  `outcome` (the HTTP status, the code, the affected `entityId` or `orderId`, and the message
  a synchronous caller would have read). The journal is local to the member that carried the
  request, so behind several members poll with session affinity; a restarted member has no
  journal (404 `UNKNOWN_OPERATION`), and retrying the key then re-submits — safe, because
  every create is unique by its natural key and every action is state-gated.
* **4xx / 5xx** is the record owner's refusal, mapped to a status by its class, or the
  door's — [Errors](#errors).

**The credential intake.** `POST /credentials` and `POST /credentials/{id}/rotate` take the
venue key's material once (`material`, each leg a non-blank string: for OKX `key`,
`secret`, `passphrase`; for Binance spot `key` and `privateKey`, an Ed25519 key — one schema
per venue, `CredentialMaterial`'s `oneOf`, the body's `exchangeId` naming which) over TLS. The gateway stores it write-only, mints the row's `secretRef`,
fingerprints it (`fp-` and 16 hex characters of SHA-256) and fills `venueKeyId` from the key
leg; only references and metadata go on to the platform, the row never serves the material,
and a body stating any of `secretRef`, `appGroup`, `venueKeyId` or `keyFingerprint` is
refused 400. The intake's own refusals: 403 `CAPABILITY_DENIED` (no `credentials:propose`,
checked before anything is stored), 413 `PAYLOAD_TOO_LARGE` (16 KiB), 429 `RATE_LIMITED`
(20 stagings a minute, with `Retry-After`), 503 `INTAKE_UNAVAILABLE`, 422
`VENUE_UNSUPPORTED`, 409 `INTAKE_KEY_REUSED` (the key already staged other material). A
retry of a key the member answering it already holds stores nothing: it answers as any retry
does — the first attempt's answer replayed with `Idempotent-Replay: true`, or attached while
that attempt is still being carried — so the store-time refusals (409 `INTAKE_KEY_REUSED`,
429 `RATE_LIMITED`, and 503 `INTAKE_UNAVAILABLE` when the store refuses the write) meet only a
key it holds no answer for. `PUT /credentials/{id}` carries no material: new material is a
rotation.

## Maker-checker

Most records live under dual control. A create or amend on `credentials`, `accounts`,
`asset-policies`, `chain-policies`, `trading-policies` and `orgs` lands as a **proposal**:
the row's `pendingTransition` says what is proposed (`ACTIVATE` for a new record, `AMEND`,
`REACTIVATE`, `UNFREEZE`, `RETIRE`, `REVOKE`, `ROTATE`, `RESUME_TRADING`; `NONE` when nothing
is pending) and `pendingByUserId` who proposed it; `view=latest` shows the proposed content,
`view=approved` the enforced one. A second user holding the resource's `approve` capability
concludes it with `POST /{resource}/{id}/approve` — the proposer cannot (403
`SELF_APPROVAL`) — and either the proposer or a holder of that `approve` capability withdraws
it with `/withdraw` (the checker's way to reject; anyone else is 403 `NOT_PROPOSER`); a
withdrawn activation parks a credential or policy in `DRAFT`. Approving with nothing pending is 409 `NOTHING_PENDING`; an
action on a row whose proposal is still pending may be refused 409 `PROPOSAL_PENDING`, while
an amend against it revises the proposal. Some actions conclude at once, no approval needed:
suspending an organisation or a credential, freezing an account, halting trading, disabling
a user.

An organisation's `requiredApprovals` (on `GET /me` and the org row) is how many approvals a
proposal needs: `1` is the dual-control pair; `0` means the maker's own write is the
concluded record (throwaway and local stacks only). Users and portfolios are not under
maker-checker. Addresses follow the same discipline in their own words: declared by one user
(`POST /account-addresses`, or observed at the venue by a connector) and verified by a second
holding `account-addresses:approve` (`POST /account-addresses/{id}/verify`; the declarer
verifying their own is 403 `SELF_APPROVAL`).

## Money

Every money value is a **decimal string**, in and out: `"2.5"`, `"0.00010000"`. On the wire
every money value is a `Decimal` — a scaled integer that carries its own scale beside it — so
the gateway reads it without reference data at all and **serves it at exactly the scale the
value states**. That is where the digit count you see comes from: it is the precision the
platform recorded for that value, not a width derived from anything else.

**Do not read meaning into the digit count, and do not compare money as strings.** The same
amount recorded at different precisions serves different strings — `"2.5"` and `"2.500000"`
are the same number — because each states the precision its own fact carried. Parse every
money field as a decimal and compare numerically. Two consequences worth stating plainly: the
string depends on the value alone, never on whether this member happens to hold the asset or
instrument a row references, so it does not change as reference data catches up; and the same
value read over the websocket edge serves the identical string.

On the way in, the gateway encodes each decimal at **its own
scale** — the fractional digits you send, trailing zeros dropped, so `"1.50"` and `"1.5"` are
one value — and states that scale beside the value on the wire. It consults no reference data
to do so (since `0.3.4`), so it refuses nothing for precision and nothing for an asset or
instrument it does not hold: the platform judges the value. The orders owner aligns an order's
`price` and `qty` to the instrument's scales before it checks anything, so a price off the
tick is its 422 `TICK_SIZE_VIOLATION`; a policy amount is held at the scale you stated and
compared exactly. One asset or chain policy's amounts share a scale — the finest any of them
states — so a finer sibling adds trailing zeros to the others on a read, and amounts that
cannot fit a 64-bit integer together at that scale (about nineteen digits between the largest
integer part and the finest places) are the register's 422 `INVALID_FIELD`.

**The grammar is the schema's, and the gateway holds you to it** (since `0.4.1`): every money
property publishes `pattern` `^-?[0-9]+(\.[0-9]+)?$` and `maxLength` 64 — an optional minus,
ASCII digits, and at most one point with digits on both sides of it, in at most 64
characters — and a money string is accepted exactly when it matches both, judged as sent.
So no exponent (`"1e3"`, `"1E-7"`), no leading plus, no bare point (`".5"`, `"5."`), no
surrounding whitespace, no digits outside ASCII; leading and trailing zeros are fine. Mind
your number formatter: JavaScript's `String(0.0000001)` is `"1e-7"`, and Java's
`BigDecimal.toString()` and Python's `str(Decimal)` give `"1E-7"` — format money from an
exact decimal type in plain notation (`toPlainString()`, `format(d, 'f')`), or carry the
string you were served, which always matches. In JavaScript keep money in a string or a
decimal library, never a `Number`: `toFixed` rounds a binary float, and
`(0.0000001).toFixed()` is `"0"` — which means "unset". A member below `0.4.1` read those
spellings as the numbers they spell.

The door's own money refusals are all 400 `MALFORMED_REQUEST`, and none spends the key: a
value that is not a JSON string, one off the `pattern` or longer than the `maxLength`, one
with more than 18 fractional digits or past a 64-bit integer at its own scale, and an amount
beside an `assetId` or `instrumentId` that is absent or 0. `"0"`, `null` or an absent field
all mean "unset" (the platform's 0 sentinel), so a row read from `GET` round-trips into a
`PUT` body. Never send a scale of your own (`priceScale`, `maxTicketAmountScale`, …): the
document advertises none, and the gateway refuses 400.

The families below still matter on the way **in** — they say which record's scale the gateway
converts your value at — but on the way out every one of them is served at the scale its own
value states.

| Family   | Scaled on input by            | Where it appears                                                                                                                                                                                                                                          |
| -------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| amount   | the asset's `unitScale`       | asset-policy amounts (`maxTicketAmount`, `approvalThresholdAmount`, `reconciliationToleranceAmount`); chain-policy bounds (`minTransferAmount`, `maxTransferAmount`); a balance's `total`, `available` and `held`; an execution's `fee` (its fee asset's) |
| quantity | the instrument's `qtyScale`   | order and execution quantities (`qty`, `cumQty`, `leavesQty`, `lastFillQty`, `venueCumQty`, `fillQty`); a trading policy's `maxOrderQty`                                                                                                                  |
| price    | the instrument's `priceScale` | order and execution prices (`price`, `avgPx`, `lastFillPx`, `fillPx`); a trading policy's `maxOrderNotional` (a notional is a price-family amount)                                                                                                        |

Two edge cases the document's schema marks for you:

* **`unscaled`** — never present since `0.3.3` (deprecated in the document, kept so an older
  generated client compiles; it goes at the next major). Until then a row whose scale source
  this member did not yet hold was served raw — the scaled integer as a string — under
  `"unscaled": true`, and a collection carried a coarse top-level marker while any item's row
  would; a balance stopped being served that way at `0.3.1`, an order and an execution at
  `0.3.2`, the three policy families at `0.3.3`. Every money value now states its own scale,
  so a row is an exact decimal whether or not this member holds the asset or instrument it
  references. Parse every money field as a decimal string and never assume its scale — a
  value that carried six places yesterday may carry two today if the venue restated it, and
  both are the same number.
* **`SCALE_UNKNOWN`** (422) — retired at `0.3.4`, listed until the next major. A member
  below `0.3.4` scaled money at the referenced record's scale and answered this code for an
  asset or instrument it did not hold (retry once reference data has landed); a member at
  `0.3.4` or above encodes money without the record and never answers it. A cap stated with
  no asset or instrument reference at all is 400 on both.

Every money property carries the decimal `pattern` (`^-?[0-9]+(\.[0-9]+)?$`) and
`maxLength` 64, on the rows you read as on the bodies you send — the widest decimal served is
39 characters; a required money field always serves a decimal, an optional one serves `null`
at its sentinel.

## Ids, sentinels, enums and sets

* **Ids** are opaque integers the platform mints — never contiguous, never meaningful,
  unique per resource. Register ids (`orgId`, `userId`, `credentialId`, …) are 32-bit and
  cross as JSON numbers; `orderId`, `executionId`, `instrumentId` and every `version` are
  64-bit and cross as **decimal strings** (they exceed 2^53), in paths too. Timestamps
  (`…AtNs`, `…TimestampNs`) are epoch nanoseconds, also as decimal strings.
* **Sentinels.** An optional field at its unset value serves `null` — never a zero — and is
  marked `nullable` in the schema; as input, absent and `null` both mean unset. Two named
  exceptions: a trading policy whose `instrumentId` is `null` is the organisation-wide
  default row (it carries the halt flag and the count ceilings); an order's `venueCumQty`
  serves `null` for "never reported".
* **Enums** cross by name (`"ACTIVE"`), never by number, and every value set is
  **append-only**. Reading, treat a name you do not know as the field's unset value
  (`UNKNOWN`, `NONE`), never as an error — a closed-set validator breaks on the first
  append. Writing, the parse is strict: an unknown name, or the unset value itself, refuses
  400 (omit the field instead). Booleans are booleans; absent reads `false`. Strings are
  never `null`: `""` is "none".
* **Sets** (`capabilities`, a credential's `observedPermissions`) serve as an array of names
  in declared order — empty means none, never `null`; an unknown name is skipped on reading
  and refused on writing. Sets are append-only too.
* **Appended fields** carry `x-since-schema-version` in the schema: a member on an older
  schema serves the row without them, so absence there means "not served here", not "unset".

## Errors

One envelope on every refusal the gateway answers — its own, the platform's, and a request
no route answers alike:

```json
{"error": {"code": "VERSION_CONFLICT", "message": "…", "requestId": "01K6S7Z0Q4…",
           "retryable": false},
 "currentVersion": "7"}
```

* **`error.code`** is the machine surface — a stable string to branch on. The code set is
  open and append-only: branch on the codes you know and treat an unknown one by its HTTP
  status.
* **`error.message`** is for humans. Every refusal carries one; its wording may change
  without notice, so never parse it.
* **`error.requestId`** is the request's id. The `X-Request-Id` response header carries the
  same value on every response, a success included: quote it when you ask about a request.
  Send your own `X-Request-Id` — 8 to 128 of `[A-Za-z0-9._-]` — and it is the id used;
  anything else is replaced with a fresh one. The document declares the header on every
  operation, as a request header you may send and on every response.
* **`error.retryable`** says whether sending the identical request again, its
  `Idempotency-Key` included, after `Retry-After` or a short pause, can succeed with nothing
  else changed ([Retrying](#retrying) has the rule).
* **`currentVersion`** sits beside `error`, on a 412 only.

The envelope is the gateway's. A request the web server turns away before the gateway reads
it — a request line it cannot parse, headers past its bound, a WebSocket upgrade — is
answered by the server itself, and so is a failure too deep for the gateway to answer.
Neither carries the envelope or `X-Request-Id`: treat such an answer by its status.

A member below `0.12.0` sends `code`, sometimes `message`, and neither `requestId` nor
`retryable`. Where `retryable` is missing, decide by the status and the code: treat a record
owner's refusal as final for its key, and bound any retry of `INTAKE_UNAVAILABLE` and
`NOT_SERVING`, whose two cases ([Retrying](#retrying)) a status and a code cannot tell apart.

**The door's codes** — the gateway refused; nothing reached the platform, the key was not
journaled, a retry takes fresh:

| Status | Codes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `MALFORMED_REQUEST`, `MISSING_IDEMPOTENCY_KEY`, `INVALID_IDEMPOTENCY_KEY`, `INVALID_IF_MATCH`, `INVALID_CURSOR` (a list's cursor that does not decode, or that came from another query)                                                                                                                                                                                                                                                                                                                                                                                                     |
| 401    | `UNAUTHENTICATED`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 403    | `FORBIDDEN_PRINCIPAL`, `AWAITING_ADMISSION`, `ORG_NOT_ACTIVE`; `CAPABILITY_DENIED` at the credential intake                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 404    | `NOT_FOUND` (a read's or an entity route's id: absent, or another organisation's), `UNKNOWN_OPERATION`, `ROUTE_NOT_FOUND` (no route answers the path: a misspelt path, or an operation the answering member's version does not serve yet; refused before authentication)                                                                                                                                                                                                                                                                                                                    |
| 405    | `METHOD_NOT_ALLOWED` (a path that exists, asked with a method it does not take; the `Allow` header names the ones it does)                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 409    | `IDEMPOTENCY_KEY_REUSED`, `INTAKE_KEY_REUSED`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 413    | `TOO_LARGE`, `PAYLOAD_TOO_LARGE` (the credential intake's cap, and any request whose `Content-Length` declares a body past 1 MB), `DOCUMENT_TOO_LARGE` (an algo config document past the platform's 6,144 bytes of UTF-8 — nothing was sent)                                                                                                                                                                                                                                                                                                                                                |
| 422    | `INSTRUMENT_NOT_LIVE` and `ACCOUNT_NOT_ACTIVE` on `POST /orders` (a symbol this member holds no instrument under, or a display name no live account of your organisation holds — the orders owner answers the same codes to an id it will not trade); `REFERENCE_UNKNOWN_ACCOUNT` and `REFERENCE_UNKNOWN_INSTRUMENT` on the algos create and config write (a display name or a symbol in the document nothing here holds — the algos owner answers the same codes to an id it does not know); `VENUE_UNSUPPORTED`; `SCALE_UNKNOWN` — retired at `0.3.4`, answered only by a member below it |
| 429    | `INBOX_FULL`, `IN_FLIGHT_FULL`, `RATE_LIMITED` (the credential intake and the WebSocket ticket route, each per user) — with `Retry-After`                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 500    | `INTERRUPTED`, `INTERNAL_ERROR`, `PROJECTION_MISS`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 503    | `NOT_PRIMED`, `NOT_SERVING`, `OWNER_ABSENT`, `INTAKE_UNAVAILABLE`, `BALANCES_UNAVAILABLE` (the balance surface, on a member deployed without it), `ALGOS_UNAVAILABLE` (the algos surface, likewise), `TICKETS_UNAVAILABLE` (no ticket signing key on this member), and the platform's own names for a command lost in transit (`LOST`, `REJECTED_NOT_REGISTERED`, `REJECTED_NOT_ACTIVE`)                                                                                                                                                                                                    |

The 422s for a name stand only for a key the member answering holds no answer for: a key it
holds answers as that key's retry instead, whatever the name resolves to now
([Orders](#orders), [Algos](#algos)).

**The record owner's codes** — the platform judged the command, and the refusal is a fact on
its stream: it carries the position header, and a retry of the key replays it with
`Idempotent-Replay: true`. The full set is the `OrgRegisterRejected.reason` enum in the
document:

| Status | Codes                                                                                                                                                                                                                                                                                                                              |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `CAPABILITY_DENIED`, `SELF_APPROVAL`, `NOT_PROPOSER`, `USER_DISABLED`, `USER_NOT_ADMITTED`, `ORG_NOT_ACTIVE`                                                                                                                                                                                                                       |
| 409    | `ILLEGAL_TRANSITION`, `PROPOSAL_PENDING`, `NOTHING_PENDING`, `NAME_TAKEN`, `DUPLICATE_POLICY`, `DUPLICATE_ADDRESS`, `CREDENTIAL_IN_USE`, `PORTFOLIO_IN_USE`, `LAST_ADMIN`, `CREDENTIAL_NOT_ACTIVE`, `CREDENTIAL_UNASSIGNED`, `SCOPE_INSUFFICIENT`, `SCOPE_MISMATCH`, `ALREADY_DISCOVERED`, `ALREADY_BOOTSTRAPPED`, `IDP_REF_TAKEN` |
| 412    | `VERSION_CONFLICT`                                                                                                                                                                                                                                                                                                                 |
| 422    | `UNKNOWN_ENTITY`, `INVALID_FIELD`, `MISSING_VENUE`, `WRONG_CREDENTIAL`, `ORG_MISMATCH` — and any reason this gateway does not yet know                                                                                                                                                                                             |

The orders owner's codes are under [Orders](#orders), the algos owner's under [Algos](#algos).

### Retrying

`error.retryable` is the answering member's verdict on the refusal it sends: `true` when
sending the identical request again — the same `Idempotency-Key` included — after
`Retry-After` or a short pause, can succeed with nothing else changed. These are the
conditions that clear:

| Status | Codes                                                                                  |
| ------ | -------------------------------------------------------------------------------------- |
| 429    | `INBOX_FULL`, `IN_FLIGHT_FULL`, `RATE_LIMITED`                                         |
| 500    | `INTERRUPTED`, which a member answers while it shuts down                              |
| 503    | `NOT_PRIMED`, `OWNER_ABSENT`, `LOST`, `REJECTED_NOT_REGISTERED`, `REJECTED_NOT_ACTIVE` |

It is `false` for every other refusal, where something has to change first — the request,
its key, its precondition, the caller's standing, or the platform's own records: a 409 or a
412 asks for a read before the next write, not for the same write again, and a 422 for a
name the platform does not hold yet waits for the record, not for a resend.

Six codes under a 429 or a 503 do not follow their status. Five state how the service is
deployed, and are `false` wherever only a change to the deployment clears them; the sixth is
a record owner's refusal:

| Code                   | `retryable`                                                                                                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BALANCES_UNAVAILABLE` | `false` always: the environment runs without the balance surface                                                                                                                                      |
| `ALGOS_UNAVAILABLE`    | `false` always: the environment runs without the algos surface                                                                                                                                        |
| `TICKETS_UNAVAILABLE`  | `false` always: the member holds no WebSocket ticket signing key, which only its deployment can give it                                                                                               |
| `INTAKE_UNAVAILABLE`   | `false` on a member that stores no credentials; `true` when the store refused the write — bound that retry, because a store that keeps refusing needs its operator                                    |
| `NOT_SERVING`          | `false` on a read replica, which takes no write; `true` on a member that lost its seat, or that could not look the request's key up in time                                                           |
| `RATE_CEILING`         | `false` always: the orders owner judged the order, and the refusal is the recorded answer to its key. `Retry-After` says when the ceiling's window ends; send the order again then, under a fresh key |

A refusal a record owner judged is never retryable, whatever its status: it is a fact on the
stream and the recorded answer to its `Idempotency-Key`, so that key sent again replays it —
the flag included — with `Idempotent-Replay: true`. `RATE_CEILING` is the only one under a
status that otherwise clears. A refusal at the door journals nothing: where the flag is
`true` the identical request goes again under its key, and a corrected request may reuse the
key whatever the flag says ([Writes](#writes) has the rule).

## Orders

`POST /orders` submits, `POST /orders/{id}/cancel` cancels, `GET /orders` and
`GET /orders/{id}` serve orders, and `GET /executions` and `GET /executions/{id}` serve
fills — one immutable economic event each (a bust or a correction is its own row), joined to
their order by `orderId` (`?orderId=` narrows), fills not yet attributed to an order
included. An order's `cumQty` and `avgPx` derive from its executions. The order surface has
one view, and an order's `ETag` is its own `version`.

* **The key is the client order id.** On `POST /orders` the `Idempotency-Key` *is* the
  order's `clientOrderId`: 1 to 36 characters of the key grammar. A body `clientOrderId` may
  restate it, never contradict it. A retried key replays or attaches like any key, and one
  that reaches the platform afresh (after a member restart) restates the same order — the
  same key can never mint a second order; another user's key is 409
  `CLIENT_ORDER_ID_CONFLICT`.
* **Name the instrument** by `instrumentId` or by `instrument`, the platform symbol
  (`"OKX@BTC/USDT"`): at least one, and both stated must agree (400). The gateway resolves the
  symbol before anything is sent and served rows carry the id — symbols are reference data's
  to change, the id is the stable address. An instrument that is not held is 422
  `INSTRUMENT_NOT_LIVE` by either spelling: the orders owner answers it to an id it does not
  hold — a fact on the stream, with the position header, and the key (your `clientOrderId`) is
  spent, so retry under a fresh one — and the gateway answers it to a symbol it cannot resolve
  to an id: nothing sent, no position header, and the same key retries fresh. `price` (`LIMIT`
  only; absent, `null` or `"0"` for `MARKET`) and `qty` cross at their own scale, and the
  owner aligns them to the instrument's `priceScale` and `qtyScale` before its checks — a
  value off the tick or the step is its refusal, never the door's.
* **Name the account** by `accountId` or by `account`, its display name within your
  organisation (`"okx-main"`): the same rule, spelled the same way — at least one, and both
  stated must agree (400). The gateway resolves the name before anything is sent, so only the
  id crosses: a name is your organisation's to change, and a command carrying one would name
  a different account after a rename. A name no live account of your organisation holds is
  422 `ACCOUNT_NOT_ACTIVE` at the door — nothing sent, no position header, and the key
  retries fresh; the orders owner answers the same code, as a fact, to an id it will not
  trade. An account still being classified has no bound name yet and is addressable by id
  alone. **The WebSocket door takes the same alias**, resolved through the same rule, so one
  submit body serves both lanes.
* **A retried key replays, whatever its names resolve to now.** The gateway's 422 for a symbol
  or a display name stands only for a key the member answering it holds no answer for. A retry
  of a key it already holds answers as any retry does — the order placed, or the platform's
  refusal of it, replayed with `Idempotent-Replay: true`; a submit still being carried,
  attached — even after the account was renamed or deactivated, or the member stopped holding
  the symbol. After the gateway's 422, fix the name and retry under the **same** key: it is
  your `clientOrderId`, so a retry under it can never place a second order, and a fresh key
  can.
* **The accept** answers the `QUEUED` row (200) or the platform's refusal. The gateway
  pre-checks nothing else: the orders owner's validation chain answers, with the observed
  value against the limit and the governing trading policy in `error.message`.
* **Cancel** is never gated — not on the reference price, not on the venue connection, not
  on a trading halt — and takes no `If-Match` (a stated one is 400): a cancel must never lose
  a version race. The accept answers the row with `cancelRequestedAtNs` set and the status
  **unchanged**; the venue confirms asynchronously and `CANCELED` arrives later as an
  ordinary order update. "Is a cancel in flight" is a live status with a non-null
  `cancelRequestedAtNs`; there is no pending-cancel status. Refused for 404
  `UNKNOWN_ORDER`, 422 `ORG_MISMATCH`, 409 `ORDER_TERMINAL` or 409 `ORDER_STATUS_UNKNOWN`
  (an order whose status this member cannot interpret).
* **Who placed it**: `userId` is the submitting user and is never restamped — a cancel's
  actor rides the cancel, not the row. An order the platform's autonomous algos placed
  carries `algoInstanceId` (the instance) and a `userId` of `0`; a person's order carries the
  user and `algoInstanceId` `null`. Exactly one is ever set; read `algoInstanceId != null`,
  never `userId == 0`, as "an algo's order". A user of the organisation may cancel such an
  order exactly as any other.
* **Money on orders**: every value states its own scale on the wire (a `Decimal`, since
  orders schema `111.v1`) and is served at exactly the scale the value itself states, with no reference data
  consulted: prices (`price`, `avgPx`, `lastFillPx`, `fillPx`), quantities (`qty`, `cumQty`,
  `leavesQty`, `lastFillQty`, `venueCumQty`, `fillQty`) and a fill's `fee` each render every
  fractional digit the platform recorded — so the digit count you see is the precision that
  value states, never a width derived from the instrument or asset. An order
  or execution row is never `unscaled` (the marker is deprecated on this surface, kept so an
  older generated client compiles). A refusal's `observed … against limit …` legs in
  `error.message` are the decimals the owner stamped, each at the scale it states.

The orders owner's refusals, by status (the full set is the `OrderRejected.reason` enum):

| Status | Codes                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `CAPABILITY_DENIED` (no `orders:trade`), `ORG_NOT_ACTIVE`                                                                                                                                                                                                                                                                                                                                                                                                  |
| 404    | `UNKNOWN_ORDER`                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 409    | `CLIENT_ORDER_ID_CONFLICT`, `ACCOUNT_NOT_ACTIVE`, `CONNECTION_DOWN`, `TRADING_HALTED`, `NO_REFERENCE_PRICE`, `OPEN_ORDER_CEILING`, `ORDER_TERMINAL`, `ORDER_STATUS_UNKNOWN`                                                                                                                                                                                                                                                                                |
| 422    | `ORG_MISMATCH`, `INSTRUMENT_NOT_LIVE`, `CREDENTIAL_UNAVAILABLE`, `TRADING_POLICY_MISSING`, `INVALID_FIELD`, `UNSUPPORTED_AT_VENUE`, `TICK_SIZE_VIOLATION`, `QTY_STEP_VIOLATION`, `QTY_BOUND_BREACH`, `MIN_NOTIONAL_BREACH`, `TICKET_QTY_CAP_EXCEEDED`, `TICKET_NOTIONAL_CAP_EXCEEDED`, `COLLAR_BREACH`, `PRICE_BOUND_BREACH`; `FILL_PRECISION_OVERFLOW` — in the enum, never answered to a request (below) — and any reason this gateway does not yet know |
| 429    | `RATE_CEILING` (the organisation's per-minute ceiling), with `Retry-After: 60` and `retryable: false`: the key is spent, so the order goes again under a fresh one                                                                                                                                                                                                                                                                                         |

`FILL_PRECISION_OVERFLOW` is in the `OrderRejected.reason` enum and in no response of yours:
the orders owner answers it to a *connector's* progress report, never to `POST /orders` or a
cancel. It means a venue stated a fill at a precision the order cannot hold in a 64-bit
integer — the fill was refused whole, never rounded to fit, and the platform's operators are
paged; none is expected. You meet it only if you read refusals off the platform's stream.

## Balances

`GET /account-balances` serves your organisation's **latest observed balance per
(account, asset)** — what each of your accounts holds at its venue, as the venue states it.
The platform does not keep these books: its account connector polls the venue on your
credential and republishes each reading as a measurement (the custody domain's
`AccountBalance`, schema 115), keyed latest-wins — a reading is superseded by the next and
never corrected, and a venue that stops reporting a pool simply leaves its last reading in
place. Every row carries:

* **`accountId`, `assetId`, `credentialId`** — the references. A balance row carries no venue,
  symbol or classification of its own: `GET /accounts/{id}` names the venue, the credential
  and the account's classification, and reference data names the asset. To view balances
  by classification (exchange treasury, hot vs cold), join the two collections on
  `accountId` — both are your organisation's whole set.
* **`total`, `available`, `held`** — the venue's partition of the holding, decimal strings at
  the scale the reading itself states, served at exactly that scale with no reference data
  consulted — so the digit count you see is the precision the platform recorded, never a
  width derived from the asset.
  `available` is what the venue would let *move* now (a withdrawal or
  transfer semantic — never "usable as margin": collateral eligibility and haircuts are the
  venue's own gates), `held` what the venue itself has locked (order margin, pending
  withdrawals, venue-side freezes), and `total` the load-bearing arm. Additivity
  (`total = available + held`) holds only where the venue's model is additive. **Zero is a
  measurement**: the three are required and never `null`.
* **`observedAtNs`** — the platform time of the reading (epoch nanoseconds, a decimal
  string, never `null`) — the clock to judge freshness from. The connector republishes a
  pool on every change and restates every pool on a heartbeat grid regardless (every 30 s by
  default), so `observedAtNs` advances while the venue keeps answering and stalls when it
  does not: *idle* and *stalled* are told apart by it, and only by it — `venueTsNs`
  (the venue's own clock, where it states one) and `venueRevision` (its sequence, where one
  exists) are `null` when unreported and say nothing about freshness on this platform.
* **`unscaled`** — never present on a balance since `0.3.1` (deprecated in the document, kept
  so an older generated client compiles): a balance states its own scale, so it is never
  served raw.

The collection is your organisation's by construction: a reading names its account, the
account row names the organisation, and a reading whose account the member does not hold
(or another organisation's) is indistinguishable from absent — a `DISCOVERED` account's
pools are served while the account is being classified, so a new venue account can be
classified with its holdings in view. `?accountId=` narrows to one account's pools (a
positive int32; an account outside your organisation, or one with no reading yet, yields an
empty collection). Rows are ordered by `accountId` then `assetId`. The surface is one
(`?view=approved` is 400), there is no entity route and no `ETag` — the key is the pair and a
reading carries no version — and the position header moves on every reading, restatements
included: treat `X-Immix-Global-Sequence` as the consistency token it is, not as a change
flag.

**Where the surface is off.** A member deployed without the balance surface — an environment
into which no account connector publishes readings yet — answers this route
`503 BALANCES_UNAVAILABLE` after admission, whatever the query, with no position header;
every other route is unaffected. It is the environment's posture, not a transient: it clears
when the environment gains its connector, never by retrying, so a client that renders
balances shows "not available here" on it rather than an empty table.

## Algos

An **algo instance** is a strategy the platform runs for your organisation: created from a
loaded **algo version** (the catalog, `GET /algos/versions` — the platform's, not yours:
every version per engine pool with its manifest and the JSON Schema its documents must
conform to) with a **config document**, started and stopped by your users, placing orders of
its own. Its orders are machine flow — `algoInstanceId` set, `userId` `0` — and
`GET /algos/instances/{id}/orders` is exactly its rows, in the `OrderUpdated` shape of
[Orders](#orders). Everything under `/algos` needs `algos:manage` for the writes; the reads
are any admitted member's, scoped to their organisation.

**Instances and documents.** `GET /algos/instances` lists every instance ever created,
`DELETED` ones included, in id order; `GET /algos/instances/{id}` serves one with its
attribution — `createdByUserId`, and `lastUpdatedByUserId` with `lastCause` and
`lastCommandKind`: `null` with `CONFIG_APPLIED`, `ENGINE_FAULT` or `DIVERGED` when the engine
or the platform's own rule moved it, a user with `COMMAND` or `HALT` — and an
**`ETag: "<configSeq>"`**, the sequence number of its latest document. The documents are the
instance's history: `GET …/configs` (oldest first) and `…/configs/{seq}`, each the JSONC text
as submitted — a name in it reads as the id it named, the name beside it as a comment (below)
— its `outcome` (`STAGED` until the engine judges it, then `APPLIED` or `REFUSED` with
the engine's `detail`), `outcomeAtNs` (`null` while staged), who submitted it and when, and
the `accountIds` and `instrumentIds` the platform projected from it. The algos reads have
one surface (`?view=` other than `latest` is 400) and no status filter; another
organisation's instance is 404 everywhere, indistinguishable from an absent id.

**The create.** `POST /algos/instances` with `algoVersionId`, `pool`, `displayName`,
`remark` and `document` creates the instance **with** its first document in one request.
The document is one JSON **string** holding JSONC — comments allowed, one object, typed by
the version's schema — and the door holds it to the platform's bound of **6,144 bytes of
UTF-8**: past it, 413 `DOCUMENT_TOO_LARGE` with nothing sent; everything else about the
text is the platform's to judge (422, naming the field — the table below). On every algos
write the **`Idempotency-Key` is the command's `commandKey`** (1 to 64 of `[A-Za-z0-9._~-]`;
a body `commandKey` may restate it, never contradict it), a body `userId` is refused 400, and
on the instance routes a body `instanceId` must equal the path's. A key names one operation
here as everywhere ([Writes](#writes)): reused on another algos route — a halt after a
create, a stop after a start — or on another instance, it refuses 409
`IDEMPOTENCY_KEY_REUSED` while the journal remembers it. The create answers **201**
with the row once both the instance and its document (`configSeq` 1) are recorded — or 202
with `Location: /operations/{key}` past the deadline, like every write ([Writes](#writes));
the operation record's `outcome` then carries the `instanceId` and `configSeq` the pair
minted, and a retry of the key replays the 201 with `Idempotent-Replay: true`. A new
instance is `CREATED` until the engine applies its document, then `STOPPED`.

**Naming an account or an instrument.** Wherever the version's schema types a reference
(`"format": "account"` or `"format": "instrument"`), the document may state an id — a JSON
number — or a **name**, a JSON string: an account's display name in your organisation, an
instrument's platform symbol (`OKX@BTC/USDT`) — the names `POST /orders` accepts, resolved the
same way (surrounding spaces trimmed, then exact; `"700"` is a name, never the id). The
gateway resolves each name when the create or the config write arrives and sends the document
with the name replaced by its id, the name kept beside it as a comment where it fits:

```jsonc
{
  // as you write it — keep this one in git
  "account": "okx-main",
  "instrument": "OKX@BTC/USDT",
  "spreadBps": 25,
  "quoteQty": "0.01"
}
```

is staged, and read back, as

```jsonc
{
  // as you write it — keep this one in git
  "account": 118310760 /* okx-main */,
  "instrument": 3210201010000 /* OKX@BTC/USDT */,
  "spreadBps": 25,
  "quoteQty": "0.01"
}
```

Every other byte — your comments, the layout, the other values — is kept as you wrote it. A
name is resolved once: renaming the account later never moves the running instance, and
submitting the document again resolves it again — the staged document's
`accountIds`/`instrumentIds` show what it named each time. Ids are your environment's, so
keep documents with names in your own source and submit them to each environment, rather
than copying a staged document across. A name nothing here holds is 422
`REFERENCE_UNKNOWN_ACCOUNT` or `REFERENCE_UNKNOWN_INSTRUMENT` from the gateway itself —
nothing sent, nothing journaled, the key free for a retry; the message names the value's
path and the name. The 6,144-byte bound holds as written and as sent: a document that fits as
written but not once its names are ids is 413 `DOCUMENT_TOO_LARGE` (the comments are dropped
before that happens). A retry of a key the member answering it already holds replays its answer
whatever its names resolve to now: a refusal answers only a key's first attempt. A version this
member has not caught up with yet — one loaded a moment ago — sends the document as written,
and the platform refuses its names 422 `FIELD_TYPE_MISMATCH` — a fact, so the key is spent:
once `GET /algos/versions` lists the version, retry under a new key.

**Re-configuring.** `PUT /algos/instances/{id}/config` with `remark` and `document` stages a
new document — legal in every state but `DELETED`, while the instance runs too — under
**`If-Match: "<configSeq>"`**, the instance's `ETag`: absent or `*` is unconditional, a weak
validator is 400 `INVALID_IF_MATCH`, and a stale one is **412 `CONFIG_SEQ_CONFLICT`** with
`currentVersion` and a fresh `ETag` — re-read, reapply, retry. It answers 200 with the
**staged** document (no `ETag`: a document carries no version); the engine's verdict is a
later state on `…/configs/{seq}`, and until it lands a second write is 409 `CONFIG_PENDING`.
A refused document leaves the applied one in force.

**Start, stop, delete, halt.** `POST /algos/instances/{id}/start`, `/stop` and `/delete`
take `{ remark }` and **no `If-Match`** — one refuses 400 `INVALID_IF_MATCH`: stopping is the
safe direction, and no version race may lose it — and answer 200 with the row. `start` needs a
`STOPPED` instance whose document is applied and whose version is still loaded on its pool;
`stop` takes a `STARTED` or `ERROR` one (the engine cancels its live orders as the change
applies); `delete` is a verb route rather than the `DELETE` method, so the remark and the key
ride as on every write, and is terminal — the row and its documents stay readable, the id is
never reused, its orders keep naming it; a started instance stops first. `POST /algos/halt`
stops every started instance of your organisation, in id order, and answers 200 with
`count` — `0` when nothing was started, so the call always has an answer.

**Where the surface is off.** A member deployed without the algos surface — an environment
whose algos owner is not released yet — answers every `/algos` route `503 ALGOS_UNAVAILABLE`
after admission, with no position header; every other route is unaffected. It is the
environment's posture, not a transient, exactly as `BALANCES_UNAVAILABLE` is.

The algos owner's refusals, by status (the full set is the `AlgoRejected.reason` enum; the
message carries the detail — the field's path, the capability, the current sequence number):

| Status | Codes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `CAPABILITY_DENIED` (no `algos:manage`), `ORG_MISMATCH`, `ORG_NOT_ACTIVE`                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 404    | `UNKNOWN_INSTANCE`, `UNKNOWN_ALGO_VERSION`                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 409    | `ILLEGAL_TRANSITION`, `CONFIG_PENDING`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 412    | `CONFIG_SEQ_CONFLICT` — with `currentVersion` and a fresh `ETag`                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 413    | `DOCUMENT_TOO_LARGE` — the platform's word on the same bound the door holds                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 422    | `VERSION_NOT_ON_POOL`, `NO_APPLIED_CONFIG`, `DOCUMENT_UNPARSEABLE`, `DUPLICATE_KEY`, `UNKNOWN_FIELD`, `FIELD_MISSING`, `FIELD_TYPE_MISMATCH`, `FIELD_OUT_OF_RANGE`, `TOO_MANY_REFERENCES`, `REFERENCE_UNKNOWN_ACCOUNT`, `REFERENCE_ACCOUNT_NOT_ACTIVE`, `REFERENCE_UNKNOWN_INSTRUMENT`, `REFERENCE_INSTRUMENT_NOT_LIVE`, `BINDING_UNKNOWN_EXCHANGE`, `BINDING_ACCOUNT_VENUE_MISMATCH`, `DIGEST_MISMATCH`, `INVALID_FIELD`, `SELF_APPROVAL`, `TRANSITION_SEQ_CONFLICT` — and any reason this gateway does not yet know |

## The organisation lifecycle

Organisations are the platform's to manage. `POST /orgs` and the five arms under
`/orgs/{id}` (`approve`, `suspend`, `reactivate`, `retire`, `withdraw`) are served to members
of the platform's own organisation holding `orgs:propose` or `orgs:approve`, who also read
every organisation's row and the members of one still `PENDING_APPROVAL`. To everyone else
`GET /orgs` lists their own organisation alone, and any other organisation's id is 404 on the
reads and the arms alike. A customer never needs `/orgs`: the organisation's name, its
approval count and your capabilities are on `GET /me`.

How an organisation comes to exist: on the first login of one of its members the door
registers it, `PENDING_APPROVAL`, with that member `DISCOVERED` under it (or a platform user
proposes it with `POST /orgs`, naming the first administrator); a platform approver activates
it (`POST /orgs/{id}/approve`, naming the first administrator among its discovered members
and fixing `requiredApprovals`), and that administrator admits everyone else. Until then its
members are refused 403 `ORG_NOT_ACTIVE`, and so are a suspended organisation's, on every
route.

## WebSocket tickets

The trading WebSocket authenticates you before it opens. A browser cannot send an
`Authorization` header on the WebSocket handshake, so it first exchanges its bearer token for a
**ticket** and puts that in the WebSocket URL; any other client may send its bearer token on
the handshake instead, and needs no ticket.

```http
POST /v1/ws-tickets
Authorization: Bearer <your token>
Content-Type: application/json

{"stream": "trading"}
```

```json
{"ticket": "AQcBARjXW4Qj…", "expiresAtNs": "1790000010000000000", "stream": "trading"}
```

Then open the WebSocket with `?ticket=<ticket>` on its URL, unchanged — the ticket is URL-safe
as served. The rules:

* **One ticket, one session, soon.** A ticket opens one session, once, and only before
  `expiresAtNs` — 10 seconds after it is created by default, or your token's expiry if that
  comes first. Create a new ticket for every connection attempt, a reconnect included; a
  ticket you used, or one refused at the handshake, opens nothing.
* **It is a credential.** It carries your identity, so treat it like your token: the response
  is `Cache-Control: no-store`, and nothing should log it or the URL that carries it.
* **The session lasts as long as your token, not the ticket.** The session the ticket opens
  ends at your token's expiry; refresh it on the WebSocket the way the WebSocket's own
  contract describes.
* **The route is admitted like any other.** A token that does not verify is 401, a user the
  platform refuses 403 — a refused user never gets a ticket. A token with no expiry cannot
  create one (401): a ticket never opens a session that outlives its token.
* **Per user, at most 60 a minute** — past it, 429 `RATE_LIMITED` with `Retry-After`. A
  malformed request does not count.
* **`stream`** is `trading`, the one stream a ticket opens today; anything else is 400
  `MALFORMED_REQUEST`.
* **503 `TICKETS_UNAVAILABLE`**: the member you reached holds no ticket signing key, so a retry
  there changes nothing. A browser cannot open the trading WebSocket through it until it
  serves tickets; any other client sends its bearer token on the handshake instead.
* **No `Idempotency-Key`.** Unlike every other write, this route takes none: a replayed answer
  would hand a retry the same single-use ticket, so every call mints a new one.
* **From a browser on another origin**, the call needs CORS, which the gateway does not serve
  yet: until it does, call it from the gateway's own origin or through your backend.

The route is the first under `/v1`; every other route keeps its path.

## Versioning

* **`info.version`** is the version of this document — semver, hand-bumped in the same change
  as the changelog section that describes it: a **major** for a break (a route, field, code or
  generated identifier removed or renamed), a **minor** for a route, field or enum member
  added (regenerate: a closed enum type gains a member), a **patch** for prose and for
  behaviour inside the published shapes and code set (what a value is served as, which
  requests the door refuses — and a constraint keyword that writes such a rule into the
  schema once the door already enforces it, as `0.4.1`'s `maxLength` did) — the changelog
  says what each side of the deploy window sees, whatever the part. `1.0.0` is the first customer pin; until then a minor may rename
  generated identifiers (tags, operation ids), and the changelog says which. The newest
  section of the [changelog](/trading/changelog/rest-api) is always headed by the served version.
* **`info.x-immix-register-schema`**, **`info.x-immix-orders-schema`**,
  **`info.x-immix-custody-schema`** and **`info.x-immix-algos-schema`** name the platform
  wire schemas the message shapes derive from (`114.v8`, `111.v2`, `115.v1`, `121.v0`):
  every schema under `components.schemas` that shares a name with a message
  (`CredentialUpdated`, `SubmitOrder`, `AccountBalance`, `CreateAlgoInstance`, …) is
  generated from that schema, and those schemas are append-only — an appended property
  carries `x-since-schema-version`.
* **Generated clients.** The document is written to be generated from: one row schema per
  resource (`CredentialRow`, …) that its read and its writes share, an `operationId` on every
  operation, a typed `capabilities` set, and an open `Error.code`. Regenerate before you meet
  a new minor.