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

# Retries and idempotency

> Retry any order safely after a timeout, a dropped connection or a server error.

Networks fail. A request can reach us and place an order, and the response can still be lost on the way back. The Trading API is built so that you can always retry, and never place an order twice by accident.

## The rule

**Retry with the same `client_order_id` and the same body.** Choose a new `client_order_id` only for a new order.

* If the first request placed the order, the retry returns that order with `200` and the header `Idempotent-Replayed: true`. Nothing new is placed.
* If the first request placed nothing, the retry places the order now, with `201`.

Either way, you end up with exactly one order.

<Warning>
  Never retry with a new `client_order_id`. If the first request did place an order, you now have two.
</Warning>

## When to retry

| What happened | What to do |
| - | - |
| Timeout, dropped connection, no response | Retry with the same `client_order_id` |
| `503 unavailable` | Retry with the same `client_order_id`, after a short wait |
| `500 internal_error` | Retry with the same `client_order_id`, after a short wait |
| Any other `4xx` | Don't retry unchanged. Nothing was recorded. Fix the request, then send it again; you can keep the same `client_order_id` |
| `201` or `200` with `status: "pending"` | Don't place it again. Read it with `GET /v1/orders/{id}` |

An error response always means that nothing was recorded. The one exception is `503` and `500`, where the order may have been recorded before the failure, which is exactly the case a retry with the same `client_order_id` handles.

## A retrying client

<CodeGroup>
  ```python Python theme={"system"}
  import os
  import time
  import uuid

  import requests

  API = "https://tradingapi.dev/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['TRADINGAPI_KEY']}"}


  def place_order(body: dict, attempts: int = 5) -> dict:
      """Places one order, retrying safely. body must include client_order_id."""
      for attempt in range(attempts):
          try:
              response = requests.post(f"{API}/orders", json=body, headers=HEADERS, timeout=30)
          except requests.RequestException:
              time.sleep(2**attempt)
              continue  # Same body, same client_order_id.
          if response.status_code in (200, 201):
              return response.json()
          error = response.json()["error"]
          if not error["retryable"]:
              raise RuntimeError(f"{error['code']}: {error['message']} {error.get('hint') or ''}")
          time.sleep(2**attempt)
      raise RuntimeError("Gave up; the order may exist. Look it up by client_order_id.")


  order = place_order({
      "client_order_id": f"rebalance-{uuid.uuid4()}",  # New ID per new order, created once.
      "market": "ETH",
      "side": "buy",
      "type": "market",
      "value": "20.00",
  })
  print(order["summary"])
  ```

  ```javascript JavaScript theme={"system"}
  const API = "https://tradingapi.dev/v1";
  const headers = {
    Authorization: `Bearer ${process.env.TRADINGAPI_KEY}`,
    "Content-Type": "application/json",
  };
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  // Places one order, retrying safely. body must include client_order_id.
  async function placeOrder(body, attempts = 5) {
    for (let attempt = 0; attempt < attempts; attempt++) {
      let response;
      try {
        response = await fetch(`${API}/orders`, { method: "POST", headers, body: JSON.stringify(body) });
      } catch {
        await sleep(1000 * 2 ** attempt);
        continue; // Same body, same client_order_id.
      }
      if (response.status === 200 || response.status === 201) return response.json();
      const { error } = await response.json();
      if (!error.retryable) throw new Error(`${error.code}: ${error.message} ${error.hint ?? ""}`);
      await sleep(1000 * 2 ** attempt);
    }
    throw new Error("Gave up; the order may exist. Look it up by client_order_id.");
  }

  const order = await placeOrder({
    client_order_id: `rebalance-${crypto.randomUUID()}`, // New ID per new order, created once.
    market: "ETH",
    side: "buy",
    type: "market",
    value: "20.00",
  });
  console.log(order.summary);
  ```
</CodeGroup>

Create the `client_order_id` once, before the first attempt, and reuse it for every retry of that order. If your program can restart mid-order, save the ID before sending, so the restarted program retries the same order instead of placing a new one.

## Looking an order up

To check whether a request was recorded without retrying it, look the order up by its `client_order_id`:

```bash theme={"system"}
curl https://tradingapi.dev/v1/orders/by-client-id/rebalance-7f3a \
  -H "Authorization: Bearer $TRADINGAPI_KEY"
```

A `404 order_not_found` means no order was recorded under that ID, so nothing traded. Retrying the original request is still the simplest way to finish: it covers both outcomes in one call.

## Reusing an ID for a different order

A `client_order_id` belongs to one order forever. Sending it with a different body returns `409 client_order_id_reused`. The error names the existing order and lists the fields that differ:

```json theme={"system"}
{
  "error": {
    "type": "conflict",
    "code": "client_order_id_reused",
    "message": "client_order_id \"dca-btc-0927\" already names order ord_7kq2m4x9h3v8r5t2w6y4n8p3za (buy $250.00 of BTC, filled at 14:30 UTC). This request differs in side.",
    "param": "client_order_id",
    "hint": "If you meant a new order, use a new client_order_id. If you are retrying, send the original body unchanged.",
    "details": { "order_id": "ord_7kq2m4x9h3v8r5t2w6y4n8p3za", "differs": ["side"] },
    "errors": [],
    "retryable": false,
    "doc_url": "https://docs.tradingapi.dev/api-reference/errors#client_order_id_reused"
  }
}
```

A rejected order keeps its `client_order_id` too. To try again after a rejection, use a new one.

## Pending orders

A `pending` order is recorded and may already be at the exchange. Don't place it again. Read it until it is final:

```bash theme={"system"}
curl https://tradingapi.dev/v1/orders/ord_5nd2r7k4x9w3m5t2v6y4q8p3zc \
  -H "Authorization: Bearer $TRADINGAPI_KEY"
```

Most orders are final within a second. An order still `pending` after a minute is waiting for the exchange to confirm what happened. It becomes final on its own, and we never guess the outcome.
