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

# Working with limit orders

> Choose a trigger price, place a limit order safely, watch it, edit or cancel it, and find the order it became.

A limit order waits for a price instead of trading now: buy when your price falls to a level, or sell when it rises to one. This guide walks through a limit order's life in the **test environment**, where nothing is booked, then explains what changes in production. The fields, statuses and every answer are on the [Limit orders](/api-reference/limit-orders/overview) reference page.

> [!NOTE]
>
> Test limit orders are checked like real ones but **never trigger**: they rest until you cancel them. Production limit orders are real, and production trading is switched off today, so production placements, edits and cancels are refused with `403 production_trading_disabled`. Reads work in both environments.

You need a test key with `orders:write` (to place, edit and cancel) and `orders:read` (to read limit orders back), plus `rates:read` to look up your price first. See the [Scope reference](/scopes).

## 1. Choose a trigger price

A limit order must **wait**. Look up your current price for the symbol with `GET /rates/{symbol_code}` (see [Market data](/api-reference/rates/overview)), then pick a trigger on the far side of it:

| Side | Trigger must be | Triggers when |
| - | - | - |
| `buy` | **below** your Buy price (`buy`) | your Buy price falls to the trigger or below |
| `sell` | **above** your Sell price (`sell`) | your Sell price rises to the trigger or above |

A trigger the market has already reached is refused with `400 limit_would_execute`: use a market order (`POST /orders`) to trade at the current price instead.

`trigger_price` is per the symbol's quoted measure (per 10 grams for gold, per kilogram for silver), and `quantity` is a weight in the symbol's measure, as for market orders. See [Units and pricing](/units-and-pricing).

## 2. Place it, with an Idempotency-Key

Send `POST /limit-orders` with a new `Idempotency-Key` (a UUID) and store the key with your own record **before** you send it. If the request times out, send it again with the **same** key and body: you get the same limit order back (`200`, `Idempotent-Replayed: true`), never a second one. Limit-order keys are separate from market-order keys.

## 3. Watch it

Read it with `GET /limit-orders/{limit_order_id}`, or list the resting ones with `GET /limit-orders?status=pending`. While it is `pending`, `actions` is `["edit", "cancel"]`.

In production, also read `blocked_reason`. When it is not `null`, the limit order would **not** be booked if the price arrived now, for example because your weight allocation has no room for it. It is worked out again on every read, so it clears by itself once the cause is resolved.

## 4. Edit or cancel it

* **Edit** with `PATCH /limit-orders/{limit_order_id}` and a new `trigger_price`, `quantity`, or both. The new values face the placement's rules; a larger quantity must also fit your weight allocation now.
* **Cancel** with `DELETE /limit-orders/{limit_order_id}`. It answers the limit order, `cancelled`.

Both work only on a `pending` limit order.

This script runs steps 1 to 4 against the test environment: it reads your Buy price, places a buy limit 10% below it, reads it back, lowers its trigger, and cancels it.

```python Python theme={null}
import os
import uuid
from decimal import ROUND_DOWN, Decimal

import requests

BASE = "https://api.bullionwale.com/api/trading/test/v1"
HEADERS = {"X-API-Key": os.environ["BULLIONWALE_API_KEY"]}
SYMBOL = "GOLD_TRADE_999"

# 1. Your current Buy price; a buy limit has to wait below it.
rate = requests.get(f"{BASE}/rates/{SYMBOL}", headers=HEADERS, timeout=10).json()
if rate["buy"] is None:
    raise SystemExit("no Buy price right now; try again later")
buy_now = Decimal(rate["buy"])
trigger = (buy_now * Decimal("0.90")).quantize(Decimal("1"), rounding=ROUND_DOWN)

# 2. Place it. Store the key first, and reuse it on every retry of this placement.
idempotency_key = str(uuid.uuid4())
r = requests.post(
    f"{BASE}/limit-orders",
    headers={**HEADERS, "Idempotency-Key": idempotency_key},
    json={"symbol_code": SYMBOL, "trade_type": "buy", "quantity": 10,
          "trigger_price": float(trigger), "client_order_id": "guide-limit-1"},
    timeout=10,
)
r.raise_for_status()
limit_order = r.json()
limit_order_id = limit_order["id"]
print("placed", r.status_code, limit_order["status"], limit_order["trigger_price"])

# 3. Watch it.
lo = requests.get(f"{BASE}/limit-orders/{limit_order_id}", headers=HEADERS, timeout=10).json()
print("now", lo["status"], lo["actions"])

# 4. Lower the trigger by 1%, then cancel it.
lower = (trigger * Decimal("0.99")).quantize(Decimal("1"), rounding=ROUND_DOWN)
edited = requests.patch(f"{BASE}/limit-orders/{limit_order_id}", headers=HEADERS,
                        json={"trigger_price": float(lower)}, timeout=10).json()
print("edited", edited["trigger_price"], edited["total"])

cancelled = requests.delete(f"{BASE}/limit-orders/{limit_order_id}", headers=HEADERS,
                            timeout=10).json()
print("cancelled", cancelled["status"], cancelled["actions"])
```

## 5. When it triggers (production)

When your price reaches the trigger, Bullionwale books a market order **at that moment's price**, so a buy can book below your trigger and a sell above it if the price moved past it between checks. The limit order becomes `triggered`, and its `order_id` names the order: read it with `GET /orders/{order_id}` for its `deal_no`, booked `price` and `total`.

Your weight allocation is checked at that moment, not when you placed the limit order. If it has no room then (or the symbol has stopped trading, or the quantity no longer meets the rules, or there is no stock), the limit order is **cancelled** with the reason in `cancel_reason`, for example `not enough free limit when the price triggered`. It does not wait and try again: place a new limit order once the cause is resolved.

A trading halt, a frozen price or a missing live price does not cancel it: it keeps resting and triggers once the price is live again.

## Handling the answers safely

| You get | What it means | What to do |
| - | - | - |
| `201` on `POST` | Placed, `pending`. | Store the `id`. |
| `202` on `POST` (production) | `confirming`: Bullionwale's hedge is confirming it. | Read it every few seconds until it is `pending` (or `cancelled`). Do **not** place it again with a new key. |
| `409 limit_order_rejected` on `POST` (production) | It was not placed. `detail.limit_order` shows it, `cancelled`. | Decide whether to place a new one, with a **new** key. A retry with the same key answers `409` again. |
| A timeout or `503 booking_…` on `POST` | You do not know whether it was placed. | Send the same request with the **same** key. The answer is what was committed. |
| `409 limit_order_confirming` on `PATCH` or `DELETE` | It is `confirming`. | Read it again after a few seconds, then retry. |
| `202` on `DELETE` (production) | The cancel is settling. | Read it until it is `cancelled`. |
| `503 limit_cancel_failed` on `DELETE` | The cancel did not complete: it is **still pending and can still trigger**. | Wait `Retry-After` seconds, then send the cancel again. |
| `409 limit_order_not_pending` on `DELETE` | It is already `triggered` or `cancelled`. | Read it: if it is `triggered`, an order was booked; `order_id` names it. |

A cancel that arrives just as the price triggers is settled one way or the other, never both: the limit order is either `cancelled` or `triggered` with its order.

## Test and production

| | Test | Production |
| - | - | - |
| Triggers | Never. It rests until you cancel it. | When your price reaches the trigger. |
| Statuses | `pending`, `cancelled` | `pending`, `confirming`, `triggered`, `cancelled` |
| First answer to `POST /limit-orders` | `201` | `201`, `202`, `409 limit_order_rejected`, or a `503 booking_…` answer after a failure |
| `GET /limit-orders` | Your test limit orders | Your limit orders from every channel (app, web and API); `source` says which came from the API |
| Weight check on an edit | Against your test book | Against your real position |
| Webhook events | None | `limit_order.placed`, `limit_order.triggered`, `limit_order.cancelled` (see the [Event catalog](/webhooks/event-catalog)) |
| Production switches | Not involved | Must be on; today every write answers `403 production_trading_disabled` |


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