> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tplus.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# API overview

> Services, authentication, signing, and conventions.

Two services, each serving its own live, interactive OpenAPI reference:

<CardGroup cols={2}>
  <Card title="Trading API" icon="code" href="https://oms.tplus.cx/api-reference">
    The Order Management System's REST + WebSocket surface: orders, positions, margin, account management, withdrawals, settlements.
  </Card>

  <Card title="Market Data API" icon="chart-line" href="https://mds.tplus.cx/api-reference">
    Public klines, depth, trades, and tickers over REST and WebSocket.
  </Card>
</CardGroup>

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](https://scalar.com) 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:

* Trading API: [`https://oms.tplus.cx/api-doc.json`](https://oms.tplus.cx/api-doc.json)
* Market Data API: [`https://mds.tplus.cx/api-doc.json`](https://mds.tplus.cx/api-doc.json)

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`](#sdks) handles the auth handshake, Ed25519 signing, and per-field amount encoding.

```bash theme={null}
pip install tpluspy
```

```python theme={null}
import asyncio
from tplus.client import OrderBookClient, MarketDataClient
from tplus.model.asset_identifier import AssetIdentifier
from tplus.model.limit_order import GTC
from tplus.utils.user import User

OMS = "https://oms.tplus.cx"
MDS = "https://mds.tplus.cx"
ASSET = AssetIdentifier(200)  # registry index, or "address@chain"

async def main():
    user = User(private_key="<ed25519-private-key-hex>")  # account master key
    async with (
        OrderBookClient(OMS, default_user=user) as client,  # signs + authenticates
        MarketDataClient(MDS) as md,                         # public, no auth
    ):
        book = await md.get_orderbook_snapshot(ASSET)
        market = await client.get_market(ASSET)             # decimals, tick, fees
        order = await client.create_limit_order(
            asset_id=ASSET, side="Sell", quantity=5, price=1_000,
            time_in_force=GTC(),
        )
        print(book.sequence_number, market.book_price_decimals, order.model_dump())

asyncio.run(main())
```

To drive the REST/WS API directly, do the [Authentication](#authentication) handshake for a bearer token, sign each order/withdrawal/settlement per [Request signatures](#request-signatures), and follow the per-field [amount encoding](#amounts). 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](#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`](#sdks) 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](/trading/multisig).

## Accounts

Balances and positions live in sub-accounts (spot, cross-margin, or isolated; see [account types](/trading/margin#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

| Identifier      | Wire form                                                                                                                        | Applies to                                                                                                                                                                                                                                |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Index(n)`      | Decimal string such as `"18"`; reserved `"0"` is USD                                                                             | Normal trading markets, order payloads, balances, positions, margin simulation, funding, OMS market routes, MDS public market data routes, and risk-parameter lookups.                                                                    |
| `address@chain` | `"<32-byte hex address>@<9-byte hex chain>"`                                                                                     | Isolated chain-specific assets in `asset_id`; withdrawal `asset` / `asset_address`; registry decimals requests; chain-specific inventory when a token is not fungified into an index.                                                     |
| `assetAddress`  | Conceptual 32-byte token address; in `/registry/assets`, this is the row `address` and the address part of the chain-address key | Registry asset rows from `GET /registry/assets`; used with `chainId` to form an `AssetAddress`. Settlement request fields `asset_in` and `asset_out` carry the same raw 32-byte token-address value, with `chain_id` supplied separately. |
| `chainId`       | 9-byte chain id (`routing_id` byte followed by 8-byte `vm_id`); in `/registry/assets`, encoded in the chain-address key          | Registry asset rows, settlement request `chain_id`, withdrawal asset address, decimals lookup, and vault chain selection.                                                                                                                 |
| Market id       | Same value as `asset_id`                                                                                                         | OMS order routes and market routes (`/market/{id}`, `/orders/user/{user_id}/{asset_id}`); MDS market data routes use the same asset identifier.                                                                                           |
| Vault address   | Settlement `vault` is an optional 32-byte address; `/registry/vaults` returns `ChainAddress` values                              | Settlement optional `vault` target and registry vault discovery. If omitted from settlement, CE selects a registered vault on the request chain.                                                                                          |
| Settler id      | Hex-encoded Ed25519 user public key                                                                                              | Settlement `settler` / `mm_pubkey` identity and settlement authorization; not an asset identifier.                                                                                                                                        |

## 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`](#sdks), 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](/trading/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:

```json theme={null}
{
  "error": {
    "code": "INSUFFICIENT_MARGIN",
    "message": "...",
    "retryable": false,
    "details": { "...": "..." },
    "trace_id": "...",
    "span_id": "..."
  }
}
```

`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](/api-reference/websockets) 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`](https://pypi.org/project/tpluspy/) on PyPI. Async REST/WS clients, typed models, Ed25519 signing; optional `[evm]` extra for onchain workflows.
