Skip to main content
Two services, each serving its own live, interactive OpenAPI reference:

Trading API

The Order Management System’s REST + WebSocket surface: orders, positions, margin, account management, withdrawals, settlements.

Market Data API

Public klines, depth, trades, and tickers over REST and WebSocket.
Each reference is served by the running service, so it always matches the live API. This page covers the cross-cutting conventions: authentication, signing, accounts, and finality.

Programmatic access

The /api-reference pages are client-rendered Scalar apps: the returned HTML is a shell that loads the spec via JavaScript; fetching it without a browser yields no endpoints. Fetch the machine-readable OpenAPI document instead: Use HTTPS; take exact paths from the spec. Endpoints are served at the service root with no prefix: GET https://oms.tplus.cx/markets, not /api/markets.

Quickstart

The Python SDK tpluspy handles the auth handshake, Ed25519 signing, and per-field amount encoding.
To drive the REST/WS API directly, do the Authentication handshake for a bearer token, sign each order/withdrawal/settlement per Request signatures, and follow the per-field amount encoding. For state-changing calls, read the response body first: some CE-proxy endpoints return success: false, and a positive ack is still not final confirmation; see Finality. After authenticating, the safest first state-read call is POST /account/simulate/{user_id}. It is non-mutating: OMS validates the request, reads inventory, books, registry, prices, protocol values, and fee state, then returns a projected margin result without forwarding a state change to the orderbook or clearing engine.

Authentication

User IDs are hex-encoded Ed25519 public keys. Session auth is a three-step flow:
  1. GET /nonce/{user_id} - returns a random nonce bound to the user, valid 5 minutes.
  2. Sign the nonce’s value as raw UTF-8 bytes with the Ed25519 private key.
  3. POST /auth - exchange the signed nonce for a bearer token, valid 24 hours.
Nonce behavior is replay-safe. A successful POST /auth consumes the nonce, so the same signed nonce cannot be exchanged again. A second GET /nonce/{user_id} returns the existing valid nonce when it has more than 10 seconds of viability left; otherwise OMS generates a replacement. Send Authorization: Bearer <token> and User-Id: <hex public key> on REST requests. On WebSocket, you may send those same headers or use Sec-WebSocket-Protocol. For v1 envelopes in browser clients, pass protocols in this order so the server echoes the version subprotocol: ["tplus.ws.v1", "Bearer-<token>", "User-<id>"]. The auth parser accepts Bearer- and User- in any comma-separated position, and tplus.ws.v1 opts the stream into the v1 envelope. There is no refresh endpoint today. To refresh a session, request a nonce and authenticate again. POST /logout revokes all bearer tokens for the authenticated user; after revocation, requests that reuse those tokens fail auth before reaching the handler. Tokens are keyed by user public key and token value, and token validation checks only that stored token and its expiry; multisig config changes do not revoke existing tokens in the current OMS code path. REST auth failures use the normal error envelope. Missing or invalid bearer credentials on protected routes return HTTP 401 with code UNAUTHORIZED; invalid signatures return 401 with code INVALID_SIGNATURE; nonce failures return 400 with NONCE_NOT_FOUND, NONCE_EXPIRED, or INVALID_NONCE; auth rate limits return 429 with RATE_LIMITED. WebSocket upgrade auth failures pass through the same REST rejection path before upgrade. After upgrade, user/path mismatches send a channel error (unauthorized on subscription streams; UNAUTHORIZED for the control-channel invalid-auth payload) and return from that stream without defining an auth-specific close code. V1 lag/resync uses close code 1013, which is not an auth signal. There is no public master-key rotation flow today, so post-rotation bearer-token semantics do not apply.

Request signatures

Independent of session auth, signed actions such as orders, withdrawals, settlements, and transfers carry an Ed25519 signature from the account’s master key. The OMS checks that the authenticated user matches the signer; the clearing engine re-verifies clearing-engine actions before finalizing. Replacing an order requires a new signature. The signed payload is the action-specific signable object serialized to compact JSON: whitespace stripped, fields in struct-declaration order, signed as raw UTF-8 bytes, not a hash. For example, withdrawal, settlement, transfer, and multisig-add flows sign their inner payload, while order create/cancel/replace use the order action’s own signable object. The exact byte layout (including how optional fields are serialized) must match or verification fails; use tpluspy unless you are reproducing it deliberately. Optional multisig registers weighted co-signers (secp256k1 / P-256 / WebAuthn) with per-action-category thresholds; manage them via the /multisig/* endpoints. See Multisig.

Accounts

Balances and positions live in sub-accounts (spot, cross-margin, or isolated; see account types). Orders target a sub-account index plus a spot-vs-margin flag. GET endpoints return data across all of the caller’s sub-accounts and accept an optional sub-account filter.

Identifying assets and markets

Markets and assets are identified by asset_id, not by a ticker symbol. An asset_id is either a protocol index ("18") or, for isolated chain-specific assets, "<token address>@<chain id>". The reserved index 0 is USD: the wire value is "0"/Index(0), and client symbol resolution should display it as USD.
  • GET /markets - tradeable markets keyed by asset_id (price/quantity decimals, leverage, tick and min size, fees).
  • GET /registry/assets - returns the cached CE asset config map for index assets. It returns 503 until the first CE snapshot; the JSON is nested by asset index and chain-address key, and each row includes the token address and deposit caps.
To attach a human-readable symbol, resolve the token onchain: take the token address and chain id from GET /registry/assets (the chain-address key encodes the chain id; the row address is the token address), or use the address@chain already inside an isolated asset_id. Then read the token contract’s symbol/name onchain, for example ERC-20 symbol() / name() on EVM chains. An index asset absent from /registry/assets has no onchain token to read and cannot be mapped to a symbol from the API alone; do not infer one.

Identifiers by API surface

Amounts

Internally, all balances are normalized to 18 decimals. On the wire, amount encoding depends on the field. Check each field’s type, or use tpluspy, which encodes for you:
  • Settlement and withdrawal request amounts (amount, amount_in, amount_out) are hexadecimal integer strings with no 0x. 1000000 is "f4240"; the decimal-looking "1000000" is read as 0x1000000 = 16,777,216. (The OpenAPI examples render decimal-looking strings, but they are parsed as hex.)
  • Transfer and simulate amounts are decimal strings (e.g. "1000000000000000000").
A wrong base is silent, not an error. Confirm the field before sending.

Conventions

  • Pagination: zero-based page plus limit. Defaults and caps vary per endpoint; see the endpoint reference. User orders are sorted newest first by timestamp_ns, then order_id descending; user trades are sorted newest first by timestamp_ns, then trade_id descending; positions are sorted by sub_account_index, then asset_id; funding history is sorted by timestamp_ns descending with no secondary tie-breaker defined today.
  • Rate limits return HTTP 429 with code RATE_LIMITED and retryable: true. OMS currently does not attach Retry-After or rate-limit reset headers.
  • Client deadlines: use more than 5 seconds for orderbook/clearing-engine proxy writes such as orders, withdrawals, and settlements; use more than 15 seconds for GET /withdrawal/queue/{user_id}. For pure OMS reads, pick a shorter deadline that fits your latency budget and retry policy.
  • Per-asset risk parameters: GET /registry/risk-parameters; vault addresses: GET /registry/vaults.
  • Three independent nonce families, never interchangeable: session-auth - the value from GET /nonce/{user_id}, signed and exchanged at POST /auth; chain actions (deposits, withdrawals, settlements) - independent per user, per chain, and per action type, read from the onchain vault counters; multisig config - config_nonce, returned as ce_nonce by GET /nonce/{user_id} and bumped on each successful /multisig/* change.
  • Order-level rules (ID format, batch limits, cancel semantics) are in Orders.
  • Deprecation/versioning: GET /margin/user/{user_id} currently returns Deprecation: true. Tplus does not currently publish a formal API versioning or deprecation policy.

Errors

REST errors share one envelope:
code is a stable SCREAMING_SNAKE_CASE string, message is human-readable, retryable says whether the same request might succeed later, and details is present on some codes. Common codes: INVALID_REQUEST, UNAUTHORIZED, INSUFFICIENT_MARGIN, INSUFFICIENT_INVENTORY, REDUCE_ONLY_WOULD_INCREASE, WOULD_CROSS, ORDER_EXPIRED, ORDER_NOT_FOUND, MARKET_NOT_FOUND, ALREADY_CANCELLED, REQUEST_TOO_OLD; the full set is in the spec. A malformed body is rejected with 400 before auth runs; rate-limited requests return 429.

Finality

Positive acknowledgments are optimistic: accepted for processing does not mean settled. Check endpoint response bodies first because some CE-proxy endpoints can return HTTP 200 with success: false. An accepted order or matched fill stays pending until the clearing engine confirms it, and may still roll back. The WebSocket streams carry pending, confirmed, and rolled-back states with rollback reasons (the at-fault party sees its own reason; counterparties see CounterpartyAtFault). REST trade history returns confirmed trades only.

SDKs

Python: tpluspy on PyPI. Async REST/WS clients, typed models, Ed25519 signing; optional [evm] extra for onchain workflows.