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

# Order Management

> Place and manage spot and perps orders, configure risk, and stream live execution updates.

Build trading experiences on Monaco by choosing the product surface your application needs. Spot and perps share market data, order entry, order management, live streams, and trading pair concepts, but they use different funding and risk models.

## Surface Map

| Surface | Use |
| - | - |
| REST | `GET /api/v1/orders`, `POST /api/v1/orders`, `PUT /api/v1/orders/{orderId}`, `POST /api/v1/orders/cancel`, `POST /api/v1/orders/batch-*`, `GET /api/v1/orders/conditional`, `DELETE /api/v1/orders/conditional/{conditionalOrderId}` |
| TypeScript SDK | `sdk.trading.placeLimitOrder()`, `placeMarketOrder()`, `cancelOrder()`, `replaceOrder()`, `batchCreate()`, `batchReplace()`, `batchCancel()`, `batchCancelAll()`, `listConditionalOrders()`, `cancelConditionalOrder()` |
| React SDK | `useTrade()`, `useUserOrders()` |
| WebSocket | `orders`, `orders:{tradingPairId}:{mode}`, `conditional_orders`, `conditional_orders:{tradingPairId}` |
| gRPC | `OrdersService` |

<CardGroup cols={2}>
  <Card title="Spot" icon="chart-candlestick" href="/trading-mechanics/spot">
    Spot trading uses balances deposited into Monaco's vault and settles swaps on Monaco's orderbook.
  </Card>

  <Card title="Perps" icon="scale" href="/trading-mechanics/perps">
    Perps trading uses collateral, margin, leverage, funding, positions, and liquidation controls.
  </Card>

  <Card title="Order Entry" icon="list-checks" href="#order-entry-and-management">
    Place, cancel, replace, batch, and monitor spot and perps orders.
  </Card>

  <Card title="Account Balances" icon="wallet-cards" href="/developers/account-balances">
    Read available, locked, and total balances before enabling order entry.
  </Card>
</CardGroup>

## Before You Build

* Use [Wallets & Auth](/developers/accounts/wallets), [Account Balances](/developers/account-balances), [Vault Deposits & Withdrawals](/developers/vault), and [Perps Collateral](/developers/perps-collateral) to understand wallet funds, Monaco spot balances, and perps collateral.
* Use [Markets](/developers/markets) and [Orderbooks & Trades](/developers/orderbooks-trades) to resolve pair IDs, product support, order constraints, and market data.
* Use [REST API](/api-reference), [WebSocket API](/reference/websockets), [gRPC API](/reference/grpc), or an SDK for markets, orderbooks, trades, candles, balances, orders, and live account events.

## Shared Trading Flow

Most spot and perps trading interfaces follow the same high-level loop:

<Steps>
  <Step title="Load markets and account state">
    Query supported trading pairs, product support, market metadata, and the user's balances or collateral before enabling order entry.
  </Step>

  <Step title="Render order controls">
    Use tick size, order-size limits, available balance, leverage constraints, and product support to validate inputs before submission.
  </Step>

  <Step title="Submit, replace, or cancel orders">
    Place market or limit orders, cancel stale orders before replacing intent, and use batch endpoints for active interfaces and agents.
  </Step>

  <Step title="Stream updates">
    Subscribe to order, balance, orderbook, trade, OHLCV, position, and movement streams so UI state tracks Monaco state.
  </Step>
</Steps>

### Self-Trade Prevention

What happens when your incoming order would match one of your own resting orders is selectable: `CANCEL_MAKER` (cancel your resting order and keep matching — the platform default), `CANCEL_TAKER`, `CANCEL_BOTH`, or `SKIP` (trade around your own orders). Pass `selfTradePreventionMode` on any create, replace, or batch item, or set a wallet-wide default with `PUT /api/v1/accounts/self-trade-prevention` (`sdk.profile.setSelfTradePreventionDefault`). "Your own" spans the whole wallet family — every subaccount and application sharing the wallet address. STP outcomes are cancellations, not errors — the affected orders terminate as `CANCELLED` with `terminalReason: "SELF_TRADE_PREVENTION"` — with one exception: a `FOK` order under `CANCEL_BOTH` whose fill path contains your own order is rejected with `SELF_TRADE_NOT_ALLOWED`. Full semantics, mode by mode: [Self-Trade Prevention](/trading-mechanics/self-trade-prevention).

## Spot Trading

Spot trading uses balances deposited into Monaco's vault. A spot integration usually needs account balances, supported markets, order entry, order management, and live order or balance updates.

### Spot Flow

<Steps>
  <Step title="Load markets and balances">
    Query supported markets, then load authenticated user balances so your UI can enable only markets and assets the user can trade.
  </Step>

  <Step title="Place orders">
    Submit market or limit orders against a spot trading pair. Use time-in-force and slippage controls based on the UX you expose.
  </Step>

  <Step title="Manage open intent">
    Cancel stale orders before submitting replacement intent. Use batch endpoints for active interfaces and agents.
  </Step>

  <Step title="Stream updates">
    Subscribe to order, balance, orderbook, trade, or OHLCV channels so the UI updates as Monaco state changes.
  </Step>
</Steps>

### Spot SDK Entry Points

```typescript theme={null}
const pair = await sdk.market.getTradingPair(tradingPairId);
const balances = await sdk.profile.getUserBalances();
const order = await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.0", "2000");
const unsubscribe = sdk.ws.orders(tradingPairId, "SPOT", handleOrderEvent);
```

## Perps Trading

Perps use a separate perps account for collateral and risk. A perps integration should make collateral, leverage, liquidation risk, funding, and reduce-only behavior explicit before users place orders.

### Perps Flow

<Steps>
  <Step title="Load collateral and positions">
    Query perps collateral, positions, funding, and risk state before enabling leveraged orders.
  </Step>

  <Step title="Preview risk">
    Simulate meaningful orders before submission so traders can see projected margin, free collateral, and liquidation impact.
  </Step>

  <Step title="Place or reduce exposure">
    Submit market or limit orders with the correct leverage and reduce-only settings for the user's intent.
  </Step>

  <Step title="Monitor continuously">
    Stream order, position, balance, and market updates. Perps UIs should keep mark price, margin ratio, funding, and liquidation distance current.
  </Step>
</Steps>

### Perps SDK Entry Points

```typescript theme={null}
const account = await sdk.marginAccounts.getParentMarginAccountSummary();
const risk = await sdk.marginAccounts.simulateParentMarginOrderRisk(orderPayload);
const { positions } = await sdk.positions.listPositions({ status: "OPEN" });
const order = await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
  tradingMode: "MARGIN",
  leverage: "10",
});
```

## Order Entry and Management

Orders are the shared execution primitive for spot and perps. The same SDK methods submit market and limit orders; spot orders use Monaco balances, while perps orders add leverage and reduce-only controls (`side` sets the one-way direction).

Use this section for order tickets, quoting interfaces, market-maker systems, and agent workflows.

<Note>
  Delegated-agent sessions can place, cancel, and replace orders only when the active policy allows the requested action and market scope. Monaco re-checks the delegation on every order mutation, so revocation or expiry blocks new delegated trading even if the delegated auth session itself has not expired.
</Note>

## Order Concepts

| Concept | What it means | Builder note |
| - | - | - |
| Trading pair ID | UUID for the market being traded | Resolve from [Markets](/developers/markets) and [Orderbooks & Trades](/developers/orderbooks-trades), then pass the UUID to order APIs |
| Side | `BUY` or `SELL` | For spot, buy spends quote and sell spends base |
| Quantity | Base-asset size as a decimal string | Use strings such as `"1.5"`, not floats for internal accounting |
| Price | Limit price as a decimal string | Required for limit orders |
| Trading mode | `SPOT` or `MARGIN` | Use `MARGIN` for perps |
| Time in force | `GTC`, `IOC`, or `FOK` | Applies to limit orders. A market order has none: order reads omit `timeInForce` and WebSocket events report it `null` |
| Slippage tolerance | Maximum price movement for market orders | Use for market orders and urgent exits |
| Client order ID | Your own label for an order, echoed back unchanged | Optional. Correlates a response to a request — see [Client Order IDs](#client-order-ids) |

## Place Orders

Trading operations require an authenticated Monaco session. Resolve a trading pair UUID first, then pass that UUID into order methods.

```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
import { createWalletClient, custom } from "viem";
import { seiTestnet } from "viem/chains";

const [account] = await window.ethereum.request({
  method: "eth_requestAccounts",
});

const walletClient = createWalletClient({
  account,
  chain: seiTestnet,
  transport: custom(window.ethereum),
});

const sdk = new MonacoSDK({
  walletClient,
  network: "staging",
  seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});

await sdk.login("your-client-id");

const pair = await sdk.market.getTradingPairBySymbol("ETH/USDC");
```

### Limit Orders

Limit orders execute at the specified price or better. They provide price protection, can rest on the book, and default to `GTC`.

```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(
  pair.id,
  "BUY",
  "2.0",
  "2000.00",
  {
    tradingMode: "SPOT",
    timeInForce: "GTC",
    expirationDate: "2026-12-31T23:59:59Z",
  }
);
```

Use limit orders when the user cares about price protection or wants to provide liquidity.

### Market Orders

Market orders execute immediately against available liquidity. Use slippage protection when the user cares about maximum price movement.

```typescript theme={null}
const order = await sdk.trading.placeMarketOrder(
  pair.id,
  "SELL",
  "0.5",
  {
    tradingMode: "SPOT",
    slippageTolerance: 0.01,
  }
);

if (order.matchResult) {
  console.log(order.matchResult.averageFillPrice);
  console.log(order.matchResult.totalFilled);
}
```

## Time in Force

Time-in-force controls how a limit order behaves when it cannot fully execute immediately.

| Type | Name | Behavior | Common use |
| - | - | - | - |
| `GTC` | Good Till Canceled | Rests until filled, canceled, or expired | Patient limit orders, maker liquidity |
| `IOC` | Immediate or Cancel | Executes immediately and cancels the unfilled remainder | Fast execution where partial fills are acceptable |
| `FOK` | Fill or Kill | Fills completely immediately or rejects entirely | Atomic execution, strict risk controls |

```typescript theme={null}
const iocOrder = await sdk.trading.placeLimitOrder(
  pair.id,
  "BUY",
  "10.0",
  "2000.00",
  { timeInForce: "IOC" }
);

const fokOrder = await sdk.trading.placeLimitOrder(
  pair.id,
  "SELL",
  "2.0",
  "50000.00",
  { timeInForce: "FOK" }
);
```

Use `GTC` when you want price protection and are willing to wait. Use `IOC` when you want immediate execution but can accept a partial fill. Use `FOK` when partial execution is worse than no execution.

<Note>
  Time-in-force options apply to limit orders. Market orders execute immediately and use slippage protection instead.
</Note>

### Post-Only Orders

`postOnly: true` guarantees an order can only add liquidity — it is a maker-only flag, not a new order type. Pass it alongside a normal limit order (spot or perps) to reject the order instead of letting it take: if the price would cross the book at admission time (`BUY` at or above the best ask, or `SELL` at or below the best bid), Monaco rejects it — REST `400`, gRPC `InvalidArgument` — with the structured engine code `POST_ONLY_WOULD_CROSS` instead of matching. For a single `placeLimitOrder` or `replaceOrder` call, that code rides on the response: REST puts it in the error envelope's optional `code` field, and gRPC attaches a `google.rpc.ErrorInfo` status detail with `reason` set to the code and `domain` set to `matching-engine.0xmonaco.com`. Match on the structured code; the rejection message still contains "post-only order would cross", but substring-matching it is a legacy fallback, not the contract. [Batch Create](#batch-create) and [Batch Replace](#batch-replace) carry the same code per item: each per-item result's structured `error.code` is set to `POST_ONLY_WOULD_CROSS` for a post-only crossing failure, as before. A non-crossing `postOnly` order is admitted and rests exactly like an ordinary limit order of the same time-in-force. The flag is persisted with the order and echoed back on order detail and order list responses as `postOnly: true` — like `reduceOnly`, it is absent when the order was not placed post-only (including orders placed before the flag was persisted).

`postOnly` only composes with `LIMIT` orders using `GTC` time in force. It is rejected on `MARKET` orders and rejected when combined with `IOC` or `FOK` (both are immediate-or-cancel semantics that contradict a maker-only guarantee).

```typescript theme={null}
try {
  const order = await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.0", "2000.00", {
    timeInForce: "GTC",
    postOnly: true,
  });
} catch (error) {
  if (
    error instanceof APIError &&
    error.statusCode === 400 &&
    (error.responseBody as { code?: string })?.code === "POST_ONLY_WOULD_CROSS"
  ) {
    // Price moved onto or through the book — adjust price and resubmit.
    // Retrying the same request verbatim will fail again.
  }
}
```

Use `postOnly` for market-maker quoting and for rebuilding quotes around a fresh price (for example, right after a market reopens) where an accidental take is worse than a rejected order you can immediately re-price.

<Note>
  `replaceOrder` and `batchReplace` each accept their own `postOnly` — a replacement re-prices the order, so the caller re-states maker-only intent explicitly; it is never inherited from the original order. See [Replace Orders](#replace-orders) and [Batch Replace](#batch-replace) for how a rejected post-only replacement affects the original order.
</Note>

### Safe Retries with Idempotency Keys

The TypeScript SDK always sends an `idempotencyKey` on `placeLimitOrder`, `placeMarketOrder`, and every `batchCreate` item. It generates a UUID when you omit the key; React hooks and MCP order tools inherit this behavior. This is separate from `clientOrderId`, which remains a reusable order label.

Each SDK call without an explicit key starts a **new submission**. For recovery across calls or process restarts, generate and persist your own key before the first call, then reuse it with the same order details. The SDK currently sends each request once; it does not automatically retry order placement. An `APIError` retains the submitted body, including its key, in `requestBody`.

```typescript theme={null}
const submission = {
  idempotencyKey: crypto.randomUUID(),
  clientOrderId: "btc-bid",
};
const result = await sdk.trading.placeLimitOrder(pair.id, "BUY", "1", "2000", submission);
// If the response is lost, retry the SAME order with the SAME submission key.
// Generate a NEW key only for a deliberately new order.
```

Keys contain 1–64 characters from `A-Za-z0-9._:-`, with no surrounding whitespace. They are scoped to your authenticated user/subaccount and application. For **24 hours after acceptance**, the same key and normalized order payload return the original committed submission result, even if the order has since filled, been cancelled, or the engine has restarted. This result describes the original submission; use order reads for current lifecycle state. Decimal spellings such as `1.0` and `1.00` are equivalent.

Reusing a retained key with different order details returns `IDEMPOTENCY_KEY_CONFLICT` (HTTP 409 / gRPC `ALREADY_EXISTS`). Requests rejected before acceptance do not reserve the key. An accepted order whose match result is `REJECTED` does reserve it: a retry returns that same result instead of trying to trade again.

Retries do not extend the window. **After 24 hours, the same key may place a new order.** Do not retry an unresolved submission beyond that window. Without an idempotency key, continue to follow the lost-response guidance below; a `clientOrderId` alone cannot prevent duplicate execution.

Two further codes describe the storage behind these keys, and both are retryable rather than terminal. `ORDER_SUBMISSION_CAPACITY_EXCEEDED` refuses a **new** key when retained submission receipts are at capacity because responses are protected by pending durability or persistence; existing retained receipts still replay while storage is full, and the refusal does not accept or reserve the key, so nothing was applied. Capacity frees as protected receipts finish acknowledgement and persistence, usually within seconds, so retry the same key with bounded backoff rather than waiting for anything to expire. It also refuses a new key when your own share is full: each user and application can hold at most 10,000 receipts (32 MiB of receipt payload) that are not yet evictable, and while one owner is at its share every other owner keeps placing orders; retries of an existing key and orders without a key are unaffected. `ORDER_SUBMISSION_UNAVAILABLE` means the original response for a retained key cannot currently be retrieved, or keyed admission is busy — read it as unknown, never as a rejection, because a submission under that key may already have executed. Both return HTTP 503 / gRPC `UNAVAILABLE` on a single create and arrive as a per-item `error` in a batch, and neither ever authorizes duplicate execution — retry the same key with the same normalized payload. Do not mint a fresh key for an order you already submitted under one: the original submission may still commit, and a new key is a second independently admissible order.

For `batchCreate`, the TypeScript SDK generates a separate key for each item that omits one. Supply and retain your own per-item keys when you need to retry across calls. Results remain in input order, failures stay per item, and an item's key also works when retrying that order through single create. A key protects its own item; it does not make the batch all-or-nothing. Only items carrying keys are safe to retry after an ambiguous batch outcome.

A create batch containing any `idempotencyKey` processes every item in input order through the single-create path. Other submissions can execute between items; the batch has no shared sequence or atomicity guarantee. Results retain input positions, and a failure does not roll back successful siblings. Unkeyed items sent by legacy or raw API clients remain unsafe to retry blindly. Updated TypeScript SDK batches always use the keyed path.

### Two-phase rollout

**Phase one:** REST and gRPC keep the field optional for existing clients. Updated TypeScript SDK calls always send keys; callers using raw REST or generated Rust/Python bindings must generate and retain keys before serialization and signing. Upgrade the migration, persistor/verifiers, matching engine, and **all public gateway replicas** before releasing clients that rely on this protection. Old gRPC replicas silently ignore the field.

**Phase two:** after client migration, require a key on every single create and every batch-create item at the API boundary. Reject missing keys before order admission; never generate a replacement key on the server. This enforcement is planned, not enabled in phase one. Confirm supported clients and direct integrations send keys before enabling it.

## Client Order IDs

`clientOrderId` is a label you choose and Monaco echoes back. Pass it on `placeLimitOrder`, `placeMarketOrder`, `replaceOrder`, `batchCreate` or `batchReplace`; read it back on order detail and order list responses, and on the order WebSocket events — the `OrderPlaced` acknowledgement, taker and maker fills, cancellation, expiry, and the `OrderRejected` raised after acceptance — an `IOC`/`FOK` that cannot fill, or a `MARKET` order whose apparent liquidity is invalidated by maker-risk revalidation. Because the acknowledgement and the fill both carry it, a matching event on the stream is positive proof that a submit whose HTTP response was lost did land — though only in that direction: silence never resolves the negative case. Maker fills carry the resting order's own handle, so a quote's fill identifies itself without a lookup.

It accepts up to 64 characters from `A-Za-z0-9._:-`. Surrounding whitespace is trimmed, and a blank string is treated as absent. Anything longer, or containing another character, is rejected with a `400` (gRPC `INVALID_ARGUMENT`). In a batch, both endpoints scope the rejection to the offending item with the same `error.code` `INVALID_CLIENT_ORDER_ID`, letting the rest proceed — and a failed `batchReplace` item never touches its original. (Earlier releases rejected the whole `batchReplace` request on one malformed handle.)

```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
  clientOrderId: "quote-btc-1130-a",
});
```

### Uniqueness among your resting orders

A `clientOrderId` may be held by only one of **your resting** orders at a time. On a *single* create or replace, reusing a value another of your resting orders is still carrying is rejected with `409` and the code `CLIENT_ORDER_ID_CONFLICT`; the message names the order already holding it.

In a **batch** (`batchCreate` / `batchReplace`) the same conflict is reported differently: the request itself succeeds with `2xx`/`OK` and only the offending item carries `error.code` `CLIENT_ORDER_ID_CONFLICT` in its `results[]` slot. There is no transport-level `409` to wait for — inspect `results[]`.

Read "resting" strictly — it means sitting on the book, not merely "recently sent". The scope is per user, and only among orders that are resting:

* **Only a resting order holds a `clientOrderId`.** An order that never rests — a `MARKET` order, an `IOC` or `FOK`, or a `LIMIT` that fills completely on arrival — does not take the handle, so the value stays free and a later create reusing it is accepted.
* **Two different users may use the same value simultaneously.** Uniqueness is never global — your labels are yours.
* **The value frees the moment its order reaches a terminal state** (filled, cancelled, expired, rejected). A stable per-quote label can be reused indefinitely as you re-quote — but **not if you intend to reconcile by it.** Reuse makes a matching event ambiguous: the previous holder's terminal event can still be in flight when the reused value is submitted again, so an event carrying that handle may belong to the earlier attempt. Use a **fresh value per create intent** when the handle is your reconciliation key. A replace is the deliberate exception — it restates the original's handle, so distinguish the two by `orderId`: the original's id you already hold, versus the replacement's new id.
* **A replacement may restate the original's value.** The original releases the handle as the replacement claims it, so `quote-btc-1130-a` can follow your quote across every replace.

<Warning>
  **For a legacy unkeyed submission, do not resubmit on a lost response.** `clientOrderId` is a correlation handle, not an idempotency key, and resubmitting is unsafe for any order that takes liquidity.

  If a create's response is lost and you resubmit the same request with the same `clientOrderId`:

  * If the first attempt **rested**, and is still resting, you get `409 CLIENT_ORDER_ID_CONFLICT` naming the order that holds it.
  * If the first attempt **executed** — a `MARKET` order, an `IOC`/`FOK`, or a marketable `LIMIT` that filled — it never took the handle, so **the resubmission is accepted as a new order and can execute again**. A success response does *not* mean the first attempt failed.
  * If the first attempt rested and has since **filled or been cancelled**, the handle is already free, so the resubmission is likewise accepted as a new order and can execute again.

  Treat a lost response as an **unknown** outcome and reconcile it before acting.
</Warning>

#### Recovering from a lost response

**Subscribe before you place.** The stream is not replayable: a slow-client disconnect
(close code `1013`) signals a gap on the whole connection, including state streams, and there is no wire sequence number to
backfill from — see [WebSockets](/reference/websockets#slow-client-disconnect-close-code-1013).
A subscription opened *after* your request timed out cannot recover the acknowledgement you
missed. Use `sdk.ws.userOrders(handler)` for every pair at once, or
`sdk.ws.orders(pairId, mode, handler)` for a single market.

The stream is pushed from the matching engine as each event occurs, and carries your
`clientOrderId` on the `OrderPlaced` acknowledgement, on taker and maker fills, on
cancellation and expiry, and on the `OrderRejected` raised after acceptance — an
`IOC`/`FOK` that cannot fill, or a `MARKET` order whose apparent liquidity is invalidated
by maker-risk revalidation — including for orders that never rested. A `MARKET` order that fills on
arrival emits two labelled events, `OrderPlaced` then `OrderFilled`, so the stream
acknowledges even a shape that never takes the handle on the book.

Rejections split in two, and the difference decides whether the stream can answer for you:

* **Rejected before acceptance** — a malformed or oversize order, a post-only order that
  would cross, a `clientOrderId` conflict, a plain `MARKET` submit with no liquidity
  inside the protective band, which is rejected during pricing before a sequence number is
  allocated, or a perp order the [perp price band](/trading-mechanics/perps#perp-price-band)
  keeps from filling anything. The request fails with an error status, the order never enters the book,
  and **no event is emitted**. It is never written to order history either, so it leaves no
  trace afterwards. That is safe rather than dangerous — nothing executed.
* **Rejected after acceptance** — a `LIMIT` with `IOC` or `FOK` that cannot fill on arrival,
  or a `MARKET` order whose apparent liquidity is invalidated by maker-risk revalidation
  after it was accepted. The request **succeeds** with a `2xx`, `matchResult.status` is
  `REJECTED`, and the stream emits `OrderPlaced` then `OrderRejected`, both carrying your
  `clientOrderId`.

<Warning>
  **A success response does not mean the order executed.** An `IOC` or `FOK` that cannot fill —
  or a `MARKET` order that loses its apparent liquidity to maker-risk revalidation — is accepted
  and then rejected, so it returns a `2xx` with nothing traded. Read `matchResult.status` rather
  than inferring the outcome from the HTTP status.
</Warning>

**How to read silence.** You cannot. A matching event is positive proof the order was
accepted, but silence is never proof of the opposite — and there is no client-observable
condition that makes it so. The fan-out is non-persistent Core NATS, and the service can
re-establish an ended upstream subscription without closing your socket, so events can be
missed while your connection still looks perfectly healthy. Treat a bounded wait that
returns nothing as **unresolved**, not as a negative answer.

**When the stream cannot answer**, check whether your position or balance moved. For a
taking order that is the economic truth, and because it does not depend on the order read
model it survives replica lag.

Do **not** reconcile by polling the order list. Order history is written asynchronously and
served from a read replica, so immediately after a create it can report "not found" for an
order that did land — most likely exactly when you need it, since a timeout usually means
the system is busy. A missing row is not evidence. Note also that "rejected" splits here: an
order rejected *before* acceptance has no row at all, while one rejected *after* acceptance is
persisted with status `REJECTED` — so a `REJECTED` row is real information, but its absence
still is not.

If no surface is conclusive, **hold rather than resubmit.** The two mistakes are not
symmetric: a wrongful resubmit doubles live exposure, while a missed quote can simply be
quoted again.

<Warning>
  **It is not an idempotency key.** Monaco does not return the original order on a duplicate — where the guard fires at all, it rejects the duplicate. It also does not compare the rest of the payload: a resubmission with the same `clientOrderId` but a different price or quantity is rejected just the same, because the handle is what is taken, regardless of what else changed.

  Reuse after the original is terminal is legal and expected, so a lookup by a value you have reused across many orders is ambiguous — the guarantee is only that at most one *resting* order holds it at any instant.
</Warning>

Where the conflict *does* fire — a resting order you are re-quoting — it is a reliable signal, because the check runs in the matching engine against live state rather than against replicated history. The limitation is its scope, not its freshness.

Orders placed before the field existed return no `clientOrderId` at all.

## Perps Order Options

Perps use the same order methods with margin-specific options. New integrations can usually let Monaco resolve the parent margin account and risk bucket; explicit `marginAccountId` remains available for legacy or fixed-account workflows.

```typescript theme={null}
await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.5", "2000", {
  tradingMode: "MARGIN",
  leverage: "10",
});

await sdk.trading.placeMarketOrder(pair.id, "BUY", "0.5", {
  tradingMode: "MARGIN",
  reduceOnly: true,
});
```

| Option | Type | Notes |
| - | - | - |
| `tradingMode` | `"SPOT" \| "MARGIN"` | Use `"MARGIN"` for perps |
| `leverage` | string | Decimal string, such as `"10"`. Required for normal margin orders; omit only for reduce-only |
| `reduceOnly` | boolean | Order can only reduce existing exposure |
| `marginAccountId` | string | Optional legacy override for explicit-account flows |
| `positionSide` | `"LONG" \| "SHORT" \| "NONE"` | Deprecated — `side` is the authoritative direction. If supplied, must agree with `side` |
| `riskBucketId` | string | UUID of an existing isolated risk bucket to trade against (wire `riskBucketId`) |
| `marginMode` | `"ISOLATED" \| "CROSS"` | Defaults to `ISOLATED`. One margin mode per market and margin account: while the account holds the pair in its other risk bucket (an open position, a resting order that is not reduce-only, or a live TWAP parent that is not reduce-only), an order that is not reduce-only is rejected with `MARGIN_MODE_CONFLICT`; reduce-only orders always pass. On an isolated-only market (`TradingPair.isolatedOnly`) a `CROSS` order that is not reduce-only is rejected with `CROSS_MARGIN_NOT_ALLOWED` |
| `riskBucketCollateral` | string | Collateral (decimal string) to seed a **new** isolated risk bucket — only valid on the **first order for a trading pair**, when no isolated bucket exists yet. Must be omitted once a bucket exists; the backend auto-resolves the existing bucket. Passing this field when a bucket already exists returns `400`. Mutually exclusive with `riskBucketId`. The amount is the bucket's **own collateral**: it backs the bucket's positions and stays in the bucket until you release it. Omit it and the bucket is created empty. Either way, whatever an order needs beyond the bucket's own collateral is drawn from the parent margin account when the order is admitted, and that draw goes back to the parent when the order is cancelled; a draw never counts toward a position's liquidation price |
| `strategyKey` | string | Optional strategy tag used to resolve a pair-scoped risk bucket |

Use `riskBucketId` to route an order into an existing isolated risk bucket. Use `riskBucketCollateral` only on the first order for a new trading pair — when no isolated bucket exists yet; once the bucket is created, omit this field and the backend auto-resolves it. Passing `riskBucketCollateral` when a bucket already exists returns `400`. `riskBucketId` and `riskBucketCollateral` are mutually exclusive. Use `strategyKey` when a bot, strategy, or UI workspace should keep risk separated for the same market. See [Margin Accounts](/sdk/typescript/margin-accounts) for the risk-bucket model.

A `riskBucketId` that does not exist or is not yours returns `404` `Risk bucket not found`, checked before anything else about the bucket. For one of yours, a bucket scoped to a different trading pair or margin account returns `400` `riskBucketId does not belong to tradingPairId` / `riskBucketId does not belong to marginAccountId`, and a bucket under a non-default `strategyKey` — a [copy trading](/developers/copy-trading) risk bucket included — returns `400` `riskBucketId names a copy trading risk bucket; copy risk buckets are managed by the engine`. Route strategy-keyed orders by `strategyKey` rather than by `riskBucketId`.

Admission is the one collateral check for a margin order. The risk bucket's own collateral covers the order first; if it falls short, the engine draws exactly the order's additional requirement from the parent margin account's unallocated capacity, atomically with the order. What an order is charged depends on how it trades. A crossing order is charged its initial margin at the mark, the loss each fill takes against the mark (a sell into a bid below the mark, a buy into an ask above it), and its taker fee; a resting order is charged its initial margin at its limit and its maker fee, and one resting through the live mark (a sell below it, a buy above it) is charged as if it filled now: its initial margin at the mark and the loss it would take against it. Only the part that opens or flips a position is charged: a close is never refused for its loss. The draw covers only what the orders cost, so a position's loss never stops a new order the parent can fund and is never drawn for; a risk bucket below its initial margin stays below it until you add collateral or reduce. Once admitted, an order is never rechecked or topped up when it fills, whether it fills as a taker, as a resting maker, or rests after partially crossing. A fill keeps in the risk bucket only what its new position needs and returns the rest of the draw; a cancel, smaller replace or IOC remainder returns everything it releases. Liquidation freezes still reject new orders. No client flag is required; use risk simulation and `availableOrderCollateral` for pre-trade sizing rather than assuming parent headroom guarantees admission.

### Risk Buckets and Pre-Trade Simulation

A parent margin account holds reusable collateral. Orders create or reuse pair-scoped risk buckets under that parent. New integrations can preflight orders without storing a `marginAccountId`.

Use parent simulation when the order should draw from the user's general parent margin collateral:

```typescript theme={null}
const risk = await sdk.marginAccounts.simulateParentMarginOrderRisk({
  tradingPairId: pair.id,
  side: "BUY",
  orderType: "LIMIT",
  price: "2000",
  quantity: "1.5",
  leverage: "10",
  reduceOnly: false,
});

if (!risk.accepted) {
  showError(risk.rejectReason);
  return;
}

console.log(risk.freeCollateralAfter);
const estimatedLiquidationPrice = risk.estimatedLiquidationPrice?.trim();
renderLiquidationPrice(
  estimatedLiquidationPrice
    ? risk.marginMode === "CROSS"
      ? `Conditional liquidation price: ${estimatedLiquidationPrice}`
      : estimatedLiquidationPrice
    : "unavailable",
);
```

Use risk-bucket simulation when the order targets a specific market bucket or strategy key. This endpoint (`POST /api/v1/margin/risk-buckets/simulate-order-risk`) accepts `marginMode: "ISOLATED"` (default) or `marginMode: "CROSS"`. A cross preview names no pairs: cross scope is derived from trading, so the previewed pair is in scope by construction and the response's `selectedTradingPairIds` reports the derived scope. Isolated simulation models the risk bucket a first order would create when none exists yet; cross simulation works before the user has placed their first cross order. For parent-margin simulation without a specific bucket, use `simulateParentMarginOrderRisk` above.

A refused preview answers `accepted: false` with `rejectReason` (the message placement would return) and `rejectCode` (the structured code placement would return in the error envelope, e.g. `MARGIN_MODE_CONFLICT` when the margin account holds the pair in its other margin mode, or `INSUFFICIENT_LIQUIDITY` for an `IOC` order that would not fill) — branch on the code, not the text. An order that closes an isolated position flat, when that close retires the risk bucket (nothing else resting in it and no live TWAP order holding it open), reports the risk bucket as closed, with `equityAfter`, `initialMarginRequiredAfter`, `maintenanceMarginRequiredAfter` and `freeCollateralAfter` all `"0"`; otherwise the preview returns the retained risk bucket's actual values. `rejectCode` is present for client-class refusals; it is absent when the preview failed on a server-side fault, and `rejectReason` is then a generic retry message. For isolated simulation, `estimatedLiquidationPrice` is the simulated position/risk-bucket threshold. For `marginMode: "CROSS"`, it is conditional per target position, not a whole-account scalar: it varies only the target position's mark while all other marks in the cross risk bucket remain unchanged. Other position marks, funding, realized PnL, fees/reserves, and collateral can change it. Render an absent or blank estimate as unavailable, never `0`.

```typescript theme={null}
const risk = await sdk.marginAccounts.simulateRiskBucketOrderRisk({
  tradingPairId: pair.id,
  strategyKey: "btc-mm-a",
  side: "BUY",
  orderType: "LIMIT",
  price: "2000",
  quantity: "1.5",
  leverage: "10",
});

if (risk.accepted) {
  const estimatedLiquidationPrice = risk.estimatedLiquidationPrice?.trim();
  renderLiquidationPrice(
    estimatedLiquidationPrice
      ? risk.marginMode === "CROSS"
        ? `Conditional liquidation price: ${estimatedLiquidationPrice}`
        : estimatedLiquidationPrice
      : "unavailable",
  );
}
```

Run risk simulation on debounced order-form changes, surface `rejectReason` directly, and block submission when the simulated order is not accepted.

### Reduce-Only

`reduceOnly: true` guarantees the order can only shrink an existing position. It cannot flip a position or grow exposure. If the reducing portion is no longer available by the time the order matches, the remainder is cancelled instead of extending risk.

Common uses:

* Closing a position with a market order
* Placing a limit close at a target price
* Implementing manual stop logic that fires a reduce-only market order

## Conditional Perps Orders

Take-profit and stop-loss orders are conditional perps orders. They sit dormant until the mark price crosses a trigger, then submit the underlying market or limit order.

Conditional orders trigger on mark price:

* TP on a long triggers when mark rises to or above `triggerPrice`.
* TP on a short triggers when mark falls to or below `triggerPrice`.
* SL on a long triggers when mark falls to or below `triggerPrice`.
* SL on a short triggers when mark rises to or above `triggerPrice`.
* A trailing stop on a long triggers when mark falls to or below `watermark × (1 − trailBps/10000)`.
* A trailing stop on a short triggers when mark rises to or above `watermark × (1 + trailBps/10000)`.

To check whether a target clears its own costs before you set it, preview the close as a reduce-only MARKET order of the position's size with `simulateParentMarginOrderRisk` or `simulateRiskBucketOrderRisk`: `expectedMatchResult.actualSlippageBps` is the impact the current book would charge that size (apply it to your trigger price, not the current mark) and `estimatedFee` is the close leg's fee at your tier — see [Pre-trade simulation](/sdk/typescript/margin-accounts#pre-trade-simulation).

Attach TP/SL to an open position with `attachPositionTpSl`:

```typescript theme={null}
await sdk.positions.attachPositionTpSl(positionId, {
  takeProfit: {
    triggerPrice: "2400",
    orderType: "MARKET",
  },
  stopLoss: {
    triggerPrice: "1850",
    orderType: "LIMIT",
    limitPrice: "1845",
    timeInForce: "GTC",
  },
  oco: true,
});
```

Pass `oco: true` to make the two legs one-cancels-other: when one triggers, the sibling is cancelled (requires both legs). Set `closePosition: true` on a leg to resolve the full live position size on trigger. Subscribe to conditional-order events and regular order events to confirm both the trigger and the resulting fill.

A full-close (`closePosition`) leg attached this way **supersedes** the position's previously active full-close legs of the same condition type: attaching a new closePosition take-profit cancels the old closePosition take-profits atomically in the same engine step, and likewise for stop-losses — so moving a TP is one call, with no window where the position holds both (or neither) of the two levels. Each superseded leg announces itself with a `conditional_order_update` event carrying `reason: "cancelled"` before the replacement's `"created"`.

Conditional creation is also subject to resident cardinality limits: by default 10 active legs per position, 100 resident legs per user on an engine shard (including pending entry-order legs), and 100,000 resident legs on that shard. Deployments can override the defaults. Position attaches check all three; entry-order TP/SL placement checks the user and shard limits before admission. A create that would exceed a limit creates none of its new legs. Entry-order placement reports REST `400` / gRPC `INVALID_ARGUMENT` (or a per-item batch rejection); the position-attach adapter instead reports an engine cap refusal as generic REST `500` `MATCHING_ENGINE_ERROR` / gRPC `INTERNAL`. That status alone does not identify a cap refusal. Full-close legs superseded by the attach are subtracted before the cap check, so moving an existing level at the cap does not require an extra slot. Cancel unused fixed-quantity legs rather than retrying a known over-cap create unchanged.

Three things deliberately survive a supersede:

* the superseded leg's **OCO sibling** — a moved TP does not silently drop your stop;
* every **fixed-`quantity` leg** — laddered partial take-profits stack; only full-close legs replace each other;
* **trailing stops**, which are a separate rail with their own one-per-position rule.

Full-close legs attached to an **entry order** (below) compete for the same slot once they arm, and the leg you set **most recently** wins — judged by when each leg was created, not by when it armed. A position keeps at most one active full-close take-profit and one full-close stop-loss:

* attaching a position TP/SL retires an already-armed entry-order full-close leg of the same type;
* a leg still pending on an unfilled entry order is left alone — it protects exposure that does not exist yet, and is cancelled with its order as usual. If that order later fills after you attached a newer position TP/SL, its leg is cancelled instead of armed, so your later choice stands;
* if you attach a position TP/SL first and later place an entry order with its own full-close TP/SL, the entry's legs are the newer choice and replace the position's when the entry fills.

A leg retired this way announces `reason: "cancelled"` with the failure reason "superseded by a newer full-position TP/SL of the same type". Fixed-`quantity` entry-order legs never compete.

Alternatively, attach TP/SL to the entry order itself: pass `takeProfit`/`stopLoss` on `placeLimitOrder` / `placeMarketOrder` (margin entry orders only — not reduce-only). The close direction follows the order's `side`. If the entry order rests, the legs are created pending and activate when the order fills; they are cancelled automatically if the order is cancelled or expires unfilled. When both legs are supplied they are OCO-linked. The response returns `takeProfitOrderId` / `stopLossOrderId`.

Each entry-attached leg sizes itself with the same contract as a position-attached leg: omit both `quantity` and `closePosition` (or set `closePosition: true`) to resolve the full live position size when the leg triggers, so it keeps covering the position as it grows; or set a `quantity` for a deliberate partial that never resizes. The two are mutually exclusive, and `quantity` is required when `closePosition` is `false`. (Before this contract existed, an entry-attached leg was frozen at the entry order's quantity, so a position that later grew past it kept a stop covering only part of what was held.)

```typescript theme={null}
await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
  tradingMode: "MARGIN",
  leverage: "5",
  takeProfit: { triggerPrice: "2400", orderType: "MARKET" },
  stopLoss: {
    triggerPrice: "1850",
    orderType: "LIMIT",
    limitPrice: "1845",
    timeInForce: "GTC",
  },
});
```

### Trailing Stop

A trailing stop is a reduce-only market close that fires when the mark price retraces from its best level since arming by `trailBps`.

* Long: trigger = watermark × (1 − trailBps/10000), fires when mark falls to or below it.
* Short: trigger = watermark × (1 + trailBps/10000), fires when mark rises to or above it.
* The watermark is the best mark seen since arming, so the trigger only ever moves in the position's favour.
* `activationPrice` (optional) starts tracking when the mark reaches that level and seeds the watermark there; it is rejected if the mark has already reached it. Omitted, tracking starts at the current mark.
* One trailing stop per position — a second attach returns `409` — and there is no modify: cancel it and attach a new one. It coexists with fixed TP/SL and never joins OCO.
* Attached to an entry order, it arms only when the order fully fills and is cancelled with the order otherwise.

Attach one to an open position, alone or alongside fixed legs:

```typescript theme={null}
await sdk.positions.attachPositionTpSl(positionId, {
  trailingStop: { trailBps: 150, activationPrice: "2400" },
});
```

Or on the entry order (margin, not reduce-only):

```typescript theme={null}
await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
  tradingMode: "MARGIN",
  leverage: "5",
  trailingStop: { trailBps: 150 },
});
```

Both return `trailingStopOrderId`, though an entry order returns it only when the trailing stop materializes: the order fully fills on arrival or leaves a resting remainder; a partial fill whose remainder is cancelled (an `IOC` or `MARKET` order) creates no trailing stop and omits the id. The row reads `conditionType: "TRAILING_STOP"` and, once armed by its position (or by its entry order's full fill — until then it waits as `PENDING_PARENT`), stays `state: "ACTIVE"` while waiting for activation, with `trailArmedAt` set once tracking begins and `watermarkPrice` next to the live `triggerPrice`. On the `conditional_orders` channel it emits `reason: "armed"` when tracking begins and `reason: "ratcheted"` on every trigger move, each frame carrying the new `triggerPrice` and `watermarkPrice`.

### Cancel and List Conditional Orders

```typescript theme={null}
await sdk.trading.cancelConditionalOrder(conditionalOrderId);

const { orders } = await sdk.trading.listConditionalOrders({
  marginAccountId: marginAccountId,
  tradingPairId: pair.id,
  state: "ACTIVE",
});
```

### Conditional Order Lifecycle

| State | Meaning |
| - | - |
| `PENDING_PARENT` | Attached to a parent order that has not filled yet |
| `ACTIVE` | Created and waiting for trigger |
| `TRIGGERING` | Trigger condition met; underlying order is being submitted |
| `TRIGGERED` | Underlying order submitted and now follows regular order lifecycle |
| `CANCELLED` | Cancelled by the user, by its OCO sibling triggering, with its entry order, by its position closing, by a superseding full-close order, or refused as a second trailing stop (`reason` says which) |
| `EXPIRED` | The trigger fired but the position it was attached to was no longer open, had been replaced, or had flipped side (see `failureReason`) |
| `FAILED` | Trigger fired but underlying order submission failed |

### Trigger Price Is Not Fill Price

A conditional order fires on the **mark price** and then trades against the **book**, so the price it triggered at and the price its close filled at are two independent numbers. The mark is a reference price, not the book's midpoint, and the trigger worker samples it on a tick, so by the time the close is placed the mark may already be well past the level. A `MARKET` leg then pays the spread it crosses plus whatever depth the size consumed — a large close walks several levels — which usually leaves it worse than the trigger price, but there is no guaranteed minimum gap and a fast move can leave it better. A `LIMIT` leg may rest (`SUBMITTED` / `PARTIALLY_FILLED`) or fill only partly, and a `MARKET` leg that finds nothing fillable inside the 1,200 bps protective band is cancelled with a `terminalReason`. Round-trip taker fees apply on top, so a take-profit set within a tick or two of entry can trigger exactly as set and still close at a loss.

The read surfaces carry both sides so the two never have to be reconciled by hand:

* A `TRIGGERED` conditional order (`getConditionalOrder` and `listConditionalOrders`) carries `triggeredOrder` — the close's current `status`, `filledQuantity`, `averageFillPrice`, `totalTakerFees`, `filledAt` and `terminalReason`, read from that order at request time. Compare `triggeredOrder.averageFillPrice` with `triggerPrice` to see the spread and impact the close actually paid. Compare them as exact decimals rather than with `===` on the raw strings: the two reads can print different scales, and because this summary is a repository read while `getOrder` is cache-first, persistor lag can also leave them showing different states — read `getOrder` for the close's very latest. The field is absent until the trigger fires, and is not carried on the `conditional_orders` WebSocket frames.
* The close itself carries `conditionalOrderId` in order history (`getOrder`, `getPaginatedOrders`, `orderType: "MARKET"` or `"LIMIT"`), pointing back at the take-profit or stop-loss that placed it. It is absent on orders you placed yourself and on triggered closes written before the field existed — there is no backfill, so a missing value on an older close means unknown, not placed by hand.

```typescript theme={null}
const conditional = await sdk.trading.getConditionalOrder(conditionalOrderId);
if (conditional.state === "TRIGGERED" && conditional.triggeredOrder) {
  console.log(conditional.triggerPrice);                    // "0.1453"  — where the mark fired it
  console.log(conditional.triggeredOrder.averageFillPrice); // "0.145611" — where the close filled
  console.log(conditional.triggeredOrder.status);           // "SETTLED" once the close has filled, "SUBMITTED" while a LIMIT leg rests
}
```

```typescript theme={null}
const unsubscribe = sdk.ws.conditionalOrders((event) => {
  console.log(event.data.conditionalOrderId);
  console.log(event.data.state);
  console.log(event.data.reason);
  // The "triggered" update carries state TRIGGERED with triggeredOrderId and
  // triggeredAt: the close the trigger placed. The subscribe-time snapshot
  // lists only PENDING_PARENT, ACTIVE and TRIGGERING rows, so after a
  // reconnect read a trigger you missed from REST.
  if (event.data.reason === "triggered") {
    console.log(event.data.triggeredOrderId);
  }
});
```

To follow TP/SL all the way through execution, subscribe to both conditional-order events and the ordinary execution/state channels. The conditional event confirms the trigger. The triggered close then publishes through the same orderbook, order, trade, OHLCV, position, and balance paths as a directly submitted order, so those channels carry the fill and resulting state rather than requiring a manual refresh.

## Manage Orders

Order management is part of the active trading loop. Monaco supports single-order cancellation, batch cancellation, cancel-all, replacement, batch replacement, and batch creation.

### List Orders

`GET /api/v1/orders` ([`sdk.trading.getPaginatedOrders`](/sdk/typescript/trades#order-queries)) lists the caller's **book orders** — `LIMIT` and `MARKET` orders, resting and historical — as one stream sorted by timestamp across the hot and archived stores. Filter `status=SUBMITTED,PARTIALLY_FILLED` for the resting working set.

TWAP parents and conditional (take-profit / stop-loss) orders are not book orders and never appear in this listing. Read parents from `GET /api/v1/orders/twap` (`sdk.trading.listTwapOrders`) and conditional orders from `GET /api/v1/orders/conditional` (`sdk.trading.listConditionalOrders`); both filter by state and market. A TWAP's slices — the child orders that carry `parentOrderId` — are not rows here either, resting or finished, and are not counted in `total`: read them with `GET /api/v1/orders/twap/{twapOrderId}/slices` (`sdk.trading.listTwapOrderSlices`). The order a take-profit or stop-loss fires into *is* a book order: it is listed here as its own `MARKET` or `LIMIT` row carrying `conditionalOrderId` back to the trigger, and the conditional order's `triggeredOrder` summarizes what that close did — see [Trigger Price Is Not Fill Price](#trigger-price-is-not-fill-price).

`origin` says what placed a book order: `USER`, `LIQUIDATION`, `ADL`, `COPY` or `SYSTEM`. Label a liquidation from it. The order the liquidation engine places to close a position is a reduce-only IOC limit, so its `orderType`, `timeInForce` and `reduceOnly` are those of an order the user could have placed, and a history row drawn from them alone reads "Limit · IOC · Reduce Only". The order WebSocket events carry the same key with the same labels. `TWAP` and `CONDITIONAL` rows carry no `origin`.

A TWAP parent's resting child orders are *not* listed as separate rows you can act on — the parent is the only control surface, so cancel the parent rather than its children (see [TWAP Orders](/trading-mechanics/twap-orders)). Likewise, the `LIMIT` leg a take-profit or stop-loss fired into (a row carrying `conditionalOrderId`) can be cancelled but not replaced: a replacement would rest under a new id the conditional order does not point at, so `PUT /api/v1/orders/{orderId}` and batch replace refuse it with `INVALID_ORDER`. Cancel it and place a new order instead.

### Cancel Orders

```typescript theme={null}
const cancelResult = await sdk.trading.cancelOrder(orderId);

console.log(cancelResult.orderId);
console.log(cancelResult.status);
```

Canceling an open order releases locked balance and removes the order from the book. It cannot be undone.

Pass `remainingQuantityTarget` for a **partial cancel** — the resting order is reduced *in place* to that remaining quantity and keeps its queue position, unlike `replaceOrder`, which retires the order and rests a new one at the back of the book even for a pure size-down. Only the released delta unlocks.

```typescript theme={null}
// Leave 0.5 resting, keeping queue position; release the rest
await sdk.trading.cancelOrder(orderId, { remainingQuantityTarget: "0.5" });
```

The target means "leave this much resting", so a retried request converges instead of compounding. It must be a positive multiple of the market's quantity step and strictly below the order's current remaining quantity — emptying an order is a full cancel's job, and growing one is a replace. The reduction emits a non-terminal `OrderPartiallyCancelled` event on the `orders` WebSocket channel; the order is still resting.

<Note>
  Monaco prioritizes cancels ahead of new order intent. A cancel only protects quantity that has not already matched; fills that happen before the cancel is processed remain valid.
</Note>

### Batch Cancel

```typescript theme={null}
const result = await sdk.trading.batchCancel([
  "order_123",
  "order_456",
  "order_789",
]);

console.log(`Cancelled ${result.totalCancelled} of ${result.totalRequested}`);

for (const item of result.results) {
  if (item.error) {
    console.log(`Cancel failed for ${item.orderId}: ${item.error.message}`);
  }
}
```

Batch cancels are best-effort per order. Some cancellations can fail while others succeed, so inspect individual results before updating UI state.

### Cancel All

```typescript theme={null}
await sdk.trading.batchCancelAll();

// Or scope to one trading pair.
await sdk.trading.batchCancelAll(pair.id);
```

Use cancel-all actions for emergency exits, cleanup flows, or agents that need to clear stale intent before submitting new quotes.

Cancel-all is capped at **20,000 matching active orders**. Above that cap, it cancels nothing and returns REST `400` / gRPC `InvalidArgument`. Narrow the request by trading pair or cancel explicit batches of at most 100 IDs. The cap is checked before any cancellation.

Handle these request-level failures before treating the response as a per-order result: REST `400` / gRPC `InvalidArgument` for an invalid pair UUID or the cap; REST `401` / gRPC `Unauthenticated` for invalid authentication; REST `403` / gRPC `PermissionDenied` when a delegated session omits its required pair scope or lacks permission; REST `429` / gRPC `ResourceExhausted` when the call exceeds your risk-reduction rate budget — cancel-all costs one item per call however many orders it cancels, so this is about call rate, not order count (see [Rate Limits](/developers/rate-limits)); REST `500` / gRPC `Internal` for an internal matching-engine failure; and REST `503` / gRPC `Unavailable` for a transient matching-engine or transport failure.

### Replace Orders

Replacing an order atomically cancels the original order and creates a new one. The replacement receives a new order ID.

```typescript theme={null}
const replaceResult = await sdk.trading.replaceOrder(orderId, {
  price: "2100.00",
  quantity: "1.5",
});

console.log(replaceResult.orderId);
console.log(replaceResult.originalOrderId);
```

You can update price, quantity, or both. Filled, canceled, expired, or rejected orders cannot be replaced. `quantity` is the order's new **total**, not a fresh size: on a partially filled order the replacement rests `total - filled` and locks collateral for that remainder only (FIX cancel/replace convention, `LeavesQty = OrderQty - CumQty`), so it must exceed the amount already filled — replacing a 96/100-filled order with `quantity: "200"` rests 104 and reads as 96/200 filled. Omit `quantity` to keep the order's total; the resting remainder is then derived against the live fill state, so a fill landing while the request is in flight cannot make the replacement rest more than the true remainder. The replacement also inherits the original's fill history (filled quantity, VWAP, realized fee/payment aggregates), so summing those aggregates across a replacement chain double-counts — per-order and trade-level reads are unaffected. Margin reduce-only orders follow the same rule: on a reduce-only close of 0.8 with 0.3 filled, `quantity: "0.5"` rests 0.2 and reads as 0.3/0.5 filled. What a reduce-only replacement rests (`total - filled`) is still bounded by the live position, so a total that would rest more than the position is refused. `batchReplace` items carry the same total semantics. Pass `postOnly` to re-state maker-only intent for the replacement — see [Post-Only Orders](#post-only-orders); it is never inherited from the original order. `selfTradePreventionMode` follows the same rule: the replacement is a new order and carries its own value, falling back to your wallet default when omitted — see [Self-Trade Prevention](#self-trade-prevention).

The response's `updatedFields.quantity` reports the replacement's **total**, not its resting size, on `replaceOrder` and on each `batchReplace` result alike. With `quantity` omitted, that is the original order's total: repricing a 60/100-filled order reports `"100"` and rests 40. Releases before v1.0.44 reported the unfilled remainder (`"40"`) in this case; an explicit `quantity` has always been echoed as given. Read the order's `remainingQuantity` when you need the resting size. Margin reduce-only replacements report their total the same way. `updatedFields.price` is the replacement's price: the requested price, or the original's when omitted.

An entry order's **pending** take-profit and stop-loss legs move onto the replacement, and the replacement is judged exactly as a fresh order carrying those legs: each fixed TP or SL trigger must still sit on the correct side of the replacement's expected entry (for a buy, the lower of its limit price and the live mark; for a sell, the higher). An edit that would move the entry past its own stop-loss or take-profit is refused with `INVALID_ORDER` (REST `400` / gRPC `INVALID_ARGUMENT`; a per-item error in `batchReplace`) and the original keeps resting with its legs. Legs that are already active on the position are not re-checked, and a trailing stop has no fixed trigger to compare. The moved legs then settle on the replacement's own placement: if the replacement fills on arrival, a fixed TP/SL arms on any fill and a trailing stop on a full fill; legs stay pending while a remainder rests, and are cancelled if the replacement can never fill further.

For delegated agents, replacement is checked against both `REPLACE_ORDER` permission and the resulting order limits, including trading pair or margin account scope, order type, time in force, leverage, and notional. `maxOpenOrders` is unsupported and is rejected when configuring a delegation.

<Note>
  A single `replaceOrder` validates the replacement (including a `postOnly` crossing check) before cancelling the original. A rejected replacement — including a post-only crossing rejection — leaves the original order resting, untouched. `batchReplace` is cancel-first and so differs for most failures, but it matches this behaviour for `postOnly` crossing specifically; see [Batch Replace](#batch-replace).
</Note>

### Batch Create

```typescript theme={null}
const result = await sdk.trading.batchCreate([
  {
    tradingPairId: pair.id,
    orderType: "LIMIT",
    side: "BUY",
    quantity: "1.0",
    price: "2000.00",
    timeInForce: "GTC",
  },
  {
    tradingPairId: pair.id,
    orderType: "LIMIT",
    side: "BUY",
    quantity: "0.5",
    price: "1950.00",
    timeInForce: "GTC",
  },
]);

console.log(`Created ${result.totalSucceeded} of ${result.totalRequested}`);
```

Each order is processed independently. A failure in one item does not block the rest of the batch.

Each item's failure — including a `postOnly` order that would cross the book — is reported as a structured `{ code, message }` on that item's result (`error.code === "POST_ONLY_WOULD_CROSS"` for a post-only crossing rejection); match on `code`, the same structured code the single-order endpoints now carry on their error envelope. Validation failures (a malformed trading pair, an unparseable price, and so on) are scoped to their own item too — the same per-item contract as [Batch Replace](#batch-replace), where a failed item's original order is left untouched.

A `MARKET` item must omit `timeInForce` — it is rejected on market orders, which take no time-in-force and always execute IOC-style inside the protective price band (matching the single-order endpoint).

A batch is capped at **100 items**. The SDK rejects a longer array client-side (`At most 100 orders per batch request`, the exported constant `MAX_BATCH_ORDER_ITEMS`) before sending, and the REST/gRPC APIs reject an oversized batch with HTTP `400` carrying the same message. Split larger sets across requests. The server-enumerated [Cancel All](#cancel-all) has a separate cap of **20,000 matching active orders**.

Each item also costs one order-creation item against your per-account [rate budget](/developers/rate-limits) — `reduceOnly` items included — so batching saves round trips, not budget. An over-budget batch is rejected as a whole with `429` before any item is processed, never item by item; a raw HTTP or gRPC client can read the `x-ratelimit-remaining-*` headers on each response to size the next batch, while the TypeScript SDK surfaces only `retryAfter` on the `429`.

### Batch Replace

```typescript theme={null}
const result = await sdk.trading.batchReplace([
  { orderId: "order_1", price: "2050.00" },
  { orderId: "order_2", price: "1975.00", quantity: "2.0" },
  { orderId: "order_3", price: "1925.00" },
]);

console.log(`Replaced ${result.totalSucceeded} of ${result.totalRequested}`);
```

Each successful replacement returns a new order ID. Batch replace is **cancel-first**: the matching engine cancels every item's original before placing any replacement, in request order, so a two-sided quote can shift across its own resting prices in one call without meeting self-trade prevention at all, and a replacement can lock funds freed by any original in the batch. That ordering matters more under the mode dispatch than it did under the old whole-order rejection: were the originals still resting, a replacement crossing one of them would **cancel** it under the default `CANCEL_MAKER`.

A replacement that succeeds is not necessarily *resting*: it can be placed and then immediately cancelled by self-trade prevention — under `CANCEL_TAKER` or `CANCEL_BOTH`, or as a `SKIP` remainder that would have rested crossing one of your surviving orders. The item reports success with its new order ID either way, so read the order's status rather than inferring it from the batch result. A replacement also inherits the original's time in force rather than taking one of its own; since only a resting order is eligible, that is always `GTC`/`GTD`, never `IOC`/`FOK`.

Per-order failure reporting applies to **placement-time** failures — the ones that reach the matching engine. An item whose replacement fails to **place** (insufficient balance or a risk rejection) reports the error as a structured `{ code, message }`, with its original **already cancelled**, so such a failed item leaves no resting order behind. Meeting one of your own orders outside the batch is no longer a placement failure: the item's replacement resolves it through [self-trade prevention](#self-trade-prevention) and places. The request carries no `timeInForce` input — the replacement inherits the original's — and only an already-resting original is eligible, so that value is always `GTC`/`GTD` and the `FOK` + `CANCEL_BOTH` rejection cannot arise here; it belongs to [Batch Create](#batch-create). An item whose original order cannot be found is reported the same per-item way (`error.code === "ORDER_NOT_FOUND"`), and does not roll back other replacements in the batch.

**`postOnly` crossing rejections are the exception, and do not cost you the original.** A `postOnly` replacement that would cross is refused *before* the cancel pass runs, so its original stays resting exactly as it was — same order ID, same queue position. The item still reports `error.code === "POST_ONLY_WOULD_CROSS"`, same shape as batch create; the difference is only what happens to the original. The check is evaluated against the book as it will stand once the batch's own cancels are applied, so a two-sided ladder shifting across its own resting prices is unaffected.

Four narrow cases remain, because none of them can be seen before the cancel pass runs. In the first three the item reports `POST_ONLY_WOULD_CROSS` with its original cancelled, exactly as before:

* The item would cross a replacement that an **earlier item in the same batch** just rested. This only arises for an internally crossed target ladder — one whose own new bids sit at or above its own new asks — which is a malformed request rather than the "market moved into my quote" case.
* Another item's original **failed to cancel** and so stayed on the book, and your replacement crosses *that* order. The pre-cancel check assumed it would be gone.
* You listed the **same original twice** with different replacements. Each occurrence is judged on its own price, so a refused occurrence can report `POST_ONLY_WOULD_CROSS` while a surviving sibling occurrence still cancels the shared original (first occurrence wins, as always for duplicates).

The fourth goes the other way — the item is refused but nothing of yours is cancelled:

* An **earlier item in the same batch would have taken the liquidity** your post-only replacement is judged against (a non-post-only replacement that crosses). The check reads the touch as it stands before any placement, so it refuses a quote that would in fact have rested. Your original stays where it is; the requote simply does not happen, and retrying after the batch succeeds.

Treat the guarantee below as "a refused post-only item normally keeps its original", and confirm from your open orders rather than assuming.

Pre-sequencing validation failures are **scoped to their item**, the same contract as [Batch Create](#batch-create): a malformed `orderId` or numeric field, an item's `postOnly` shape violation (a `MARKET` order, or `postOnly` combined with `IOC`/`FOK`), a malformed `clientOrderId`, or a delegated-session item without a persisted original each fail alone — that item's result carries a structured `{ code, message }`, using the same code vocabulary as batch create (`INVALID_ORDER_ID`, `INVALID_PRICE`, `INVALID_QUANTITY`, `INVALID_CLIENT_ORDER_ID`, `INVALID_POST_ONLY`, `INVALID_SELF_TRADE_PREVENTION_MODE`; `FORBIDDEN` for a delegated item without a persisted original) — and its original order is **never touched**, while the batch's valid items proceed. This per-item contract is the **raw REST/gRPC** shape. The TypeScript SDK validates the whole array against `BatchReplaceOrdersSchema` first, so a field its schema covers — `selfTradePreventionMode` among them — throws `ValidationError` before the request is sent and no sibling item executes at all. A batch whose items *all* fail validation returns `2xx` with every result failed and no engine round trip. Only an empty or oversize (>100 items) batch still rejects the entire call with a top-level REST `400` / gRPC `InvalidArgument`. Earlier releases rejected the whole call for any one malformed item; if you relied on `400` meaning "nothing happened", read `results[]` per item instead — a `2xx` batch can contain both executed replacements and failed items.

Because a failed-to-place item's original is already cancelled, refresh your live open orders before retrying — do not assume the original is still resting, or you may re-expose it unintentionally. A `POST_ONLY_WOULD_CROSS` rejection normally leaves the original resting, so it can be retried by re-pricing rather than re-placing — but the cases above are exceptions, so confirm against your open orders rather than relying on the code alone. For every other per-item failure code, treat the original as gone.

A batch replace is capped at **100 items**, the same client-side and API `400` cap as [Batch Create](#batch-create); split larger sets across requests.

## Response Shapes

Order placement returns a standard response with optional immediate execution details.

```typescript theme={null}
interface CreateOrderResponse {
  orderId: string;
  status: "SUCCESS" | "FAILED";
  message: string;
  matchResult?: {
    tradesCount: number;
    totalFilled: string;
    remainingQuantity: string;
    averageFillPrice: string | null;
    status: string;
    actualSlippageBps: number | null;
    maxSlippageBps: number | null;
    executionPriceRange: {
      bestPrice: string;
      worstPrice: string;
    } | null;
  };
}
```

Market orders and aggressive limit orders can return `matchResult` immediately. Resting orders may be accepted with no immediate fills.

```typescript theme={null}
interface ReplaceOrderResponse {
  orderId: string;           // new order ID
  originalOrderId?: string; // replaced order ID
  status: "SUCCESS" | "FAILED";
  updatedFields: {
    price?: string;    // replacement's price (requested, or the original's)
    quantity?: string; // replacement's TOTAL, not its resting size
  };
  matchResult?: CreateOrderResponse["matchResult"];
}
```

## Error Handling

```typescript theme={null}
import { APIError } from "@0xmonaco/core";

try {
  await sdk.trading.replaceOrder(orderId, {
    price: "2020.00",
    quantity: "1.5",
  });
} catch (error) {
  if (error instanceof APIError) {
    if (error.message.includes("not found")) {
      console.error("Order does not exist or already filled/cancelled");
    } else if (error.message.includes("Cannot replace")) {
      console.error("Order status does not allow replacement");
    } else {
      console.error(error.message);
    }
  }
}
```

Common cases to handle:

* Delegated-agent policy is revoked, expired, inactive, missing, or narrower than the requested action.
* Order already filled, canceled, expired, or rejected.
* Replacement total quantity is at or below the already-filled quantity — nothing would be left to rest. This applies to margin reduce-only orders too.
* A margin reduce-only replacement whose remainder (`total - filled`) exceeds the live position.
* Price or quantity is not a positive decimal string.
* Entry-attached `takeProfit`/`stopLoss` on a spot or reduce-only order is rejected — they are margin entry-order only.
* A normal margin order is missing `leverage`, or `positionSide` disagrees with `side`.
* A market order fills partially inside the market's price band (1,000 bps by default) and cancels the remainder, or is rejected as insufficient liquidity or market unavailable.
* A perp order that takes liquidity (market or limit) stops at the first resting order outside the perp price band; a limit remainder that would then cross the book is cancelled instead of resting — see [Perp price band](/trading-mechanics/perps#perp-price-band).
* A perp limit order (any time in force, post-only included) or replacement priced past the perp price band on its aggressive side — a buy above the band's upper edge, a sell below its lower edge — is rejected with REST `400` / gRPC `InvalidArgument` code `INVALID_ORDER` (`limit price is outside the price band`). A resting perp order the mark later moves past the band is cancelled with `terminalReason` `PRICE_BAND`.
* Perps risk simulation rejects the requested leverage or collateral impact.
* A `price` above 100,000,000 per unit (and a TP/SL `triggerPrice` or trailing-stop `activationPrice` above it) is rejected with `400` / `INVALID_ARGUMENT` (`Value must not exceed 100000000`). A margin order on a market whose quote asset has more than 6 decimals is refused, as is a `riskBucketCollateral` with more than 6 decimals (`riskBucketCollateral exceeds supported precision for USDC (6 decimals)`).
* Reduce-only order would increase or flip exposure.
* A `postOnly` order or replacement would cross the book — rejected with REST `400` / gRPC `InvalidArgument` carrying the structured code `POST_ONLY_WOULD_CROSS` (REST error-envelope `code`, gRPC `ErrorInfo.reason`) — adjust price and resubmit; see [Post-Only Orders](#post-only-orders) for the crossing predicate and the replace/batch-replace resting-order contract.
* `postOnly` combined with a `MARKET` order or `IOC`/`FOK` time in force is rejected.
* A `CROSS` order that is not reduce-only on an isolated-only market (`isolatedOnly: true`): `400` / gRPC `InvalidArgument` with code `CROSS_MARGIN_NOT_ALLOWED`. Use `marginMode: "ISOLATED"`; reduce-only `CROSS` orders still pass.
* An order that is not reduce-only while the margin account holds the pair in its **other** margin mode (an open position, a resting order that is not reduce-only, or a live TWAP parent that is not reduce-only, in the cross bucket when you order isolated, or in the pair's isolated bucket when you order cross): `400` / gRPC `InvalidArgument` with code `MARGIN_MODE_CONFLICT`; the message names the holding risk bucket and its mode. Close or cancel that side first, or send a reduce-only order, which always passes. A margin TWAP create is refused the same way, including while a live TWAP parent of the account that is not reduce-only holds the pair in the other mode, and risk previews report it as `rejectCode: "MARGIN_MODE_CONFLICT"`. A collateral transfer is refused with the same `400` / `MARGIN_MODE_CONFLICT` only when it would create a second live isolated risk bucket for the pair. Idle collateral is not exposure: moving collateral into the risk bucket of the mode that is not holding the pair is allowed, but it cannot open exposure there while the other mode holds the pair. An account that held the pair in both modes before the rule can cancel a resting order on either side, but a replace that is not reduce-only on the side that is not holding the pair is refused like a new order; reduce-only replaces pass. See [One margin mode per market](/trading-mechanics/perps#one-margin-mode-per-market).
* A batch create, replace, or cancel exceeds 100 items — rejected client-side and with API `400` (`At most 100 orders per batch request`).
* Cancel-all matches more than 20,000 active orders — REST `400` / gRPC `InvalidArgument`, with nothing cancelled. Narrow by trading pair or cancel explicit batches.
* The sequencer is overloaded, or the account already has its share of the engine's queue waiting — order create, replace, cancel, and batch submits can return a retryable HTTP `503` with error code `OVERLOADED` instead of queueing (cancels keep a reserved share of the engine's capacity, so they are refused last); back off and retry (see [`retryable`](/sdk/typescript/error-handling)).
* The account's order rate budget is spent — REST `429` / gRPC `ResourceExhausted` with code `RATE_LIMIT_EXCEEDED`, a `Retry-After` header, and `details.retryAfter` in seconds. The request — a whole batch included — was rejected before admission and nothing was applied, so wait out the interval and resubmit; cancels draw a separate, larger budget and cancel-all costs one item per call. Authenticated reads such as `getOrder` and `getPaginatedOrders` draw their own per-account read budget with the same `429` contract and no budget headers, so reconcile on the `orders` WebSocket stream rather than polling. See [Rate Limits](/developers/rate-limits).
* Conditional order has already triggered, expired, or been cancelled.

When an order ends in `CANCELLED`, its `terminalReason` gives the stable cause — `USER_REQUESTED`, `REPLACED`, `LIQUIDATION`, `INSUFFICIENT_MARGIN`, `REDUCE_ONLY_EXHAUSTED`, `POSITION_CLOSED`, `OCO_SIBLING_TRIGGERED`, `SELF_TRADE_PREVENTION`, `AUTO_DELEVERAGING`, `PRICE_BAND`, `SLIPPAGE_TOLERANCE`, or `SYSTEM` — on both the REST order response and the order WebSocket event. `AUTO_DELEVERAGING` means auto-deleveraging closed the position your risk bucket held and this resting order was cancelled with it; you were deleveraged as a counterparty, not liquidated, which is why it is not reported as `LIQUIDATION`. `PRICE_BAND` means the exchange's protective price band stopped your order and its remainder was cancelled, or, on a resting perp order, that the mark moved the order past the band, where no taker may trade with it; `SLIPPAGE_TOLERANCE` means your own `slippageToleranceBps` did, so widening it can fill more, while the exchange band cannot be widened.

### Order state versions

Every order response carries `version`, an **opaque, compare-only** revision counter for that order's state: the sequencer step that wrote it. A strictly higher value is strictly newer state for that order. `getOrder` and `getPaginatedOrders` return the same `version` for the same order state, and it never advances without a state change.

The **same counter rides every order WebSocket event and the `orders` snapshot frame**, which is what makes a reconnect reconcilable without pausing the stream: keep applying events as they arrive, and merge a REST snapshot per order, taking the snapshot when `snapshot.version >= local.version`. See the [market-maker runbook](/developers/market-maker-runbook#4-reconnect-and-resync) for the full recipe.

<Warning>
  Equal versions mean different things on the two surfaces, and the difference decides whether you write `>` or `>=`.

  Between **two REST reads**, equal means identical state — a REST row is only ever a step's final state.

  On the **stream**, equal means only *same step*. One step emits several events for one order (an `OrderPlaced` and then its fills), and an unkeyed batch place or a batch replace shares one step across every item. A create batch containing any `idempotencyKey` processes items individually, so its items need not share a version. So when a snapshot row ties with local state, the snapshot is the more complete view and must win: merge with `>=`, never `>`.
</Warning>

Read nothing else into the number:

* **Absent means unknown**, not zero — an order last written before the field existed reports no `version`. Compare fields for those rows instead of assuming they are the oldest.
* **Gaps are normal.** It comes from one global sequence shared by every order, so an order's successive versions are sparse and consecutive values are never implied.
* **Not comparable across orders.** Two different orders' versions say nothing about each other, only about each order against its own earlier reads.

Prefer it over `updatedAt` for reconciliation. `updatedAt` is not a single clock: order detail reports the matching-engine event time while order lists report the database commit time, so the two endpoints report different values for the same order, the commit clock can move without a state change, and timestamp precision varies per call.

Selected persisted/read-model timestamps render at **fixed microsecond width with a `Z` suffix** — `2026-09-05T01:23:45.123456Z`. This covers ordinary-order `createdAt`, `updatedAt`, and `expirationDate`; every conditional-order timestamp; application order-view `createdAt` / `updatedAt`; and persisted TWAP window and lifecycle stamps. Ordinary-order lifecycle stamps (`cancelledAt`, `filledAt`, `expiredAt`, `submittedAt`, and `acknowledgedAt`) retain variable fractional width. `CreateTwapOrderResponse.firstSliceAt` and `CreateTwapOrderResponse.endTime` also retain their separate rendering. Fixed width does not make `updatedAt` comparable across endpoints; use `version` for that.

## Order Patterns

### Try FOK, fallback to IOC

```typescript theme={null}
let order = await sdk.trading.placeLimitOrder(
  pair.id,
  "BUY",
  "10.0",
  "2000.00",
  { timeInForce: "FOK" }
);

if (order.matchResult?.status === "REJECTED") {
  order = await sdk.trading.placeLimitOrder(
    pair.id,
    "BUY",
    "10.0",
    "2000.00",
    { timeInForce: "IOC" }
  );
}
```

### Cancel stale intent before quoting

```typescript theme={null}
await sdk.trading.batchCancelAll(pair.id);

await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.0", "1995.00");
await sdk.trading.placeLimitOrder(pair.id, "SELL", "1.0", "2005.00");
```

### Quick exit with IOC

```typescript theme={null}
const quickExit = await sdk.trading.placeLimitOrder(
  pair.id,
  "SELL",
  "1.5",
  "1950.00",
  { timeInForce: "IOC" }
);

if (quickExit.matchResult && Number(quickExit.matchResult.remainingQuantity) > 0) {
  await sdk.trading.placeMarketOrder(
    pair.id,
    "SELL",
    quickExit.matchResult.remainingQuantity
  );
}
```

## Useful Surfaces

<CardGroup cols={2}>
  <Card title="TypeScript Trades" icon="code" href="/sdk/typescript/trades">
    SDK methods for placing, replacing, cancelling, batching, and listing orders.
  </Card>

  <Card title="Orders API" icon="list-checks" href="/api-reference/orders/create-new-order">
    REST endpoints for creating, replacing, canceling, and querying orders.
  </Card>

  <Card title="Orderbook API" icon="layers" href="/api-reference/orderbook/get-orderbook-snapshot">
    Snapshot orderbook depth for a trading pair.
  </Card>

  <Card title="WebSocket API" icon="radio" href="/reference/websockets">
    Live order, orderbook, trade, balance, movement, and position streams.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.