> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.immix.xyz/guides/rest-api-guide/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, and the orders it places. 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](/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) · [The organisation lifecycle](#the-organisation-lifecycle) · [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 `@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, `:` — `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` — 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. * `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)). Collections are unordered and served whole, save the balances, which are ordered by account then asset: there is no pagination yet. * **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 — carries `X-Immix-Global-Sequence`, the position of the last change this member had applied; 200 bodies restate it as `globalSequence`. It advances 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. Error bodies never carry a position field, and a refusal at the door (400, 401, 403, 503) carries neither. * **Version.** An entity response carries `ETag: ""`, 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. ## 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 (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": ""}`, `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` — for OKX `key`, `secret`, `passphrase`, each a non-blank string) 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). `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: ```json {"error": {"code": "VERSION_CONFLICT", "message": "…"}, "currentVersion": "7"} ``` `error.code` is the machine surface — a stable string to branch on; `error.message` is for humans and may change without notice, so never parse it; `currentVersion` appears on 412 only. The code set is open and append-only: branch on the codes you know and treat an unknown one by its HTTP status. **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` | | 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` | | 409 | `IDEMPOTENCY_KEY_REUSED`, `INTAKE_KEY_REUSED` | | 413 | `TOO_LARGE`, `PAYLOAD_TOO_LARGE` | | 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), `VENUE_UNSUPPORTED`; `SCALE_UNKNOWN` — retired at `0.3.4`, answered only by a member below it | | 429 | `INBOX_FULL`, `IN_FLIGHT_FULL`, `RATE_LIMITED` — 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), and the platform's own names for a command lost in transit (`LOST`, `REJECTED_NOT_REGISTERED`, `REJECTED_NOT_ACTIVE`) | **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). ## 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. * **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). * **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` | `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. ## 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. ## 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](/changelog/rest-api) is always headed by the served version. * **`info.x-immix-register-schema`**, **`info.x-immix-orders-schema`** and **`info.x-immix-custody-schema`** name the platform wire schemas the message shapes derive from (`114.v7`, `111.v1`, `115.v1`): every schema under `components.schemas` that shares a name with a message (`CredentialUpdated`, `SubmitOrder`, `AccountBalance`, …) 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. > The long form of the REST API's rules: authentication, reads and writes, maker-checker, money, errors, orders