> For the complete documentation index, see [llms.txt](https://docs.reya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.reya.xyz/developers/devnet/api-reference/ws-exec-api-reference.md).

# WebSocket Order Entry API Reference

{% hint style="info" %}
**Download the current devnet1 contract:** [Order Entry AsyncAPI spec](https://api-devnet.reya-cronos.network/v2/asyncapi-exec-spec.yaml). This endpoint serves the deployed API contract and may advance beyond the version described in this guide. For the exact API **3.5.2** contract, use the immutable [canonical spec source](https://github.com/Reya-Labs/reya-api-specs/tree/4fafae7d19076e8d31811930000a78d842758d9f).
{% endhint %}

## Overview

The Reya DEX WebSocket Order Entry API v2 is a request/response surface for placing, modifying, and cancelling orders — and managing an account-scoped cancel-on-disconnect switch — over a persistent WebSocket connection. It carries the same operations and payload bodies as the REST `/v2` endpoints (`POST /v2/createOrder`, `POST /v2/modifyOrder`, `POST /v2/cancelOrder`, `POST /v2/cancelAll`, `POST /v2/cancelAllAfter`), with lower per-operation overhead and id-correlated responses on the same channel.

Payload bodies are reused verbatim from REST. Request and response envelopes are id-correlated; the server replies on the same connection with a frame carrying the same `id` the client sent.

This surface is **order-entry only**. For real-time market data, position updates, and fill streaming, see the [WebSocket Info API Reference](/developers/devnet/api-reference/websocket-api-reference.md). The recommended integration runs both connections in parallel: this surface for order entry, the streaming surface for market and account updates.

## Server Endpoints

Perpetual order-book trading is currently available only on **devnet1**. Other environments will be added here as the order book becomes available on them.

### Devnet1 (perpOB testnet)

* **URL**: `wss://ws-exec-devnet.reya-cronos.network`
* **Protocol**: WSS
* **Description**: The environment to integrate against today; currently the only environment with perpetual order-book support.

## Connection & Auth Model

The connection itself is **anonymous** — no handshake, no login, no API key. There is no concept of a session-bound wallet identity.

Authentication is **per-frame**: every order-bearing request carries an EIP-712 signature in its `payload` (`signature`, `nonce`, and `deadline` — the **required** signature-validity timestamp; **GTT** orders additionally carry an `expiresAfter` lifetime). `createOrder`, `modifyOrder`, and `cancelAllAfter` also carry `signerWallet`; `cancelOrder` recovers the signer from the signature. The server validates the signature against the order contents on every request — identical to the REST `/v2/createOrder` etc. body shape. See [Signatures and Nonces](/developers/devnet/authentication/signatures-and-nonces.md) for the signing model; both transports use the same scheme and the same Python SDK helpers.

A consequence of per-frame authentication is that a single WebSocket connection can carry orders signed by **multiple different `signerWallet` values** — useful for integrators operating multiple subaccounts on one connection.

Choosing WebSocket instead of REST does not change order semantics. `cancelAllAfter` is signed and nonce/deadline checked like other order-management actions, and does not count towards the current per-wallet order-entry rate limit.

## Message Structure

All WebSocket messages follow a standardized envelope structure with a `type` discriminator and a client-chosen `id` for correlation.

### Request Envelope (Client → Server)

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "payload": { /* operation-specific request body */ }
}
```

#### Components

* **type** (string, required): One of `createOrder`, `modifyOrder`, `cancelOrder`, `cancelAll`, `cancelAllAfter`, `ping`. (`pong` is server-only — see [Heartbeats](/developers/devnet/connectivity/heartbeats.md).)
* **id** (string, required): Client-chosen correlation identifier. Must be unique across in-flight requests on the connection — see [In-Flight `id` Uniqueness](#in-flight-id-uniqueness) below.
* **payload** (object, required for `createOrder` / `modifyOrder` / `cancelOrder` / `cancelAll` / `cancelAllAfter`): Operation-specific request body, byte-identical to the corresponding REST endpoint's request body.

### Response Envelope (Server → Client)

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "ok": true,
  "payload": { /* operation-specific success body */ }
}
```

or, on failure:

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "ok": false,
  "error": {
    "error": "INPUT_VALIDATION_ERROR",
    "message": "qty is missing"
  }
}
```

#### Components

* **type** (string, required): Echoes the request `type`.
* **id** (string, required): Echoes the request `id`.
* **ok** (boolean, required): `true` for success, `false` for failure.
* **payload** (object, required when `ok = true`, forbidden when `ok = false`): Operation-specific success body, byte-identical to the REST `200` response body.
* **error** (object, required when `ok = false`, forbidden when `ok = true`): See [Error Catalog](#error-catalog) for the shape and possible codes.

### Top-Level Error Envelope (Server → Client)

The server emits a top-level `error` envelope when it cannot parse a request at all (malformed JSON, unknown `type`, in-flight `id` collision, internal failure). Connection stays open. Operation-specific errors instead come back as `{ ok: false, error }` on the corresponding response envelope correlated by `id`.

```json
{
  "type": "error",
  "id": "req-7f3c1a",
  "error": {
    "error": "DUPLICATE_REQUEST_ID",
    "message": "Request id already in-flight on this connection"
  }
}
```

* **id** is present when the offending request carried one (e.g. for `DUPLICATE_REQUEST_ID`); absent for frame-level errors (e.g. unparseable JSON) where the server has no id to echo.

### Heartbeats

The heartbeat / connection-liveness mechanism is documented in detail on its own page — see [Heartbeats](/developers/devnet/connectivity/heartbeats.md). Short version: protocol-level pings handle liveness automatically, no application-level code is required on the client.

## Operations Reference

### `createOrder`

**Purpose**: Place spot or perp `LIMIT` orders (`IOC`, `GTC`, or `GTT`), or arm perp `STOP_LOSS` / `TAKE_PROFIT` trigger orders. Identical body and semantics to REST `POST /v2/createOrder`.

**Request Envelope** (spot LIMIT GTC):

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "payload": {
    "exchangeId": 1,
    "symbol": "WETHRUSD",
    "accountId": 10000000002,
    "isBuy": true,
    "limitPx": "1",
    "qty": "0.001",
    "orderType": "LIMIT",
    "timeInForce": "GTC",
    "signature": "0x...",
    "nonce": "1778601294274124",
    "signerWallet": "0x869d6494fe32B96F93F78F9c4B7aAf30eeC01C1F",
    "deadline": 1747927089,
    "clientOrderId": "1778601294274124"
  }
}
```

**Request Envelope** (perp IOC):

```json
{
  "type": "createOrder",
  "id": "req-7f3c1b",
  "payload": {
    "exchangeId": 1,
    "symbol": "ETHRUSDPERP",
    "accountId": 8017,
    "isBuy": true,
    "limitPx": "40000",
    "qty": "0.01",
    "orderType": "LIMIT",
    "timeInForce": "IOC",
    "reduceOnly": false,
    "signature": "0x...",
    "nonce": "...",
    "signerWallet": "0x869d6494fe32B96F93F78F9c4B7aAf30eeC01C1F",
    "deadline": 1747927089
  }
}
```

**Request Envelope** (perp GTT — good-til-time, post-only):

```json
{
  "type": "createOrder",
  "id": "req-7f3c1e",
  "payload": {
    "exchangeId": 1,
    "symbol": "ETHRUSDPERP",
    "accountId": 8017,
    "isBuy": true,
    "limitPx": "40000",
    "qty": "0.01",
    "orderType": "LIMIT",
    "timeInForce": "GTT",
    "postOnly": true,
    "signature": "0x...",
    "nonce": "...",
    "signerWallet": "0x869d6494fe32B96F93F78F9c4B7aAf30eeC01C1F",
    "deadline": 1747927089,
    "expiresAfter": 1747930689
  }
}
```

**Success Response** (rested without an immediate fill):

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "ok": true,
  "payload": {
    "status": "OPEN",
    "orderId": "1864998629727535104",
    "clientOrderId": "1778601294274124"
  }
}
```

**Success Response** (filled on entry):

```json
{
  "type": "createOrder",
  "id": "req-7f3c1b",
  "ok": true,
  "payload": {
    "status": "FILLED",
    "execQty": "0.01",
    "cumQty": "0.01",
    "orderId": "1864998629727535105",
    "firstFillId": "7205759403792794624",
    "fillCount": 1
  }
}
```

**Error Response**:

```json
{
  "type": "createOrder",
  "id": "req-7f3c1a",
  "ok": false,
  "error": {
    "error": "INPUT_VALIDATION_ERROR",
    "message": "limitPx is required"
  }
}
```

<details>

<summary><strong>Data Type — CreateOrderRequest payload</strong></summary>

* `exchangeId` (integer, required): Reya exchange identifier. Currently always `1` on devnet1.
* `symbol` (string): Trading symbol (e.g. `WETHRUSD`, `ETHRUSDPERP`).
* `accountId` (integer, required): Reya account ID placing the order.
* `isBuy` (boolean, required): `true` for a buy, `false` for a sell.
* `limitPx` (string, required): Limit price as a decimal string.
* `qty` (string): Order quantity as a decimal string. Required for `LIMIT` orders. **Omit** for `STOP_LOSS` / `TAKE_PROFIT` orders — a trigger has no quantity; it protects the whole live position at fire time (the signature carries the full-position sentinel — see [Trigger Orders (SL/TP)](/developers/devnet/order-entry/trigger-orders.md)).
* `orderType` (string, required): `LIMIT`, `STOP_LOSS` (stop-loss), or `TAKE_PROFIT` (take-profit). Triggers arm durably and fire against the mark price into the chosen TIF. See [Trigger Orders (SL/TP)](/developers/devnet/order-entry/trigger-orders.md).
* `timeInForce` (string, **required**): `IOC`, `GTC`, or `GTT`, on every create including triggers. For a trigger this chooses the fired child's behaviour. GTT additionally requires `expiresAfter`.
* `triggerPx` (string): Trigger price. Required for `STOP_LOSS` / `TAKE_PROFIT` orders.
* `reduceOnly` (boolean): Whether the order is reduce-only. **Perp IOC only** — **required** for perp IOC orders, and **not supported** for perp `GTC` / `GTT`, spot, or `STOP_LOSS` / `TAKE_PROFIT` orders. For unsupported request types, omit the REST/WS JSON field; the signer encodes the on-chain value as `false`.
* `postOnly` (boolean, optional): Maker-only intent — the order may only add liquidity, never take it. Valid on `GTC` / `GTT` (spot and perp) and rejected on `IOC`. A `STOP_LOSS` / `TAKE_PROFIT` create may omit it or send `false`; `true` is rejected.
* `signature` (string, required): EIP-712 signature over the order. See [Signatures and Nonces](/developers/devnet/authentication/signatures-and-nonces.md).
* `nonce` (string, required): Order nonce.
* `signerWallet` (string, required): Address that produced the signature.
* `deadline` (integer, **required**): Signature-validity timestamp (seconds since epoch). The order is rejected at entry if `deadline` has already passed (`ORDER_DEADLINE_PASSED_ERROR`). For **IOC** orders it must also be within **600 seconds** of now (`ORDER_DEADLINE_TOO_HIGH_ERROR`); `GTC` / `GTT` are not capped. Distinct from `expiresAfter`: `deadline` bounds how long the signature is valid; `expiresAfter` is a GTT order's own lifetime.
* `expiresAfter` (integer): The order's lifetime — expiration timestamp in seconds since epoch. **Required only for `GTT`** orders, where it must be strictly greater than `deadline`. For GTC/IOC, including triggers, omit it. A GTT trigger and its child share the same expiry and stop early to allow time for settlement; see [Trigger Orders (SL/TP)](/developers/devnet/order-entry/trigger-orders.md#choosing-the-fired-childs-time-in-force).
* `clientOrderId` (string, optional): Echoed back in the response; useful for client-side correlation independent of the server-issued `orderId`.

</details>

<details>

<summary><strong>Data Type — CreateOrderResponse payload</strong></summary>

* `status` (string, required): One of `OPEN`, `FILLED`, `CANCELLED`.
* `execQty` (string, optional): Executed quantity in this order update.
* `cumQty` (string, optional): Total executed quantity across all fills where the order is active.
* `orderId` (string, required): Server-issued order ID. Issued for **all** order types — including perp IOC: the matching engine assigns a non-zero `orderId` even though an IOC leaves no resting state.
* `clientOrderId` (string, optional): Echoes the request's `clientOrderId`.
* `cancelReason` (CancelReason, optional): Present when `status` is `CANCELLED` and the engine supplied a machine-readable reason.
* `cancelReasonMessage` (string, optional): Human-readable explanation for `cancelReason`.
* `firstFillId` (string, optional): Identifier of the first fill this order produced on entry. Together with `fillCount`, identifies a contiguous fill range. Absent if the order did not fill on entry.
* `fillCount` (integer, optional): Number of fills this order produced on entry. Present only with `firstFillId`.

</details>

### `modifyOrder`

**Purpose**: Amend one live order in place: a resting `LIMIT` (GTC or GTT; spot or perp) or an armed perp `STOP_LOSS` / `TAKE_PROFIT`. The order **keeps its `orderId` and `clientOrderId`**. Identical body and semantics to REST `POST /v2/modifyOrder`.

**Full restate.** The payload carries the complete post-modify `OrderDetails` state plus a target id — restate **every** value, even unchanged ones. It reuses the same signed EIP-712 field universe as `createOrder`, but not always the same JSON field set: a trigger modify requires immutable `timeInForce`, `reduceOnly`, and `postOnly` values, while a trigger create requires `timeInForce`, omits `reduceOnly`, and may omit a false `postOnly`. The modifiable set is class-split: `limitPx`, `qty`, `postOnly`, and `expiresAfter` for a `LIMIT` order; **only `limitPx` and `triggerPx`** for a `STOP_LOSS` / `TAKE_PROFIT` trigger. All other fields are immutable and must be restated at the live order's values. Signing reuses the `OrderDetails` envelope with a fresh nonce, exactly as `createOrder`. For the complete modify lifecycle — modifiable-vs-immutable fields, targeting, crossing behavior, queue-priority effects, and the full error set — see [Modifying a resting order](/developers/devnet/order-entry/order-lifecycle.md#modifying-a-resting-order). The field-level rules for this WebSocket payload are in the schema block below.

**Request Envelope** (re-price + resize, targeting by `orderId`):

```json
{
  "type": "modifyOrder",
  "id": "req-7f3c1e",
  "payload": {
    "orderId": "1864998629727535104",
    "clientOrderId": "1778601294274124",
    "symbol": "ETHRUSDPERP",
    "accountId": 8017,
    "exchangeId": 1,
    "isBuy": true,
    "orderType": "LIMIT",
    "timeInForce": "GTT",
    "limitPx": "41000",
    "qty": "0.02",
    "postOnly": true,
    "expiresAfter": 1747930689,
    "signature": "0x...",
    "nonce": "1778601294999111",
    "signerWallet": "0x869d6494fe32B96F93F78F9c4B7aAf30eeC01C1F",
    "deadline": 1747927089
  }
}
```

**Success Response** (rested after modify):

```json
{
  "type": "modifyOrder",
  "id": "req-7f3c1e",
  "ok": true,
  "payload": {
    "status": "OPEN",
    "orderId": "1864998629727535104",
    "clientOrderId": "1778601294274124"
  }
}
```

**Success Response** (modify crossed and filled):

```json
{
  "type": "modifyOrder",
  "id": "req-7f3c1e",
  "ok": true,
  "payload": {
    "status": "FILLED",
    "execQty": "0.4",
    "cumQty": "1.0",
    "orderId": "1864998629727535104",
    "clientOrderId": "1778601294274124",
    "firstFillId": "7205759403792794625",
    "fillCount": 2
  }
}
```

**Error Response**:

```json
{
  "type": "modifyOrder",
  "id": "req-7f3c1e",
  "ok": false,
  "error": {
    "error": "ORDER_NOT_FOUND_ERROR",
    "message": "No resting order found for the given identifier"
  }
}
```

<details>

<summary><strong>Data Type — ModifyOrderRequest payload</strong></summary>

* `orderId` (string): Reya-issued order ID of the order to modify. If present, this is the canonical lookup key; `clientOrderId`, when also present, restates the resting order's immutable client id.
* `clientOrderId` (string): Restated client-provided ID from the original `createOrder`. Used as the lookup key only when `orderId` is absent, and then it must be non-zero. If `orderId` is present, this field restates the resting order's immutable client id for signing; omit it when the resting order has no client id. The modification cannot assign a new `clientOrderId`.
* `symbol` (string, required): Market symbol for the order.
* `accountId` (integer, required): Account ID that owns the order. Immutable; must match the resting order.
* `exchangeId` (integer, **required**): On-chain `OrderDetails.exchangeId`. Restated immutable — must match the resting order (mismatch → `INPUT_VALIDATION_ERROR`).
* `isBuy` (boolean, **required**): Order side. Restated immutable — must match the live order. For `LIMIT`, it sets the sign of the E18 `qty`; for a trigger it sets the sign of the raw full-position sentinel.
* `orderType` (string, **required**): On-chain `OrderDetails.orderType`. Restated immutable: `LIMIT`, `STOP_LOSS`, or `TAKE_PROFIT`; a modify cannot change the order class.
* `timeInForce` (string, **required**): Restated immutable. Send `GTC` or `GTT` for a `LIMIT`; a modify cannot flip GTC ↔ GTT. For an armed trigger, restate the originally chosen IOC/GTC/GTT. A fired child is cancel-only.
* `triggerPx` (string, optional): On-chain `OrderDetails.triggerPrice`. **Modifiable** — re-prices an armed `STOP_LOSS` / `TAKE_PROFIT` trigger in place. **Required** on a trigger modify; omit for a `LIMIT` order.
* `reduceOnly` (boolean, **required**): On-chain `OrderDetails.reduceOnly`. Restated immutable — send `false` for a trigger.
* `limitPx` (string, **required**): Post-modify worst acceptable execution price. Send the complete intended value even when unchanged. On a LIMIT order, changing it loses queue priority and a non-post-only crossing modify executes immediately. On an armed trigger, the new limit is band-checked for its future child.
* `qty` (string, conditionally required): Required for a `LIMIT`: the post-modify **total** order quantity (not remaining), which must be strictly greater than `cumQty` (else `MODIFY_QTY_BELOW_FILLED_ERROR`). A decrease at an unchanged `limitPx` preserves queue priority; an increase loses it. Omit for a trigger; the signer restates the raw full-position sentinel.
* `postOnly` (boolean, **required**): The post-modify maker-only flag. For a `LIMIT`, if `true` and the order would cross, the modify is rejected with `POST_ONLY_WOULD_CROSS_ERROR` and the resting order is unchanged. Send `false` for a trigger.
* `expiresAfter` (integer, optional): Post-modify order lifetime in seconds since epoch. For a `LIMIT`, omit for GTC and send a future timestamp greater than `deadline` for GTT. For a GTT trigger, restate its original expiry; omit for GTC/IOC and sign `0`. A trigger's expiry cannot be amended.
* `signature` (string, required): **Fresh** EIP-712 signature over the full restated order state — all fields (modifiable + restated immutables) exactly as sent on the wire, using the same `OrderDetails` envelope as `createOrder`. See [Signatures and Nonces](/developers/devnet/authentication/signatures-and-nonces.md).
* `nonce` (string, required): Monotonically increasing per-signer nonce. A **fresh** nonce is required for every modify; replays are rejected with `INVALID_NONCE_ERROR`.
* `signerWallet` (string, required): Address that produced the signature. Not modifiable; must match the resting order's signer.
* `deadline` (integer, **required**): Signature-validity timestamp (seconds since epoch). Distinct from `expiresAfter` (the order's lifetime).

</details>

<details>

<summary><strong>Data Type — ModifyOrderResponse payload</strong></summary>

Modify response fields are the same order-outcome fields as `CreateOrderResponse`; `orderId` is the ID the order had before the modification.

* `status` (string, required): One of `OPEN`, `FILLED`, `CANCELLED`. `OPEN` for a partial fill leaving a remainder resting; `FILLED` for a complete fill when the modify crossed.
* `execQty` (string, optional): Quantity executed by this modification. Present only when the modify crossed and filled immediately; absent when the modified order rested. Use `cumQty` for lifetime filled quantity.
* `cumQty` (string, optional): Total executed quantity across the order's lifetime, including fills from before the modification.
* `orderId` (string, required): Modified order ID — unchanged by the modification.
* `clientOrderId` (string, optional): Client-provided order ID preserved from order creation.
* `cancelReason` (CancelReason, optional): Present when `status` is `CANCELLED` and the engine supplied a machine-readable reason.
* `cancelReasonMessage` (string, optional): Human-readable explanation for `cancelReason`.
* `firstFillId` (string, optional): Identifier of the first fill this modify produced. Together with `fillCount`, identifies a contiguous fill range. Absent if the modify did not fill immediately.
* `fillCount` (integer, optional): Number of fills this modify produced. Present only with `firstFillId`.

</details>

### `cancelOrder`

**Purpose**: Cancel a previously placed order by `orderId`, or by non-zero `clientOrderId` when `orderId` is absent. Identical body and semantics to REST `POST /v2/cancelOrder`.

**Request Envelope**:

```json
{
  "type": "cancelOrder",
  "id": "req-7f3c1c",
  "payload": {
    "orderId": "1864998629727535104",
    "accountId": 10000000002,
    "symbol": "WETHRUSD",
    "signature": "0x...",
    "nonce": "1778601294356211",
    "deadline": 1747927089
  }
}
```

**Success Response**:

```json
{
  "type": "cancelOrder",
  "id": "req-7f3c1c",
  "ok": true,
  "payload": {
    "status": "CANCELLED",
    "orderId": "1864998629727535104"
  }
}
```

<details>

<summary><strong>Data Type — CancelOrderRequest payload</strong></summary>

* `orderId` (string): Reya-issued order ID to cancel. Provide `orderId`, or provide a non-zero `clientOrderId` when `orderId` is absent.
* `clientOrderId` (string): Client-provided order ID to cancel. Used only when `orderId` is absent, and then it must be non-zero. Omit it when cancelling by `orderId` or when the order has no client id.
* `accountId` (integer, required): Account ID that owns the order.
* `symbol` (string, required): Market symbol for the order.
* `signature` (string, required): EIP-712 signature over the cancellation.
* `nonce` (string, required): Cancel nonce.
* `deadline` (integer, required): Signature-validity timestamp (seconds since epoch). Must be within 600 seconds of now (`ORDER_DEADLINE_TOO_HIGH_ERROR`).

</details>

<details>

<summary><strong>Data Type — CancelOrderResponse payload</strong></summary>

* `status` (string, required): Always `CANCELLED` on success.
* `orderId` (string, required): The cancelled order ID.
* `clientOrderId` (string, optional): Echoes the request's `clientOrderId`.

</details>

### `cancelAll`

**Purpose**: Mass-cancel all open orders for an account on a given market (spot **or perp**), or across all markets if `symbol` is omitted. Identical body and semantics to REST `POST /v2/cancelAll`. Per-market mass-cancel is supported for **both spot and perp** markets.

**Request Envelope**:

```json
{
  "type": "cancelAll",
  "id": "req-7f3c1d",
  "payload": {
    "accountId": 10000000002,
    "symbol": "WETHRUSD",
    "signature": "0x...",
    "nonce": "1778601294501222",
    "deadline": 1747927089
  }
}
```

**Success Response**:

```json
{
  "type": "cancelAll",
  "id": "req-7f3c1d",
  "ok": true,
  "payload": {
    "cancelledCount": 3
  }
}
```

<details>

<summary><strong>Data Type — MassCancelRequest payload</strong></summary>

* `accountId` (integer, required): Account ID to cancel orders for.
* `symbol` (string, optional): Symbol to cancel orders for. If omitted, cancels all orders for the account across all markets.
* `signature` (string, required): EIP-712 signature.
* `nonce` (string, required): Mass-cancel nonce.
* `deadline` (integer, **required**): Signature-validity timestamp (seconds since epoch). Must be within 600 seconds of now (`ORDER_DEADLINE_TOO_HIGH_ERROR`).

</details>

<details>

<summary><strong>Data Type — MassCancelResponse payload</strong></summary>

* `cancelledCount` (integer, required): Number of orders that were cancelled.

</details>

### `cancelAllAfter`

**Purpose**: Arm, refresh, or disarm an **account-scoped dead-man's switch** (cancel-on-disconnect). Identical body and semantics to REST `POST /v2/cancelAllAfter`. Send a signed `cancelAllAfter` with a `timeoutMs` in `[5000, 60000]` to arm/refresh a countdown, or `0` to disarm; if the countdown elapses without a refresh, the matching engine cancels every open order on the account except protective stops. The switch is transport-agnostic (REST and ws-exec share one account-level timer) and is **not** tied to the connection — closing the WebSocket neither fires nor disarms it, and only another `cancelAllAfter` refreshes it. For the full behavior — bounds, protective-stops exemption, trigger semantics, and recommended heartbeat use — see [Cancel-on-Disconnect](/developers/devnet/order-entry/cancel-on-disconnect.md). The field-level rules are in the schema block below.

The EIP-712 envelope signs `accountId`, `timeoutMs`, `nonce`, and `deadline`. Note that `deadline` is the **signature-validity window** (unix seconds), **not** the trigger time — the trigger time is `timeoutMs` relative to server receipt.

**Request Envelope** (arm a 30-second countdown):

```json
{
  "type": "cancelAllAfter",
  "id": "req-7f3c1f",
  "payload": {
    "accountId": 10000000002,
    "timeoutMs": 30000,
    "signature": "0x...",
    "nonce": "1778601295111333",
    "signerWallet": "0x869d6494fe32B96F93F78F9c4B7aAf30eeC01C1F",
    "deadline": 1747927089
  }
}
```

**Success Response** (armed — echoes `timeoutMs` and the `triggerAt` fire time):

```json
{
  "type": "cancelAllAfter",
  "id": "req-7f3c1f",
  "ok": true,
  "payload": {
    "accountId": 10000000002,
    "timeoutMs": 30000,
    "triggerAt": 1747927119946
  }
}
```

**Success Response** (disarm — `timeoutMs: 0`, no `triggerAt`):

```json
{
  "type": "cancelAllAfter",
  "id": "req-7f3c20",
  "ok": true,
  "payload": {
    "accountId": 10000000002,
    "timeoutMs": 0
  }
}
```

<details>

<summary><strong>Data Type — CancelAllAfterRequest payload</strong></summary>

* `accountId` (integer, required): Account ID whose open orders are covered by the countdown.
* `timeoutMs` (integer, required): Countdown duration in milliseconds. `0` disarms; any non-zero value must be within **\[5000, 60000]** and (re-)arms a fresh countdown of that duration, replacing any previously armed one (re-arming with the same value is the refresh/heartbeat). Out-of-range values are rejected with `INPUT_VALIDATION_ERROR`.
* `signature` (string, required): EIP-712 signature over the `CancelAllAfter` envelope (signs `accountId`, `timeoutMs`, `nonce`, `deadline`). See [Signatures and Nonces](/developers/devnet/authentication/signatures-and-nonces.md).
* `nonce` (string, required): Monotonically increasing per-signer nonce. A fresh nonce is required on every arm/refresh/disarm call; replays are rejected with `INVALID_NONCE_ERROR`.
* `signerWallet` (string, required): Address that produced the signature.
* `deadline` (integer, required): Signature-validity timestamp (seconds since epoch) — **not** the trigger time. The countdown is `timeoutMs` relative to server receipt.

</details>

<details>

<summary><strong>Data Type — CancelAllAfterResponse payload</strong></summary>

* `accountId` (integer, required): Account ID the countdown applies to, echoed from the request.
* `timeoutMs` (integer, required): Effective countdown duration in milliseconds; `0` means the switch is now disarmed.
* `triggerAt` (integer, optional): Milliseconds POSIX timestamp at which the cancel-all fires if the countdown is not refreshed before then (server receipt time + `timeoutMs`). Omitted when `timeoutMs` is `0` (disarmed).

</details>

### `ping` / `pong`

**Purpose**: Optional client-initiated application-level liveness/RTT probe. The client sends `{type:"ping", id?}`; the server replies with `{type:"pong", id?}` echoing the optional `id`. See [Heartbeats](/developers/devnet/connectivity/heartbeats.md) for the full description — when to use it, when not to, and how it relates to the protocol-level liveness mechanism that keeps the connection alive automatically.

## Error Catalog

Every error envelope (both per-operation `{ok: false, error}` and top-level `error`) carries a `RequestError`-shaped object:

```json
{
  "error": "INPUT_VALIDATION_ERROR",
  "message": "qty is missing"
}
```

The `error` field is one of the codes below. Per-operation responses (`{ok: false, error}` on `createOrder` / `modifyOrder` / `cancelOrder` / `cancelAll` / `cancelAllAfter`) use codes from the **Order Management** group, shared 1:1 with REST. Top-level `error` envelopes use codes from the **Framing Layer** group exclusively; these only make sense for a streamed envelope protocol and never appear in REST responses.

### Order Management Codes (shared with REST)

| Code                                            | When emitted                                                                                                                                                                                                                                                 | Client action                                                                                                                            |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `SYMBOL_NOT_FOUND_ERROR`                        | The `symbol` in the payload doesn't resolve to a known market.                                                                                                                                                                                               | Refresh market definitions via REST `GET /v2/perpMarketDefinitions`.                                                                     |
| `NO_ACCOUNTS_FOUND_ERROR`                       | The `accountId` doesn't exist or isn't owned by the signer.                                                                                                                                                                                                  | Verify account configuration.                                                                                                            |
| `INPUT_VALIDATION_ERROR`                        | Generic body-validation failure (missing field, wrong type, illegal value). Also a `modifyOrder` that restated an immutable field with a value that doesn't match the resting order, or whose `expiresAfter` is inconsistent with the order's `timeInForce`. | Fix the request body; consult the human-readable `message`.                                                                              |
| `RATE_LIMITED_ERROR`                            | The request exceeded the order-entry rate limit.                                                                                                                                                                                                             | Back off and retry.                                                                                                                      |
| `INSUFFICIENT_BALANCE_ERROR`                    | Available collateral is insufficient for the order.                                                                                                                                                                                                          | Reduce size or add collateral.                                                                                                           |
| `OPEN_ORDER_CAP_ERROR`                          | The account's resting open-order cap is reached.                                                                                                                                                                                                             | Cancel resting orders before placing another GTC/GTT order.                                                                              |
| `PRICE_QTY_BOUNDS_ERROR`                        | Price or quantity is outside the market's accepted bounds.                                                                                                                                                                                                   | Refresh market definitions and adjust price/quantity.                                                                                    |
| `SERVICE_DISABLED_ERROR`                        | Order entry is disabled for this market or instrument.                                                                                                                                                                                                       | Stop submitting until the market is re-enabled.                                                                                          |
| `UNAUTHORIZED_ACCOUNT_ERROR`                    | The signer is valid but is not authorized for the target account/order.                                                                                                                                                                                      | Check account ownership and trading-key permissions.                                                                                     |
| `TRADING_HALTED_ERROR`                          | Trading is halted for the market.                                                                                                                                                                                                                            | Stop submitting orders until trading resumes.                                                                                            |
| `DUPLICATE_CLIENT_ORDER_ID_ERROR`               | A live order already uses the supplied `clientOrderId`.                                                                                                                                                                                                      | Use a fresh client order id or reconcile the existing order.                                                                             |
| `ACCOUNT_BELOW_LIQUIDATION_MARGIN_ERROR`        | The account is already below liquidation margin, or its realised balance is below the chain floor.                                                                                                                                                           | Restore the margin / real-balance floor; ordinary reducing orders do not bypass it. Read `message` for the binding condition.            |
| `ACCOUNT_BELOW_INITIAL_MARGIN_ERROR`            | The proposed trade would leave the account below initial margin, including the settlement buffer.                                                                                                                                                            | Reduce size, add collateral, or submit a genuinely risk-improving order. Do not retry unchanged.                                         |
| `OPEN_INTEREST_CAP_ERROR`                       | The market's open-interest cap is reached.                                                                                                                                                                                                                   | Wait for market open interest to fall or reduce exposure; do not loop retries.                                                           |
| `OPEN_INTEREST_BUDGET_ERROR`                    | The tighter budget covering matched-but-unsettled fills is exhausted.                                                                                                                                                                                        | Let pending settlement clear, then retry deliberately; do not blindly loop.                                                              |
| `REDUCE_ONLY_CONDITION_NOT_MET_ERROR`           | A reduce-only order would not strictly reduce the current position.                                                                                                                                                                                          | Reconcile the position, side, and size, then re-sign a correcting order.                                                                 |
| `CROSSING_ORDERS_TEMPORARILY_UNAVAILABLE_ERROR` | The exchange temporarily cannot run pre-trade risk checks for an order that would cross immediately.                                                                                                                                                         | Wait for risk inputs to recover before retrying. Cancels remain available; other admission still requires its own risk data to be ready. |
| `TRIGGER_IOC_MUST_NOT_EXPIRE_ERROR`             | An IOC trigger carries an expiry. This invalid combination can also return `INPUT_VALIDATION_ERROR`.                                                                                                                                                         | Omit `expiresAfter` for IOC and re-sign.                                                                                                 |
| `TRIGGER_LIMIT_OUTSIDE_BAND_ERROR`              | `limitPx` is outside the permitted trigger-price band, or stops are unavailable on the market.                                                                                                                                                               | Read `message`: move the limit closer and re-sign for a price-band rejection; contact Reya if stops are unavailable.                     |
| `TRIGGER_ALREADY_EXISTS_ERROR`                  | An armed trigger of the same type already exists for this account and market.                                                                                                                                                                                | Modify or cancel the existing trigger before creating another of that type.                                                              |
| `ORDER_EXPIRES_TOO_SOON_ERROR`                  | A GTT `expiresAfter` is still in the future but leaves too little time for settlement.                                                                                                                                                                       | Re-sign with a later `expiresAfter`; do not retry the same payload.                                                                      |
| `CREATE_ORDER_OTHER_ERROR`                      | Generic matching-engine create failure not covered by a more specific code.                                                                                                                                                                                  | Read `message` for the underlying reason; do not infer a settlement result from this catch-all.                                          |
| `CANCEL_ORDER_OTHER_ERROR`                      | Generic cancelOrder failure not covered by a more specific code.                                                                                                                                                                                             | Read `message`.                                                                                                                          |
| `ORDER_DEADLINE_PASSED_ERROR`                   | `deadline` (the signature-validity timestamp) is in the past.                                                                                                                                                                                                | Re-sign with a fresh `deadline`.                                                                                                         |
| `ORDER_DEADLINE_TOO_HIGH_ERROR`                 | `deadline` is more than **600 seconds** in the future. Enforced on **IOC** creates and on **all cancels** (`cancelOrder` / `cancelAll`); `GTC` / `GTT` creates are not capped.                                                                               | Use a `deadline` within 600 seconds of now.                                                                                              |
| `INVALID_NONCE_ERROR`                           | Nonce is not strictly monotonic for this signer, or was already used.                                                                                                                                                                                        | Re-sign with a fresh monotonic nonce.                                                                                                    |
| `CANCEL_ALL_AFTER_OTHER_ERROR`                  | A matching-engine-side `cancelAllAfter` failure with no more specific code. (A non-zero `timeoutMs` outside `[5000, 60000]` is rejected with `INPUT_VALIDATION_ERROR`, not this code.)                                                                       | Read `message`; for an out-of-band `timeoutMs`, send `0` or a value in range.                                                            |
| `UNAVAILABLE_MATCHING_ENGINE_ERROR`             | The matching engine is unavailable (transient).                                                                                                                                                                                                              | Retry after a short backoff.                                                                                                             |
| `UNAUTHORIZED_SIGNATURE_ERROR`                  | The recovered signer is not authorized to act on the `accountId`.                                                                                                                                                                                            | Verify the signer wallet is in the account's permissioned-addresses list on-chain.                                                       |
| `NUMERIC_OVERFLOW_ERROR`                        | A numeric field exceeds the allowed uint64 / int256 range.                                                                                                                                                                                                   | Fix the request body.                                                                                                                    |
| `POST_ONLY_WOULD_CROSS_ERROR`                   | A post-only order (on `createOrder`) or post-only modify (on `modifyOrder`) would cross or touch the best opposite price at entry. Nothing is placed; on a modify the resting order is left untouched.                                                       | Re-price the order so it rests as a maker, or drop `postOnly` to allow it to take.                                                       |
| `ORDER_NOT_FOUND_ERROR`                         | The `orderId` / `clientOrderId` targeted by a `modifyOrder` or `cancelOrder` does not resolve to a live resting order.                                                                                                                                       | Verify the order is still resting (check `GET /v2/wallet/{address}/openOrders`).                                                         |
| `EMPTY_MODIFY_ERROR`                            | A `modifyOrder` whose post-modify state is identical to the order's current state — no field actually changed.                                                                                                                                               | Send a modify that changes at least one of `limitPx` / `qty` / `postOnly` / `expiresAfter` / `triggerPx`.                                |
| `MODIFY_QTY_BELOW_FILLED_ERROR`                 | A `modifyOrder` `qty` (the total post-modify quantity) is not strictly greater than the order's already-filled `cumQty`.                                                                                                                                     | Set `qty` above the filled amount, or cancel instead.                                                                                    |
| `MODIFY_ORDER_OTHER_ERROR`                      | A `modifyOrder` the matching engine can't apply — e.g. the signer isn't authorized for the account, or (for a trigger modify) a matching-engine-level trigger rule is violated.                                                                              | Read `message` — the engine's reason is passed through. A fired protective child must be cancelled, not modified.                        |

### Framing Layer Codes (top-level `error` envelope only)

These codes appear **only** in the top-level `error` envelope, never inside a per-operation `{ok: false}` response.

| Code                   | When emitted                                                                                                                                                                                                                        | Client action                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `MALFORMED_JSON`       | The frame body could not be parsed as JSON, or required envelope fields (`type`, `id`) are missing. Connection stays open.                                                                                                          | Fix the client serializer.                                                                                                        |
| `UNKNOWN_TYPE`         | The frame's `type` field is not one of the accepted values. Connection stays open.                                                                                                                                                  | Verify the request `type`.                                                                                                        |
| `DUPLICATE_REQUEST_ID` | The frame's `id` is already in-flight on this connection — i.e., the client sent a new request with an `id` whose response hasn't yet been emitted. Connection stays open; the new request is rejected, the original is unaffected. | Use a fresh `id` for each request (UUIDs work).                                                                                   |
| `INTERNAL`             | The server hit an internal failure handling the frame. Connection stays open.                                                                                                                                                       | Retry; if the problem persists, contact support with the `id`.                                                                    |
| `SERVER_SHUTTING_DOWN` | The service is restarting and is not accepting new requests.                                                                                                                                                                        | Reconnect and reconcile unanswered requests before resubmitting; see [Heartbeats](/developers/devnet/connectivity/heartbeats.md). |
| `TOO_MANY_INFLIGHT`    | The connection has too many un-acknowledged in-flight requests.                                                                                                                                                                     | Throttle — wait for outstanding responses before sending more.                                                                    |

## Data Types & Schemas

### Enumeration Types

<details>

<summary><strong>OrderType</strong></summary>

* `LIMIT` — Limit order (with `timeInForce` = `IOC`, `GTC`, or `GTT`).
* `STOP_LOSS` — Stop-loss conditional order (perp).
* `TAKE_PROFIT` — Take-profit conditional order (perp).

</details>

<details>

<summary><strong>TimeInForce</strong></summary>

* `IOC` — Immediate or Cancel: fills immediately against the book, never rests.
* `GTC` — Good Till Cancelled: rests on the book until filled or cancelled.
* `GTT` — Good Till Time: rests until filled, cancelled, or its signed `expiresAfter` timestamp passes (which must be strictly later than `deadline`).

</details>

<details>

<summary><strong>OrderStatus</strong></summary>

* `OPEN` — Order is resting in the book.
* `FILLED` — Order is fully filled by the matching engine. This is the **matching-engine** status — a fill is only final once it settles on-chain; see [Trade Settlement](/developers/devnet/executions-and-settlement/settlement.md) and [Trade Busts](/developers/devnet/executions-and-settlement/trade-busts.md).
* `CANCELLED` — Order is cancelled.

Request-level rejects such as post-only-would-cross are returned as error responses (`RequestErrorCode`), not as an order status.

</details>

<details>

<summary><strong>CancelReason</strong></summary>

* `NO_LIQUIDITY` — IOC executed zero quantity because no fillable liquidity was available at its limit.
* `IOC_REMAINDER` — IOC partially filled and cancelled the unfilled remainder.
* `SELF_TRADE_PREVENTION` — Order would have crossed the same account.
* `GTT_EXPIRED` — GTT order expired.
* `USER_CANCEL` — Explicit user cancel.
* `MASS_CANCEL` — Explicit cancel-all.
* `CANCEL_ALL_AFTER` — Cancel-on-disconnect timer fired.

</details>

{% hint style="info" %}
Protective-stop lifecycle reasons (`OCO_SIBLING_FIRED`, `PROTECTIVE_SELF_TRADE_SWEEP`, `POSITION_CLOSED`, `RISK_REJECTED`) arrive on the Info stream; see [Trigger Orders (SL/TP)](/developers/devnet/order-entry/trigger-orders.md#firing-oco-and-self-trades).

`RISK_CANCELLED` is delivered only on the WebSocket **Info** `orderChanges` stream when pre-trade risk cancels an already-resting order; it is never a create/modify response. `FEED_RESET` is an Info resynchronization signal; current devnet1 uses close code `1012` instead. WebSocket Order Entry responses carry neither value. See the [WebSocket Info API Reference](/developers/devnet/api-reference/websocket-api-reference.md).
{% endhint %}

For the complete enumeration of `RequestErrorCode` (per-operation errors) and `WsExecErrorCode` (top-level errors), see the [Error Catalog](#error-catalog) above.

## Connection Management

### Reconnection Pattern

Reconnect with the usual exponential-backoff-with-jitter pattern any robust WebSocket client should use. The Reya-specific bits are:

1. **No subscription state to replay.** There is no session-bound identity to restore — the next request authenticates itself via its EIP-712 signature just like the first one did.
2. **Verify in-flight requests via REST before resubmitting.** Closing the WebSocket has zero persistent side effects on the order book itself, but an unanswered request may already have reached the matching engine and created, matched, modified, or cancelled an order. Check `GET /v2/wallet/{address}/openOrders`, `orderHistory`, and the relevant executions endpoint before retrying — otherwise you risk a duplicate action. See [Idempotency (`clientOrderId`)](#idempotency-clientorderid) for safe correlation; do not infer settlement merely from request acceptance.

For the meaning of WS close codes you'll see on `onclose` (`1000`, `1001`, `1006`, etc.), see [What Happens When the Server Closes the Connection](/developers/devnet/connectivity/heartbeats.md#what-happens-when-the-server-closes-the-connection).

### Service Restarts

Order Entry service restarts close connections with code `1001`; reconcile any unanswered request. The Info WebSocket uses a `1012` resync close. Both are documented in [Service restarts](/developers/devnet/connectivity/heartbeats.md#service-restarts).

### In-Flight `id` Uniqueness

Each `id` must be unique across **in-flight** requests on the connection. Once the server has emitted the corresponding response envelope, the `id` is free to reuse. Sending a fresh request with the same `id` as an in-flight one triggers a top-level `DUPLICATE_REQUEST_ID` error envelope; the original in-flight request is unaffected.

In practice, clients should generate a fresh `id` for every request (e.g. UUIDv4 or a monotonic counter prefixed with a session token).

### Idempotency (`clientOrderId`)

For `createOrder`, the optional `clientOrderId` field is echoed back unchanged in the response and in any subsequent order-update events on the Info WebSocket. Use it for client-side correlation independent of the server-issued `orderId` — particularly useful when the response envelope is lost mid-flight and the client must reconcile state from Info-WebSocket updates after reconnect.

When supplied with a non-zero value, `clientOrderId` must be unique among live orders for the same signer wallet on the same market. A duplicate live `clientOrderId` is rejected with `DUPLICATE_CLIENT_ORDER_ID_ERROR`. Omit the field when you do not want a client tag; the signer encodes the omitted value as `0`. This is live-order deduplication for correlation and safe retries, not exactly-once idempotency across all historical terminal orders.

## Signatures and Nonces

All `createOrder`, `modifyOrder`, `cancelOrder`, `cancelAll`, and `cancelAllAfter` payloads carry an EIP-712 `signature` over the order contents. The shape of the signed message is identical to the REST `/v2` endpoints — see [Signatures and Nonces](/developers/devnet/authentication/signatures-and-nonces.md) for the canonical reference. The Python SDK provides the helpers `sign_order`, `sign_cancel_order`, `sign_mass_cancel`, and `sign_cancel_all_after` to produce these signatures correctly. A `modifyOrder` reuses the same `OrderDetails` envelope as `createOrder` (`sign_order`), signed over the full post-modify order state with a fresh nonce.

The WebSocket transport does **not** add or change any signing requirement. Frames are signed by your wallet (or a permissioned trading key) at the payload level; the WebSocket connection itself is unauthenticated.

## Differences vs REST

The WebSocket Order Entry surface is functionally equivalent to the corresponding REST endpoints, with transport-level differences:

| Concern           | REST                                                                                                                    | WebSocket Order Entry                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Request body      | `CreateOrderRequest` / `ModifyOrderRequest` / `CancelOrderRequest` / `MassCancelRequest` / `CancelAllAfterRequest`      | Same — embedded as `payload`                                                                |
| Response body     | `CreateOrderResponse` / `ModifyOrderResponse` / `CancelOrderResponse` / `MassCancelResponse` / `CancelAllAfterResponse` | Same — embedded as `payload` on `ok: true`                                                  |
| Success / failure | HTTP `200` vs `400` / `500`                                                                                             | `ok: true` vs `ok: false` inside the frame                                                  |
| Error body        | `RequestError`                                                                                                          | Same — embedded as `error` on `ok: false`                                                   |
| Correlation       | HTTP request/response pairing                                                                                           | Client-supplied `id` field                                                                  |
| Heartbeat         | n/a (stateless)                                                                                                         | Protocol-level, automatic — see [Heartbeats](/developers/devnet/connectivity/heartbeats.md) |
| Auth              | EIP-712 signature in body                                                                                               | Same EIP-712 signature in same body                                                         |
| Idempotency       | `clientOrderId` echoed                                                                                                  | Same                                                                                        |
| Connection        | Per-request                                                                                                             | Persistent, multiplexed                                                                     |

**When to use which:**

* **REST** — One-off requests, low frequency, simpler client integration, no need to maintain a long-lived connection.
* **WebSocket Order Entry** — High-frequency order submission, lower per-request overhead (no TLS handshake per request), latency-sensitive integrations. Recommended for latency-sensitive integrations running together with the [WebSocket Info API](/developers/devnet/api-reference/websocket-api-reference.md).

## Python SDK Example

A worked example is included in the [pinned Python SDK onboarding snapshot](https://github.com/Reya-Labs/reya-python-sdk/tree/37450ccb2babc99398d1ac1290d48860a9a2e2fa) at [`examples/websocket/exec/ws_exec.py`](https://github.com/Reya-Labs/reya-python-sdk/blob/37450ccb2babc99398d1ac1290d48860a9a2e2fa/examples/websocket/exec/ws_exec.py) — a minimal devnet quickstart that places a resting spot LIMIT GTC order and cancels it through the high-level `ReyaWsExecClient` (which handles the wire envelope, EIP-712 signing, and in-flight request dispatch for you).

After [installing the pinned SDK](/developers/devnet/getting-started/onboarding.md#install-the-pinned-python-sdk), run from its checkout:

```bash
.venv/bin/python -m examples.websocket.exec.ws_exec
```

See the [example's README](https://github.com/Reya-Labs/reya-python-sdk/blob/37450ccb2babc99398d1ac1290d48860a9a2e2fa/examples/websocket/exec/README.md) for prerequisites (`.env` setup, funded test accounts).
