> ## 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.

# Limit orders

> Place a buy or sell limit that waits for a price, follow it, edit its price or quantity, and cancel it.

A limit order is a resting instruction: buy (or sell) a weight of one symbol **when your price reaches** your `trigger_price`. When it does, Bullionwale books it as a market order at that moment's price, if your weight allocation still has room for it then. You place it with `POST /limit-orders`, follow it with `GET /limit-orders/{limit_order_id}`, list your limits with `GET /limit-orders`, change it with `PATCH /limit-orders/{limit_order_id}` and cancel it with `DELETE /limit-orders/{limit_order_id}`. For a walkthrough, see the [Limit orders guide](/limit-orders).

| Call | Endpoint | Scope |
| - | - | - |
| [Place a limit order](#place-a-limit-order) | `POST /limit-orders` | `orders:write` |
| [List limit orders](#list-your-limit-orders) | `GET /limit-orders` | `orders:read` |
| [One limit order](#read-one-limit-order) | `GET /limit-orders/{limit_order_id}` | `orders:read` |
| [Edit a limit order](#edit-a-limit-order) | `PATCH /limit-orders/{limit_order_id}` | `orders:write` |
| [Cancel a limit order](#cancel-a-limit-order) | `DELETE /limit-orders/{limit_order_id}` | `orders:write` |

Each path sits under the test base path `/api/trading/test/v1` or the production base path `/api/trading/v1`:

| Call | Test | Production |
| - | - | - |
| Place | `POST /api/trading/test/v1/limit-orders` | `POST /api/trading/v1/limit-orders` |
| List | `GET /api/trading/test/v1/limit-orders` | `GET /api/trading/v1/limit-orders` |
| One | `GET /api/trading/test/v1/limit-orders/{limit_order_id}` | `GET /api/trading/v1/limit-orders/{limit_order_id}` |
| Edit | `PATCH /api/trading/test/v1/limit-orders/{limit_order_id}` | `PATCH /api/trading/v1/limit-orders/{limit_order_id}` |
| Cancel | `DELETE /api/trading/test/v1/limit-orders/{limit_order_id}` | `DELETE /api/trading/v1/limit-orders/{limit_order_id}` |

**Test limit orders are simulated and never execute**: they are checked exactly like real ones, then rest until you cancel them. Production limit orders are **real**, and production trading is switched off today: every production write answers `403 production_trading_disabled` (see [Production access requirements](/production-access)). Reads keep working in both environments.

Limit orders are offered per platform. Where they are not offered, every call on this page answers `404`.

## The limit order object

Both environments return these fields:

| Field | Meaning |
| - | - |
| `id` | The limit order's id (a UUID). Use it with the calls above. |
| `environment` | `test` or `production`. |
| `symbol_code`, `trade_type` | What you sent. |
| `quantity` | The weight, as a string, in `unit`. |
| `unit` | `g` or `kg`: the symbol's measure. |
| `trigger_price` | The price that triggers it, per the symbol's quoted measure, in rupees, as a string. |
| `total` | The order's value **at** `trigger_price`, in rupees, as a string. The booked order is priced when it executes, so its own `total` can differ. |
| `status` | See [Statuses](#statuses). |
| `status_detail` | Bullionwale's internal status name, for support conversations. Branch on `status`, not on this. |
| `actions` | What you can do with it now: `["edit", "cancel"]` while `pending`, otherwise `[]`. |
| `cancel_reason` | Why it was cancelled when Bullionwale cancelled it, or it could not be booked when it triggered. `null` when you cancelled it yourself, and always `null` in test. |
| `client_order_id` | Your reference, if you sent one. |
| `order_id` | Once it is `triggered`: the id of the market order it became, for `GET /orders/{order_id}`. Otherwise `null`. |
| `created_at` | When it was placed, as an ISO 8601 time with its UTC offset. |

Production limit orders add:

| Field | Meaning |
| - | - |
| `source` | `api` for a limit order placed through the API; `null` for one placed in the app or on the web. |
| `blocked_reason` | Why it would not execute if its price were reached now (for example, not enough free weight allocation), or that it is being confirmed or cancelled. Worked out again on every read, so it clears by itself. Otherwise `null`. |

## Statuses

| `status` | Meaning | `actions` |
| - | - | - |
| `pending` | Resting: it executes when your price reaches `trigger_price`. | `edit`, `cancel` |
| `confirming` | Production only. Bullionwale's hedge is still being placed for it, or being cancelled. It settles by itself within a couple of minutes, to `pending` or `cancelled`. | none |
| `triggered` | Production only. Your price reached it and it was booked: `order_id` names the order. | none |
| `cancelled` | Cancelled by you, by Bullionwale, or because it could not be booked when it triggered (`cancel_reason` says why). | none |

A production limit order moves `pending` → `triggered` or `pending` → `cancelled`. When Bullionwale hedges it, it is `confirming` first while it is being placed, and again while it is being cancelled. A test limit order is only ever `pending` or `cancelled`.

**Edit and cancel only a `pending` limit order.** A `confirming` one answers `409 limit_order_confirming`: read it again after a few seconds. A `triggered` or `cancelled` one answers `409 limit_order_not_pending`.

## How a limit order triggers

Production limit orders are checked against the live price every few seconds:

| Side | Triggers when |
| - | - |
| `buy` | **Your Buy price** falls to `trigger_price` or below. |
| `sell` | **Your Sell price** rises to `trigger_price` or above. |

"Your price" is the price you see in [Market data](/api-reference/rates/overview), with your account's pricing applied. When it triggers, the order is booked **at that moment's price**, not at `trigger_price`: if the price moved past your trigger between two checks, a buy books at the lower price and a sell at the higher one.

Your weight allocation is **not reserved** while a limit order rests. It is checked when the order triggers, together with the other conditions a market order must meet then: trading on for the symbol and for your account, the quantity rules, and stock. If any of them fails at that moment, the limit order is **cancelled**, not kept waiting, and `cancel_reason` says why, for example `not enough free limit when the price triggered`. Place it again once the reason is resolved. While it rests, `blocked_reason` warns you in advance when one of these would stop it.

Some conditions only make it wait, without cancelling it: no live or confirmed price for the symbol, a frozen price, or a trading halt. It keeps resting and triggers once the price is live again.

## Place a limit order

`POST /limit-orders` takes an `Idempotency-Key` header (required: 1 to 64 characters, a new UUID per limit order, reused unchanged on every retry; see [Idempotency and safe retries](/idempotency)). Keys for limit orders are separate from keys for market orders, and are remembered per account.

| Field | Type | Required | Meaning |
| - | - | - | - |
| `symbol_code` | string | yes | The symbol, for example `GOLD_TRADE_999`. Up to 32 characters. |
| `trade_type` | string | yes | `buy` or `sell`. |
| `quantity` | number | yes | A **weight** in the symbol's measure. Greater than 0, at most 4 decimal places. |
| `trigger_price` | number | yes | The price to wait for, per the symbol's quoted measure. A buy limit must be **below** your current Buy price and a sell limit **above** your current Sell price; otherwise it would execute at once and is refused with `400 limit_would_execute`. |
| `client_order_id` | string | no | Your own reference, up to 64 characters, echoed back. |

Placement runs the same symbol, side, trading-halt, quantity and pricing checks as a market order, except your weight allocation and stock, which are checked when it triggers.

```bash curl theme={null}
curl -s -X POST https://api.bullionwale.com/api/trading/test/v1/limit-orders \
  -H "X-API-Key: $BULLIONWALE_API_KEY" \
  -H "Idempotency-Key: 0b6d3f2a-9c41-4e7a-b8d5-3a1f6e2c9d74" \
  -H "Content-Type: application/json" \
  -d '{"symbol_code": "GOLD_TRADE_999", "trade_type": "buy", "quantity": 10, "trigger_price": 100000, "client_order_id": "bot-limit-7"}'
```

```python Python theme={null}
import os
import uuid
import requests

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

idempotency_key = str(uuid.uuid4())  # store it with the limit order; reuse it on every retry
r = requests.post(
    f"{BASE}/limit-orders",
    headers={**HEADERS, "Idempotency-Key": idempotency_key},
    json={"symbol_code": "GOLD_TRADE_999", "trade_type": "buy", "quantity": 10,
          "trigger_price": 100000, "client_order_id": "bot-limit-7"},
    timeout=10,
)
print(r.status_code, r.json())
```

```json 201 Created theme={null}
{
  "id": "5c9e2a71-0b3f-4d8e-a6c2-9f1e7d3b4a60",
  "environment": "test",
  "symbol_code": "GOLD_TRADE_999",
  "trade_type": "buy",
  "quantity": "10",
  "unit": "g",
  "trigger_price": "100000.00",
  "total": "100000.00",
  "status": "pending",
  "status_detail": "pending",
  "actions": ["edit", "cancel"],
  "cancel_reason": null,
  "client_order_id": "bot-limit-7",
  "order_id": null,
  "created_at": "2026-10-21T15:01:05.120000+05:30"
}
```

A trigger the market has already reached is refused, and the message names your current price:

```json 400 Bad Request theme={null}
{
  "detail": {
    "code": "limit_would_execute",
    "message": "A buy limit has to be BELOW the current price of 141,250.00 — at 141,500.00 it would execute immediately. Lower the limit, or use Buy to trade at the market price now."
  }
}
```

The production request is the same, on `/api/trading/v1/limit-orders` with a live key. What comes back differs:

| Answer | Test | Production |
| - | - | - |
| `201` | The limit order, `pending`. | Placed: `pending`. |
| `202` | Never. | Accepted and `confirming` while Bullionwale's hedge confirms it. Read it again until it is `pending`. |
| `200` | A replay of an earlier request with the same `Idempotency-Key` and body (`Idempotent-Replayed: true`). It shows the limit order's current state. | The same. |
| `409 limit_order_rejected` | Never. | It was not placed: the hedge refused it. `detail.limit_order` shows it, `cancelled`. A replay answers `409` again. |
| `503 booking_…` | Never. | The placement failed; retry with the same key (see [Outcomes after a failure](/idempotency#outcomes-after-a-failure)). |

```json 202 Accepted theme={null}
{
  "id": "8a4d1f62-3c7e-4b90-b5e1-2d6f0a9c8e17",
  "environment": "production",
  "symbol_code": "GOLD_TRADE_999",
  "trade_type": "buy",
  "quantity": "10",
  "unit": "g",
  "trigger_price": "100000.00",
  "total": "100000.00",
  "status": "confirming",
  "status_detail": "placing",
  "actions": [],
  "cancel_reason": null,
  "client_order_id": "bot-limit-7",
  "order_id": null,
  "source": "api",
  "blocked_reason": "Being confirmed — this limit is not active yet.",
  "created_at": "2026-10-21T15:01:05.120000+05:30"
}
```

```json 409 Conflict theme={null}
{
  "detail": {
    "code": "limit_order_rejected",
    "message": "The limit order could not be confirmed. Please try again.",
    "limit_order_id": "8a4d1f62-3c7e-4b90-b5e1-2d6f0a9c8e17",
    "limit_order": {
      "id": "8a4d1f62-3c7e-4b90-b5e1-2d6f0a9c8e17",
      "environment": "production",
      "symbol_code": "GOLD_TRADE_999",
      "trade_type": "buy",
      "quantity": "10",
      "unit": "g",
      "trigger_price": "100000.00",
      "total": "100000.00",
      "status": "cancelled",
      "status_detail": "cancelled",
      "actions": [],
      "cancel_reason": "The hedge for this limit order could not be placed.",
      "client_order_id": "bot-limit-7",
      "order_id": null,
      "source": "api",
      "blocked_reason": null,
      "created_at": "2026-10-21T15:01:05.120000+05:30"
    }
  }
}
```

## Read one limit order

`GET /limit-orders/{limit_order_id}` returns one of your limit orders with its current `status`. One that is not on your account, or not in this environment, answers `404 limit_order_not_found`. This example places a test limit order and reads it back:

```python Python theme={null}
import os
import uuid
import requests

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

placed = requests.post(
    f"{BASE}/limit-orders",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={"symbol_code": "GOLD_TRADE_999", "trade_type": "buy", "quantity": 10,
          "trigger_price": 100000},
    timeout=10,
).json()

r = requests.get(f"{BASE}/limit-orders/{placed['id']}", headers=HEADERS, timeout=10)
lo = r.json()
print(r.status_code, lo["status"], lo["actions"])   # 200 pending ['edit', 'cancel']
```

Once a production limit order is `triggered`, read the order it became with `GET /orders/{order_id}`, using its `order_id`.

## List your limit orders

`GET /limit-orders` takes `status` (`pending`, `triggered`, `cancelled` or `all`, the default), `limit` (1 to 200, default 50), `offset`, `symbol_code`, `trade_type` and `since` (ISO 8601; a time without a zone is UTC), and returns `items` newest first plus `total`, the number of matching limit orders. `status=pending` is the resting queue, `confirming` limit orders included.

In production it lists your account's limit orders from **every** channel: the app, the web and the API. `source` says which were placed through the API, and only those carry your `client_order_id`. In test it lists your test limit orders.

```bash curl theme={null}
curl -s "https://api.bullionwale.com/api/trading/v1/limit-orders?status=pending&limit=20" \
  -H "X-API-Key: $BULLIONWALE_API_KEY"
```

```python Python theme={null}
import os
import requests

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

r = requests.get(f"{BASE}/limit-orders", headers=HEADERS,
                 params={"status": "pending", "limit": 20}, timeout=10)
page = r.json()
print(r.status_code, page["total"], [(lo["id"], lo["blocked_reason"]) for lo in page["items"]])
```

```json 200 OK theme={null}
{
  "items": [
    {
      "id": "8a4d1f62-3c7e-4b90-b5e1-2d6f0a9c8e17",
      "environment": "production",
      "symbol_code": "GOLD_TRADE_999",
      "trade_type": "buy",
      "quantity": "10",
      "unit": "g",
      "trigger_price": "140500.00",
      "total": "140500.00",
      "status": "pending",
      "status_detail": "pending",
      "actions": ["edit", "cancel"],
      "cancel_reason": null,
      "client_order_id": "bot-limit-7",
      "order_id": null,
      "source": "api",
      "blocked_reason": "Not enough free limit for this quantity — contact the desk to increase it, and this will execute automatically.",
      "created_at": "2026-10-21T15:01:05.120000+05:30"
    }
  ],
  "total": 1
}
```

## Edit a limit order

`PATCH /limit-orders/{limit_order_id}` takes a new `trigger_price`, a new `quantity`, or both. A body with neither is refused with `422 invalid_request`. The new values face the placement's rules: the price must still wait (`400 limit_would_execute`), the quantity must meet the quantity rules, and a **larger** quantity must fit your weight allocation now (`400 limit_exceeded`; in test, against your test book). It answers the limit order with its new values.

`PATCH` sets absolute values, so sending the same edit twice leaves the same result. It takes no `Idempotency-Key`.

```json 200 OK theme={null}
{
  "id": "5c9e2a71-0b3f-4d8e-a6c2-9f1e7d3b4a60",
  "environment": "test",
  "symbol_code": "GOLD_TRADE_999",
  "trade_type": "buy",
  "quantity": "5",
  "unit": "g",
  "trigger_price": "99500.00",
  "total": "49750.00",
  "status": "pending",
  "status_detail": "pending",
  "actions": ["edit", "cancel"],
  "cancel_reason": null,
  "client_order_id": "bot-limit-7",
  "order_id": null,
  "created_at": "2026-10-21T15:01:05.120000+05:30"
}
```

## Cancel a limit order

`DELETE /limit-orders/{limit_order_id}` cancels a `pending` limit order and answers it, `cancelled`. In production:

| Answer | Meaning |
| - | - |
| `200` | Cancelled. |
| `202` | `confirming`: the hedge is being cancelled. Read it again until it is `cancelled`. |
| `503 limit_cancel_failed` | The hedge did not cancel, so the limit order is still `pending` and can still trigger. Wait `Retry-After` seconds and send the cancel again. |

```json 503 Service Unavailable theme={null}
{
  "detail": {
    "code": "limit_cancel_failed",
    "message": "The cancel could not be completed, so the limit order is still pending. Retry the cancel shortly."
  }
}
```

A cancel that arrives as the price triggers is settled one way or the other: either the cancel wins and the limit order is `cancelled`, or the order is booked first and the cancel answers `409 limit_order_not_pending`. After a `409`, read the limit order: if it is `triggered`, `order_id` names the order that was booked.

```json 409 Conflict theme={null}
{
  "detail": {
    "code": "limit_order_not_pending",
    "message": "This limit order is no longer pending: it was triggered or cancelled."
  }
}
```

This example places a test limit order, moves its price, then cancels it:

```python Python theme={null}
import os
import uuid
import requests

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

placed = requests.post(
    f"{BASE}/limit-orders",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={"symbol_code": "GOLD_TRADE_999", "trade_type": "buy", "quantity": 10,
          "trigger_price": 100000},
    timeout=10,
).json()
limit_order_id = placed["id"]

edited = requests.patch(f"{BASE}/limit-orders/{limit_order_id}", headers=HEADERS,
                        json={"trigger_price": 99500}, timeout=10).json()
print(edited["trigger_price"])            # "99500.00"

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

## Events

Production limit orders send the webhook events `limit_order.placed` (it is resting, after any confirmation), `limit_order.triggered` (it was booked; the event names the order) and `limit_order.cancelled`. Test limit orders send none. See the [Event catalog](/webhooks/event-catalog).

## Common refusals

| Code | Status | Meaning |
| - | - | - |
| `idempotency_key_required` | 400 | `POST /limit-orders` without an `Idempotency-Key` header. |
| `idempotency_key_reused` | 422 | The `Idempotency-Key` was used for a different limit order body. Nothing was placed. |
| `limit_would_execute` | 400 | The trigger price is at or past your current price. |
| `limit_exceeded` | 400 | An edit's larger quantity does not fit your weight allocation. |
| `quantity_rule` | 400 | The quantity breaks the symbol's size rule. |
| `unknown_symbol` | 404 | No such symbol. |
| `trading_halted` | 403 | Bullionwale has halted trading: no new limit orders until it resumes. |
| `limit_order_not_found` | 404 | No such limit order on your account in this environment. |
| `limit_order_not_pending` | 409 | It was already triggered or cancelled. |
| `limit_order_confirming` | 409 | Production only: it is `confirming`; retry once it is `pending`. |
| `limit_order_rejected` | 409 | Production only: it was not placed (the hedge refused it). |
| `limit_cancel_failed` | 503 | Production only: the cancel did not complete; it is still `pending`. |
| `stale_rate` | 503 | The live price cannot be trusted right now (for example, the feed is paused). Retry shortly. |
| `production_trading_disabled` | 403 | Production only: production trading is not available to you right now. |

The full list, with what to do for each, is in the [Error reference](/api-reference/error-reference).


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