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

# Idempotency and safe retries

> How the Idempotency-Key header makes a retry after a timeout safe, and how to find out what happened to an order.

A network timeout does not tell you whether your order was booked. The `Idempotency-Key` header lets you send the same order again without any risk of booking it twice.

## The rules

`POST /orders` **requires** an `Idempotency-Key` header of 1 to 64 characters. Without it (or with an empty value) the order is refused with `400 idempotency_key_required`; a value longer than 64 characters is refused with `422 invalid_request`.

* **Use a new value per order.** A UUID is a good choice. Save it with your own order record **before** you send the request.
* **Reuse the same value when you retry** that order, after a timeout, a dropped connection, a `429` or a `503`.
* **Same key and same body:** no second order. You get the existing order back, with status `200` and the header `Idempotent-Replayed: true`. The body shows the order's **current** state, which may have moved on since the first answer.
* **Same key, and the hedge refused the order** (production): a replay answers `409 order_rejected` again, with the header `Idempotent-Replayed: true` and the same `detail.order_id` and `detail.order`. Nothing is booked; place a new order with a new key if you still want it.
* **Same key and a different body** (a different `symbol_code`, `trade_type`, `quantity`, `expected_price` or `client_order_id`): refused with `422 idempotency_key_reused`. Nothing is booked.
* **Keys are remembered per account,** not per API key. A retry sent with a new key after a [rotation](/credentials#rotation) still finds the order the old key placed.
* **Two requests at the same moment with the same key** book one order.
* **A retry counts against your order rate limit,** like any `POST /orders`. On `429 rate_limited`, wait the number of seconds in the `Retry-After` header, then retry with the **same** key. Never create a new key to get around a `429`.

## A retry loop

This sends one order and retries it safely. The key is created once, before the first attempt, and every attempt reuses it.

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

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


def place_order(body, idempotency_key, attempts=5):
    """Send one order, retrying with the SAME key until there is a definitive answer."""
    for attempt in range(attempts):
        try:
            r = requests.post(
                f"{BASE}/orders",
                headers={**HEADERS, "Idempotency-Key": idempotency_key},
                json=body,
                timeout=15,
            )
        except requests.RequestException:
            time.sleep(2 ** attempt)  # no answer: the order may or may not exist, so retry
            continue
        if r.status_code in (429, 503):  # not booked yet, or outcome unknown: retry later
            time.sleep(int(r.headers.get("Retry-After", "2")))
            continue
        return r  # 200, 201, 202 or a definitive refusal (4xx)
    raise RuntimeError("no definitive answer yet; look the order up before doing anything else")


key = str(uuid.uuid4())  # save it with your order record first
body = {"symbol_code": "GOLD_TRADE_999", "trade_type": "buy",
        "quantity": 10, "client_order_id": "bot-43"}
r = place_order(body, key)
print(r.status_code, r.json())
```

```json 201 Created theme={null}
{
  "id": "c2e4a6b8-0d1f-4a3c-9e5b-7d9f1b3d5f70",
  "deal_label": "T-13",
  "environment": "test",
  "symbol_code": "GOLD_TRADE_999",
  "trade_type": "buy",
  "quantity": "10.0000",
  "unit": "g",
  "price": "141250.00",
  "total": "141250.00",
  "status": "filled",
  "client_order_id": "bot-43",
  "created_at": "2026-10-14T15:02:11.480000+05:30"
}
```

A `409 booking_conflict` also asks for a retry with the same key. Any other `4xx` is a definitive refusal: fix the request before sending a new order with a new key.

## First answers to a production order

Test orders answer `201` the first time and `200` on a replay. Production orders can answer:

| Status | Meaning | What to do |
| - | - | - |
| `201` | Booked. The order is `open`. | Done. |
| `202` | Accepted and `pending`: Bullionwale's hedge is still confirming it. | [Poll the order](/orders-and-positions#follow-a-pending-order) until it is `open` or `rejected`. |
| `200` | A replay of an earlier request with this key (`Idempotent-Replayed: true`). | Read the order's current `status`. |
| `409 order_rejected` | The hedge refused the order. Nothing was booked. `detail.order_id` and `detail.order` show the rejected order. | Decide whether to place a **new** order, with a new key. |
| `503 booking_unavailable` | The order could not be recorded, so it was rolled back. Nothing was booked. | Retry with the same key. |
| `503 booking_busy` | The booking waited too long behind your account's other bookings and was rolled back. Nothing was booked. | Retry with the same key after `Retry-After`. |
| `503 booking_not_completed` | The booking failed and no booked order was found. | Retry with the same key after `Retry-After` for the definitive answer. |
| `503 booking_outcome_unknown` | The booking failed and its outcome could not be read. | Retry with the same key after `Retry-After` to get it. |

When an order needs hedge confirmation, expect `202` first. A refusal by the hedge then shows up later, as status `rejected` in `GET /orders/{order_id}`, or as `409 order_rejected` when you replay the same key.

This is a production order and its first answer (the request is the same as in test, on the production path with a live key):

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST https://api.bullionwale.com/api/trading/v1/orders \
    -H "X-API-Key: $BULLIONWALE_API_KEY" \
    -H "Idempotency-Key: 4b6d8f0a-2c4e-4f6a-8b0d-2f4a6c8e0b2d" \
    -H "Content-Type: application/json" \
    -d '{"symbol_code": "GOLD_TRADE_999", "trade_type": "buy", "quantity": 10, "client_order_id": "bot-42"}'
  ```

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

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

  idempotency_key = str(uuid.uuid4())  # save it with your order record before sending
  r = requests.post(
      f"{BASE}/orders",
      headers={**HEADERS, "Idempotency-Key": idempotency_key},
      json={"symbol_code": "GOLD_TRADE_999", "trade_type": "buy",
            "quantity": 10, "client_order_id": "bot-42"},
      timeout=15,
  )
  print(r.status_code, r.json())
  ```
</CodeGroup>

<Tabs>
  <Tab title="201 open">
    ```json 201 Created theme={null}
    {
      "id": "3f0c6a52-8a3e-4b8e-9a52-6d2f1c7e9b10",
      "deal_no": 240117,
      "environment": "production",
      "symbol_code": "GOLD_TRADE_999",
      "trade_type": "buy",
      "quantity": "10",
      "unit": "g",
      "price": "141250.00",
      "total": "141250.00",
      "status": "open",
      "status_detail": "fixed",
      "source": "api",
      "client_order_id": "bot-42",
      "close_price": null,
      "realized_pnl": null,
      "created_at": "2026-10-14T15:01:05.120000+05:30",
      "closed_at": null
    }
    ```
  </Tab>

  <Tab title="202 pending">
    ```json 202 Accepted theme={null}
    {
      "id": "3f0c6a52-8a3e-4b8e-9a52-6d2f1c7e9b10",
      "deal_no": 240117,
      "environment": "production",
      "symbol_code": "GOLD_TRADE_999",
      "trade_type": "buy",
      "quantity": "10",
      "unit": "g",
      "price": "141250.00",
      "total": "141250.00",
      "status": "pending",
      "status_detail": "placing",
      "source": "api",
      "client_order_id": "bot-42",
      "close_price": null,
      "realized_pnl": null,
      "created_at": "2026-10-14T15:01:05.120000+05:30",
      "closed_at": null
    }
    ```
  </Tab>

  <Tab title="409 order_rejected">
    ```json 409 Conflict theme={null}
    {
      "detail": {
        "code": "order_rejected",
        "message": "The order could not be confirmed. Please try again.",
        "order_id": "3f0c6a52-8a3e-4b8e-9a52-6d2f1c7e9b10",
        "order": {
          "id": "3f0c6a52-8a3e-4b8e-9a52-6d2f1c7e9b10",
          "deal_no": 240117,
          "environment": "production",
          "symbol_code": "GOLD_TRADE_999",
          "trade_type": "buy",
          "quantity": "10",
          "unit": "g",
          "price": "141250.00",
          "total": "141250.00",
          "status": "rejected",
          "status_detail": "rejected",
          "source": "api",
          "client_order_id": "bot-42",
          "close_price": null,
          "realized_pnl": null,
          "created_at": "2026-10-14T15:01:05.120000+05:30",
          "closed_at": null
        }
      }
    }
    ```
  </Tab>

  <Tab title="503 booking_not_completed">
    ```json 503 Service Unavailable (Retry-After: 2) theme={null}
    {
      "detail": {
        "code": "booking_not_completed",
        "message": "Nothing was booked yet. Retry with the same Idempotency-Key for the definitive outcome."
      }
    }
    ```
  </Tab>
</Tabs>

## Outcomes after a failure

If a production booking fails after you sent it, the answer reflects what was actually **recorded**, never a bare server error:

| What was recorded | Answer |
| - | - |
| The order, `rejected` | `409 order_rejected` with `detail.order_id` and `detail.order`, the same body a replay gets. |
| The order, in any other status | The order itself: `201` (`open`) or `202` (`pending`). |
| No order found | `503 booking_not_completed` with `Retry-After`. Nothing was booked yet. |
| Unknown: the check itself failed | `503 booking_outcome_unknown` with `Retry-After`. |

Treat `booking_not_completed` as "not booked **yet**", not as final. In a rare case (the connection dropped at the moment the order was being saved) the order can exist after all. The same-key retry settles it: it returns the order if it exists, and books it exactly once if it does not.

After either `503`, wait `Retry-After` seconds and retry with the **same** key and the **same** body. If the answer is `422 idempotency_key_reused`, the key belongs to a different order body: check your records.

If, before your retry, your key was revoked or rotated out, API access was turned off, or your account was made inactive, the retry is refused with `401` or `403` (for example `credential_revoked`, `api_access_disabled` or `account_inactive`) **before** the order is looked up. That refusal says nothing about the original order. Find it as described below, with a key that still works, or ask Bullionwale.

## Recovering an order whose outcome you do not know

For a `pending` order, or a `POST` that timed out or lost its connection:

1. **You have the order id** (from a `202` or an earlier answer): poll `GET /orders/{order_id}`. Reads do not use your order budget, and they keep working when production trading is switched off.
2. **You have no id:** list `GET /orders?since=<a moment before you sent it>` and match your `client_order_id`. It is echoed on every order you placed through the API, which is why you should always send one.
3. **Only if the order is still unknown,** replay the `POST` with the **same** key and body. It returns the order if it was booked and books it once if it was not. A replay uses order budget like any `POST`. When production trading is switched off, the replay is refused with `403 production_trading_disabled` like any order; that check happens when your key is authenticated, before any order budget is used. That refusal says nothing about whether the original was booked: go back to step 2.

<CodeGroup>
  ```bash curl theme={null}
  curl -s "https://api.bullionwale.com/api/trading/v1/orders?since=2026-10-14T09:30:00Z&limit=200" \
    -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}/orders", headers=HEADERS,
                   params={"since": "2026-10-14T09:30:00Z", "limit": 200}, timeout=10)
  mine = [o for o in r.json()["items"] if o["client_order_id"] == "bot-42"]
  print(r.status_code, [(o["id"], o["status"]) for o in mine])
  ```
</CodeGroup>


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