> Treat every fee rate, margin ratio, order limit, minimum and address in this documentation as an illustration of the shape of the value, never as a constant to hardcode - the live values are served by GET /markets and GET /config on the Perps API.

# Orders

> Submit, cancel, and modify orders — including post-only, trigger (TP/SL), bracket orders, and batching.

All order actions are signed by your **trading key** — see [Authentication & Signing](/developers/perps-api/authentication) for the domain and signing rules. `account_id` and `wallet` are as defined in [Account format](/developers/perps-api/overview#account-format).

## Submit orders

```
POST /orders
```

The endpoint is a **batch**: 1–20 independently signed legs per request, all for the same `account_id`.

```json
{
  "account_id": "0x…",
  "orders": [
    {
      "market": "BTC",
      "side": "BUY",
      "order_type": "MARKET",
      "size": "0.001",
      "limit_price": "65129",
      "time_in_force": "IOC",
      "reduce_only": false,
      "leverage": 10,
      "client_order_id": "my-order-1",
      "nonce": "1787067314951",
      "signature": "0x…"
    }
  ]
}
```

| Field                                   | Required         | Signed | Notes                                                                                          |
| --------------------------------------- | ---------------- | ------ | ---------------------------------------------------------------------------------------------- |
| `market`, `side`, `size`, `limit_price` | yes              | yes    | `side` is `BUY` or `SELL`                                                                      |
| `order_type`                            | no               | yes    | Defaults to `MARKET` — but sign `orderType: "MARKET"` anyway                                   |
| `time_in_force`                         | `MARKET`/`LIMIT` | yes    | `GTC`, `IOC`, or `ALO` (post-only). Omit on `TRIGGER`; still sign `IOC`                        |
| `reduce_only`                           | no               | yes    | Defaults to `false`                                                                            |
| `leverage`                              | no               | yes    | Integer. Not range-checked at validation                                                       |
| `client_order_id`                       | **yes**          | **no** | Not a member of `Order`. Omitting it is a `400`                                                |
| `trigger`                               | `TRIGGER` only   | yes    | Plain orders still sign `triggerPrice: "0"`, `isMarket: false`, `tpsl: ""`                     |
| `parent_client_order_id`                | no               | **no** | Attaches a TP/SL leg to an entry leg in the same batch — see [Bracket orders](#bracket-orders) |
| `nonce`                                 | yes              | yes    | Decimal string in the body, `uint256` when signed                                              |

### Order types

| `order_type` | Behavior                                                                                                                     |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `MARKET`     | Takes liquidity immediately. Always IOC. The default when `order_type` is omitted                                            |
| `LIMIT`      | Rests on the book at `limit_price`, `time_in_force: "GTC"` — or `IOC` to take-or-cancel, or `ALO` to [post only](#post-only) |
| `TRIGGER`    | A TP/SL order, armed until its `trigger` fires                                                                               |

The schema also carries `LIQUIDATION` and `ORPHAN_LIQUIDATION`. They are server-generated, appear on read routes only, and must never be submitted — accept them when parsing.

What `limit_price` means changes with the type:

* On a **`MARKET`** order it is your slippage bound, which you compute yourself as mark × (1 ± tolerance). You can never be filled beyond it; whatever cannot fill within the bound is cancelled.
* On a **`LIMIT`** order it is the resting price.
* On a **`TRIGGER`** order it is the price the order fires at — not the price that arms it. That is `trigger.trigger_price`.

`TRIGGER` orders omit `time_in_force` entirely; the server derives it from `trigger.is_market`.

### Post-only

Post-only is not a separate flag: it is the `time_in_force` value `ALO` (add liquidity only). An order carries exactly one `time_in_force` — `GTC`, `IOC`, or `ALO` — so post-only cannot be combined with another time in force. A post-only order rests until filled or cancelled, like `GTC`, but it never takes: it rests or it is rejected.

```json
{
  "account_id": "0x…",
  "orders": [
    {
      "market": "BTC", "side": "BUY", "order_type": "LIMIT",
      "size": "0.001", "limit_price": "59000", "time_in_force": "ALO",
      "leverage": 10, "client_order_id": "maker-1",
      "nonce": "1787067314950", "signature": "0x…"
    }
  ]
}
```

The leg signs `timeInForce: "ALO"`.

* `LIMIT` only. A `MARKET` leg with `ALO` is rejected with `market_order_must_be_ioc`. A `TRIGGER` leg's time in force is always derived from `trigger.is_market`, so a trigger cannot be post-only.
* The crossing check happens at the venue, not at validation. A post-only order whose price would match the book passes `POST /orders` as `SUBMITTED` and then reads `REJECTED` with `reason: post_only_would_take` on `GET /orders/{orderId}`. Keep a buy below the best ask and a sell above the best bid.
* A post-only fill is always a maker fill, so it is charged the maker rate.
* A [modify](#modify-an-order) inherits `ALO`, so the new order can be rejected with `post_only_would_take` in the same way.

### Trigger orders

```json
{
  "market": "BTC",
  "side": "SELL",
  "order_type": "TRIGGER",
  "size": "0.001",
  "limit_price": "60000",
  "trigger": { "trigger_price": "60500", "is_market": true, "tpsl": "SL" },
  "reduce_only": true,
  "client_order_id": "my-sl-1",
  "nonce": "1787067314952",
  "signature": "0x…"
}
```

* `trigger_price` — the raw mark-price threshold that arms the fire (no slippage applied).
* `is_market` — `true` fires an IOC order at `limit_price`. **Only `is_market: true` is supported today**; trigger-limit (`false`) is rejected.
* `tpsl` — `TP` or `SL`. `SL` fires when the mark moves against the position, `TP` when it moves in favour. A trigger already past the mark on its fire side is rejected.
* `size: "0"` on a reduce-only trigger means *close the full position* at fire time.

### Bracket orders

A bracket is an opening `MARKET` or `LIMIT` entry with a whole-position take-profit and/or stop-loss attached in the **same batch**. The TP/SL legs are accepted with the entry, wait while it works, and arm themselves once it fills — no polling for the fill, no window where the position is unprotected.

Attach a leg by naming the entry's `client_order_id` in `parent_client_order_id`:

```json
{
  "account_id": "0x…",
  "orders": [
    {
      "market": "BTC", "side": "BUY", "order_type": "LIMIT",
      "size": "0.001", "limit_price": "60000", "time_in_force": "GTC",
      "leverage": 10, "client_order_id": "entry-1",
      "nonce": "1787067314951", "signature": "0x…"
    },
    {
      "market": "BTC", "side": "SELL", "order_type": "TRIGGER",
      "size": "0", "limit_price": "62500",
      "trigger": { "trigger_price": "63000", "is_market": true, "tpsl": "TP" },
      "reduce_only": true, "parent_client_order_id": "entry-1",
      "client_order_id": "tp-1", "nonce": "1787067314952", "signature": "0x…"
    },
    {
      "market": "BTC", "side": "SELL", "order_type": "TRIGGER",
      "size": "0", "limit_price": "57500",
      "trigger": { "trigger_price": "58000", "is_market": true, "tpsl": "SL" },
      "reduce_only": true, "parent_client_order_id": "entry-1",
      "client_order_id": "sl-1", "nonce": "1787067314953", "signature": "0x…"
    }
  ]
}
```

Shape rules, checked before anything is queued:

| Rule                                                                                                                                                                    | Reject                                           |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `parent_client_order_id` names an entry leg of the same batch; every `client_order_id` in the batch is unique                                                           | `400 INVALID_REQUEST`                            |
| An attached leg is a reduce-only `TRIGGER` with `size: "0"`                                                                                                             | `400 INVALID_REQUEST`                            |
| The entry is an opening (not reduce-only) `MARKET` or `LIMIT`                                                                                                           | `bracket_parent_invalid`                         |
| At most one `TP` and one `SL`, same market, opposite `side` to the entry                                                                                                | `bracket_child_invalid`                          |
| TP above the entry price for a long and below for a short; SL the other way. The reference is `limit_price` on a `LIMIT` entry and the current mark on a `MARKET` entry | `bracket_tp_wrong_side`, `bracket_sl_wrong_side` |

* On a `MARKET` entry, check TP/SL against the **mark at submit**, not the entry's `limit_price` — that is only your slippage bound. A TP placed just above a long's slippage bound but at or below the mark is rejected with `bracket_tp_wrong_side`.
* `parent_client_order_id` is **not signed** — each leg still signs its own plain `Order`.
* The group is decided together: if any member fails validation, every member of the group comes back `REJECTED` with the same `reason`. Other legs in the batch are unaffected.
* Attached legs count toward `max_open_orders` from the moment they are accepted.

Lifecycle of an attached leg:

| Entry outcome                                                                | Attached TP/SL                                                                                                                                                  |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Working                                                                      | `PENDING_PARENT` — not armed, cannot fire, does not block a close. Listed by `/account/open-orders` with `parent_order_id` set, not yet by `/position-triggers` |
| Any fill, however the entry ended (`FILLED` or `PARTIALLY_FILLED_CANCELLED`) | Arms as a normal whole-position trigger once the fill has settled into a position — it reads `SUBMITTED`, and `/account/position-triggers` lists it             |
| Cancelled or rejected with no fill                                           | `CANCELLED`, `reason: bracket_parent_not_filled`                                                                                                                |
| Filled but no position on the entry's side by settlement                     | `CANCELLED`, `reason: bracket_no_position`                                                                                                                      |
| Entry never persisted                                                        | `CANCELLED`, `reason: bracket_parent_missing`                                                                                                                   |

When a bracket leg arms, it **replaces** any armed whole-position trigger of the same `tpsl` kind on that market — the older one is cancelled with `reason: bracket_replaced`, so a position never carries two full-size stops.

While it is `PENDING_PARENT` the leg can be cancelled (`order_type: "TRIGGER"`) but not modified (`modify_not_modifiable`). Modifying the entry re-checks the pending legs against the new `limit_price` and rejects the modify with `bracket_tp_wrong_side` / `bracket_sl_wrong_side` if a level would end up on the wrong side; otherwise the legs follow the modify to the new `order_id`.

### Price and size rules

| Rule                                                                                      | Source                       |
| ----------------------------------------------------------------------------------------- | ---------------------------- |
| `limit_price` is an exact multiple of `tick_size`                                         | `tick_size`                  |
| Price has at most `px_decimals` places; size at most `sz_decimals`                        | `px_decimals`, `sz_decimals` |
| `size × price ≥ min_order_value`, checked **after** rounding. Reduce-only legs are exempt | `min_order_value`            |

`tick_size` binds harder than `px_decimals`: BTC allows one decimal place but its tick is `1`, so `60000.5` is rejected with `price_off_tick`.

### Signing an order

Each leg signs `Order(bytes32 account, …)` — the struct is on [Authentication & Signing](/developers/perps-api/authentication#structs). Every signed field must byte-match the body; a mismatch is a `403`, not a validation error.

### Response

`data` is an array **index-aligned with your legs**; top-level `success` is `true` only if every leg was `SUBMITTED`:

```json
{
  "success": true,
  "data": [
    { "order_id": "…", "client_order_id": "my-order-1", "status": "SUBMITTED" }
  ],
  "timestamp": 1787067314999
}
```

Legs are independent — one `REJECTED` (with `reason`) never cancels the others. The one exception is a [bracket](#bracket-orders): an entry and its attached TP/SL are accepted or rejected as a group. `SUBMITTED` means validated and queued, not filled; the order then advances **asynchronously**.

### Rejections

| Stage          | Shape                                      | Scope         |
| -------------- | ------------------------------------------ | ------------- |
| Validation     | `400`, `error.code: INVALID_REQUEST`       | Whole request |
| Signature      | `403`, `FORBIDDEN`                         | Whole request |
| Business rules | `200`, leg `status: "REJECTED"` + `reason` | That leg only |

| `reason`                                                                                            | Cause                                                                                                                                                                 |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `price_off_tick`                                                                                    | `limit_price` is not a multiple of `tick_size`                                                                                                                        |
| `close_trigger_coexistence`                                                                         | Reduce-only order on a position that still has an armed reduce-only `TRIGGER`. Cancel the trigger first — see [Exit](/developers/perps-api/position-lifecycle#6-exit) |
| `market_order_must_be_ioc`                                                                          | A `MARKET` leg with `time_in_force` other than `IOC` — including `ALO`                                                                                                |
| `post_only_would_take`                                                                              | Asynchronous: a post-only order that would have crossed the book. Read it from `GET /orders/{orderId}`, not the submit response                                       |
| `bracket_parent_invalid`, `bracket_child_invalid`, `bracket_tp_wrong_side`, `bracket_sl_wrong_side` | Bracket shape or level rules — see [Bracket orders](#bracket-orders). Returned on every leg of the group                                                              |

The list is not exhaustive.

## Order lifecycle

```
GET /orders/{orderId}
```

The happy path is `SUBMITTED` → `ACCEPTED` → `EXECUTING` → `FILLED`.

| Status           | Meaning                                                                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `SUBMITTED`      | Validated and queued. A TRIGGER order **stays here while armed** — a long-lived `SUBMITTED` trigger is normal, not stuck                |
| `ACCEPTED`       | Live at the venue. A trigger reaches this only once it fires                                                                            |
| `EXECUTING`      | Filling                                                                                                                                 |
| `PENDING_MODIFY` | The **new** order a modify created, held until the order it replaces terminates                                                         |
| `PENDING_PARENT` | A TP/SL attached to an entry that has not filled yet. Arms (→ `SUBMITTED`) once the entry fills — see [Bracket orders](#bracket-orders) |

Five statuses are terminal:

| Terminal status              | Meaning                                                                                                                                                              |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FILLED`                     | Fully executed                                                                                                                                                       |
| `PARTIALLY_FILLED_CANCELLED` | Partly filled, remainder cancelled                                                                                                                                   |
| `CANCELLED`                  | Cancelled before filling. `reason` says why when the system did it: `bracket_parent_not_filled`, `bracket_no_position`, `bracket_parent_missing`, `bracket_replaced` |
| `REJECTED`                   | Turned down — see `reason`                                                                                                                                           |
| `FAILED`                     | Could not be routed to the venue                                                                                                                                     |

> **Note**
>
> `GET /orders/{orderId}` can briefly return `404` right after submit — orders persist asynchronously. Retry with backoff. Statuses here are uppercase; list reads use a [different vocabulary](/developers/perps-api/account#status-vocabularies).

## Cancel an order

```
POST /orders/{orderId}/cancel
```

Signed by your trading key as:

```
Cancel(address account, string orderId, string orderType, uint256 nonce)
```

Note that `account` here is your **`wallet`**, not the `account_id`:

```json
{
  "account": "0xYourWallet",
  "order_type": "LIMIT",
  "nonce": "1787067314953",
  "signature": "0x…"
}
```

`order_type` is `LIMIT` (resting order) or `TRIGGER` (armed trigger, or a TP/SL still waiting for its entry); it is part of the signed payload, as is `orderId` — taken from the URL path, not the body. Returns `status: SUBMITTED | REJECTED`; `403` when the signature does not authorize the account. Cancellation is asynchronous — confirm with `open-orders` or `position-triggers`. Cancelling an entry that still has attached TP/SL cancels them too, with `reason: bracket_parent_not_filled`, unless the entry had already partly filled — then they arm against the position that fill opened.

## Cancel in batch

```
POST /orders/cancel-batch
```

```json
{
  "account": "0xYourWallet",
  "cancels": [
    { "order_id": "…", "order_type": "LIMIT", "nonce": "1787067314954", "signature": "0x…" }
  ]
}
```

1–20 legs, each signing its own `Cancel`. Per-leg status is `SUBMITTED` or `FAILED` (with `reason`) — `FAILED`, not `REJECTED`. A bad signature fails only its leg, as `200` with top-level `success: false`.

## Modify an order

```
POST /orders/{orderId}/modify
```

Signed by your trading key as:

```
Modify(bytes32 account, string orderId, string clientOrderId,
       string limitPrice, string size, string triggerPrice, uint256 nonce)
```

```json
{
  "account_id": "0x…",
  "client_order_id": "my-order-1b",
  "limit_price": "64900",
  "size": "0.002",
  "trigger_price": "0",
  "nonce": "1787067314955",
  "signature": "0x…"
}
```

* Every field is required on every call; there is no partial modify.
* `client_order_id` must be **new** — reusing the modified order's id is rejected. It is signed here, unlike on an order leg.
* Only price, size, and trigger price can change. Market, side, time in force (including `ALO`), reduce-only, leverage, and order type are all inherited from the original order.
* For a limit order send `trigger_price: "0"`; for a trigger order `limit_price` is the fire price.
* `size: "0"` (full-position close) is allowed only on TP/SL triggers.
* Success returns the **new** order's `order_id`. If a trigger modify races the trigger firing (or an open-order-cap check at apply time), the modify is dropped and the original order is untouched.
* A TP/SL in `PENDING_PARENT` cannot be modified (`modify_not_modifiable`). Modifying its entry re-validates the attached levels against the new `limit_price` and moves them to the new `order_id` — see [Bracket orders](#bracket-orders).

**Next:** [Position Lifecycle](/developers/perps-api/position-lifecycle)