> 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/order-entry/trigger-orders.md).

# Trigger Orders (SL/TP)

`STOP_LOSS` and `TAKE_PROFIT` are whole-position protective stops on perp markets. **Arming and firing are live on devnet1.** Accepted triggers survive service restarts and are evaluated against the market's **mark price**. While armed it adds no order-book liquidity; when it fires it becomes an order with the time-in-force you chose at creation.

## The whole-position model

A trigger create or modify **omits JSON `qty`**. The signature carries a full-position sentinel, so you do not re-sign when your position size changes. At firing, the engine sizes the child from the live position still reducible in the signed direction.

| Position protected | `isBuy`        | Stop-loss condition      | Take-profit condition    |
| ------------------ | -------------- | ------------------------ | ------------------------ |
| Long               | `false` (sell) | Mark price ≤ `triggerPx` | Mark price ≥ `triggerPx` |
| Short              | `true` (buy)   | Mark price ≥ `triggerPx` | Mark price ≤ `triggerPx` |

At most **one stop-loss and one take-profit** can be armed per account per market. They form an OCO (one-cancels-the-other) pair. Cancel or re-price an existing same-type trigger before arming another. Triggers are perp-only; spot triggers are rejected.

{% hint style="info" %}
This contract is live in API **3.5.2** as of September 14, 2026. Trigger create and modify requests must omit both Boolean fields, including `false`. Use the matching Python SDK **3.5.2.0** source snapshot or npm SDK **0.146.5-perpob.257**; see the [Changelog](/developers/devnet/changelog/changelog.md).
{% endhint %}

## Request contract

Use REST `POST /v2/createOrder` or the ws-exec `createOrder` message:

| Field          | Trigger create rule                                                                                 |
| -------------- | --------------------------------------------------------------------------------------------------- |
| `orderType`    | `STOP_LOSS` or `TAKE_PROFIT`                                                                        |
| `qty`          | **Omit**; sign the full-position sentinel below                                                     |
| `triggerPx`    | Required, positive firing threshold                                                                 |
| `limitPx`      | Required, positive worst acceptable execution price; subject to the trigger band below              |
| `timeInForce`  | **Required:** `IOC`, `GTC`, or `GTT`; selects the fired child's behaviour                           |
| `expiresAfter` | Required for GTT: a future Unix-second timestamp strictly greater than `deadline`; omit for GTC/IOC |
| `reduceOnly`   | Omit, including `false`; sign `false`. The resulting order can only reduce the protected position   |
| `postOnly`     | Omit, including `false`; sign `false`                                                               |

For example, this stop-loss protects a long ETH position by selling at no worse than 2,490 when the mark reaches 2,500. The fired child is IOC. Replace the account, signer, nonce, signature and illustrative deadline with current values before submitting.

```json
{
  "accountId": 12345,
  "symbol": "ETHRUSDPERP",
  "exchangeId": 1,
  "isBuy": false,
  "orderType": "STOP_LOSS",
  "timeInForce": "IOC",
  "triggerPx": "2500",
  "limitPx": "2490",
  "signerWallet": "0xYourTradingWallet",
  "nonce": "1700000000000001",
  "signature": "0x…",
  "deadline": 1789114830
}
```

A successful create acknowledges arming with an `orderId` and `status: OPEN`; it is not a fill confirmation. A trigger already satisfying its firing condition can fire on the next evaluation — do not assume it will remain armed after the acknowledgement.

### Choosing the fired child's time-in-force

| TIF   | Behaviour after firing                                                                                                                |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `IOC` | Fill available liquidity within `limitPx`, then cancel the remainder. A partial fill can leave a residual position without protection |
| `GTC` | Fill available liquidity within `limitPx`, then rest any remainder until filled or cancelled                                          |
| `GTT` | Fill and rest like GTC, but stop at the expiry boundary below                                                                         |

A GTT trigger has **one absolute `expiresAfter` shared by the armed trigger and fired child**. Time spent armed reduces the child's remaining lifetime. Both phases expire **before** that timestamp to allow time for settlement, with `cancelReason: GTT_EXPIRED`. An expiry that leaves too little time is rejected with `ORDER_EXPIRES_TOO_SOON_ERROR`. Allow a buffer beyond your intended protection window; the required buffer is not configurable by clients or returned by the API. On-chain settlement still rejects a fill that lands after the signed expiry.

### Trigger-price band

`limitPx` must be within the market's permitted distance from `triggerPx` on both create and modify. The API does not return that distance. Keep the two prices close; `TRIGGER_LIMIT_OUTSIDE_BAND_ERROR` means either the limit is too far from the trigger or stops are unavailable on that market. Read the message: move the limit closer only for a price-band rejection, and contact Reya if stops are unavailable.

## Signing the full-position sentinel

Use the same `Order` / `OrderDetails` envelope as a limit order; see [Signatures & Nonces](/developers/devnet/authentication/signatures-and-nonces.md).

* **`quantity`** is `±(2²⁵⁵ − 1)` (`type(int256).max`), a raw integer, **not E18-scaled** and not zero. Buy signs the positive sentinel; sell signs the negative sentinel. Its sign must match `isBuy`.
* **`timeInForce`** signs your selected TIF: `0` for GTC, `1` for IOC, `2` for GTT. Send the matching JSON field; omission is invalid.
* **`expiresAfter`** signs the GTT timestamp, or `0` for GTC/IOC while omitting the JSON field.
* Prices are E18-scaled in the signature. Sign `reduceOnly = false` and `postOnly = false`.

The EIP-712 type shape and sentinel are unchanged by firing support; the chosen TIF and expiry are now part of the trigger's intended state. Do not reuse older examples that omit `timeInForce` or always sign GTC. The [pinned Python SDK](https://github.com/Reya-Labs/reya-python-sdk/tree/37450ccb2babc99398d1ac1290d48860a9a2e2fa) derives the sentinel internally from `is_buy`; omit `qty` in the request.

## Firing, OCO and self-trades

When a trigger fires, it becomes the selected order and cancels the other armed leg with `OCO_SIBLING_FIRED`. **This also happens when the child fills nothing.** A fired IOC can therefore consume both protective legs without closing the whole position. Re-arming protection is your responsibility; the exchange does not automatically retry or re-arm the pair.

If the firing stop would trade against the account's own resting makers, those maker orders are cancelled with `PROTECTIVE_SELF_TRADE_SWEEP` so the protective child can execute. The reason appears on the **maker's** cancellation. Ordinary `SELF_TRADE_PREVENTION` instead cancels the incoming taker and leaves the maker in place.

A fire can fail before a child exists:

* **`POSITION_CLOSED`** — the position has closed, flipped, or fallen below one tradable lot.
* **`RISK_REJECTED`** — [pre-trade risk](/developers/devnet/risk/margin-system.md#pre-trade-risk-checks) refuses the proposed close.

Both legs then become terminal with that refusal reason and `triggered: false`; neither leg remains as protection. `cancelReasonMessage` is a fixed explanation of the reason, not the individual risk check that failed.

A trading halt, stale required mark, or temporarily unavailable risk checks **defers** firing and leaves the trigger armed; this differs from a terminal `RISK_REJECTED` cancellation. GTT expiry still applies while firing is deferred. Once inputs recover, the engine evaluates qualifying triggers again.

Position updates can also retire armed protection with `POSITION_CLOSED` when the recorded position has closed, flipped, or become too small to trade, without waiting for a price crossing. Reconcile the position before re-arming.

## Reading armed and fired orders

`openOrders` and the `orderChanges` stream expose `triggered`:

* **`false`**: still armed, outside the book, modifiable and cancellable; `qty` is omitted.
* **`true`**: fired child. A GTC/GTT remainder can rest in the book, but is **cancel-only**. Each fill is clamped to the position still reducible at that instant, so its remaining quantity is an upper bound, not a guaranteed fillable size. A clamp to zero cancels the remainder with `POSITION_CLOSED`.

A fired child is not proactively removed when another action closes the position. It can still block the account's own crossing orders through ordinary self-trade prevention; explicitly cancel it when it is no longer wanted.

`orderHistory` **always omits `triggered`**. Absence there means **unknown**, not `false`; use `openOrders` or live `orderChanges` when you need this distinction. On those two surfaces, absence indicates an older deployment predating firing support. `LIMIT` orders are never `triggered: true`.

## Re-pricing (modify)

Only an **armed** trigger can be modified. `modifyOrder` preserves its `orderId` and `clientOrderId` and changes **only `limitPx` and `triggerPx`**. The new price pair must satisfy the same trigger-price band.

Restate the original `timeInForce` and, for GTT, the original `expiresAfter`. Omit `reduceOnly`, `postOnly`, and `qty`. Supplying either Boolean field, even `false`, is rejected. The SDK and API reconstruct both signed Boolean values as `false`, independently of stored order state. All identity fields and the sentinel's sign stay unchanged. Sign the complete post-modify state with a fresh nonce. Changing a trigger's TIF or expiry requires cancelling and creating a new trigger.

For the IOC stop above, a re-price request is:

```json
{
  "orderId": "490346525705109504",
  "accountId": 12345,
  "symbol": "ETHRUSDPERP",
  "exchangeId": 1,
  "isBuy": false,
  "orderType": "STOP_LOSS",
  "timeInForce": "IOC",
  "triggerPx": "2550",
  "limitPx": "2540",
  "signerWallet": "0xYourTradingWallet",
  "nonce": "1700000000000002",
  "signature": "0x…",
  "deadline": 1789114890
}
```

Use current request-signing values as for create. An identical restate is rejected with `EMPTY_MODIFY_ERROR`. A fired child cannot be modified.

## Cancelling protection

* **Single cancel** works by `orderId` or `clientOrderId`. Cancellation is bound to the signer wallet that armed the trigger. Manually cancelling one armed leg leaves its sibling armed.
* **User `cancelAll`** clears armed triggers and fired resting children within its scope, as well as ordinary orders.
* [**Cancel-on-Disconnect**](/developers/devnet/order-entry/cancel-on-disconnect.md) preserves both armed triggers and fired protective children. A disconnect does not remove this exposure.

## Errors

| Code                                | Meaning and action                                                                                                                                                                                                                                   |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INPUT_VALIDATION_ERROR`            | Missing/invalid TIF, invalid TIF/expiry combination, supplied `qty`, `reduceOnly`, or `postOnly` on create or modify (including Boolean `false`), invalid trigger price, spot trigger, or an invalid modify restate. Correct the request and re-sign |
| `TRIGGER_ALREADY_EXISTS_ERROR`      | Same-type armed trigger exists for this account/market; modify or cancel it first                                                                                                                                                                    |
| `TRIGGER_IOC_MUST_NOT_EXPIRE_ERROR` | An IOC trigger carries an expiry. Omit `expiresAfter` and re-sign; this invalid combination can also return `INPUT_VALIDATION_ERROR`                                                                                                                 |
| `TRIGGER_LIMIT_OUTSIDE_BAND_ERROR`  | Prices are too far apart, or stops are unavailable on the market; read the message before changing the price                                                                                                                                         |
| `ORDER_EXPIRES_TOO_SOON_ERROR`      | GTT expiry leaves too little settlement headroom; choose a later expiry                                                                                                                                                                              |
| `EMPTY_MODIFY_ERROR`                | No modifiable value changed                                                                                                                                                                                                                          |

Other request rejections can use `CREATE_ORDER_OTHER_ERROR` / `MODIFY_ORDER_OTHER_ERROR`; read the returned message. `TRIGGER_REQUIRES_GTC_ERROR` belongs to the retired GTC-only trigger contract. New clients must support the chosen-TIF contract above.
