# Client order ID: idempotent orders on exchanges and FIX

URL: https://fincodelabs.com/en/blog/client-order-id · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-11-06 · Updated: 2026-11-06

Client order ID rules compared: Binance, Bybit, Coinbase, Kraken, OKX, Hyperliquid, Alpaca and FIX tag 11, the retry trap after a fill, and tested Python.

**Short answer:** A **client order ID** is a label your software assigns to an order before sending it, so that after a timeout it can ask the venue whether the order exists instead of sending it twice. In FIX it is ClOrdID, tag 11. The label alone does not make a retry safe: as documented on 5 November 2026, Coinbase returns the existing order for a repeated ID, while Binance, Kraken and OKX enforce uniqueness only among open orders, so a retry after the first order has filled can create a second order.

If an order request times out, can the software send it again without creating a second order? The answer differs by venue. Order states are in the [crypto order management system](https://fincodelabs.com/en/blog/crypto-order-management-system) guide and the general timeout sketch in [trading bot development](https://fincodelabs.com/en/blog/trading-bot-development).

All rules were read on official pages on 5 November 2026. Format checks ran on the public Binance Spot Testnet and one lookup on the Hyperliquid testnet, without real keys, so the retry trap ran on a synthetic venue that applies the documented rules. Naming a venue is not a recommendation. Automated trading can lose money, and rules depend on your jurisdiction and the platform's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## Client order ID FIX tag: ClOrdID (11), OrigClOrdID (41), OrderID (37)

In FIX the client order ID is ClOrdID, tag 11; tag 41 points at the order being canceled or replaced, and tag 37 is the venue's own ID.

**FIX Latest text, read through the FIX Trading Community's FIXimate on 5 November 2026**

| Tag | Field | Assigned by | Rule |
| ---: | --- | --- | --- |
| 11 | ClOrdID | Buy side | Uniqueness must be guaranteed within a single trading day; firms with multi-day orders should ensure it across days, for example by embedding a date |
| 41 | OrigClOrdID | Buy side | ClOrdID of the previous non-rejected order, not the first order of the day, when canceling or replacing |
| 37 | OrderID | Sell side | Identifier of the most recent order, assigned by the broker or exchange |

So each replace carries a fresh tag 11 and links back through tag 41 to the previous step, not to the original order. The standard puts the uniqueness duty on the sender and names the rejection, OrdRejReason (103) = 6, Duplicate Order; DuplicateClOrdIDIndicator (2829) marks an intentional duplicate. The rest is left to each venue. Kraken, for example, extends its uniqueness check across open orders and the FIX session, and requires FIX clients to send ever-increasing numbers or timestamp-first UUIDs. Session mechanics are in [broker API integration](https://fincodelabs.com/en/blog/broker-api-integration-fix-rest).

## Idempotent order creation: client order ID rules by venue

Idempotent order creation means a repeated request cannot create a second order, and only some venues document that behavior.

**Official documents read 5 November 2026 (alphabetical, not a ranking); the last column is a reading of those rules, not a test**

| Venue and field | Format | Documented uniqueness | Repeated ID | Retry after a fill |
| --- | --- | --- | --- | --- |
| Alpaca `client_order_id` | Up to 128 characters; generated if omitted | "A unique identifier"; an Alpaca article of 4 March 2026 says "each active order" | HTTP 422, code `40010001`, client_order_id must be unique | Not stated |
| Binance Spot `newClientOrderId` | `^[a-zA-Z0-9-_]{1,36}$` | Among open orders; accepted again once the previous order is filled | `-2010` Duplicate order sent. | ID is free: second order |
| Binance USD-M `newClientOrderId` | `^[\.A-Z\:/a-z0-9_-]{1,36}$` | Among open orders | `-4116` clientOrderId is duplicated | ID is free: second order |
| Bybit `orderLinkId` | Up to 36: letters, digits, dash, underscore | "Always unique" on futures, perps and spot; period not stated | `110072` (UTA), `170141` (spot) | Rejected, if "always" has no limit |
| Coinbase Advanced Trade `client_order_id` | Required string | "A unique ID"; period not stated | No new order; the existing one is returned | Existing order returned |
| FIX `ClOrdID` | String | Within a trading day | `OrdRejReason` (103) = 6, where the venue enforces it | Venue's rules |
| Hyperliquid `cloid` | Optional 128-bit hex, such as `0x1234567890abcdef1234567890abcdef` | Not stated | Not stated | Not stated |
| Kraken Spot `cl_ord_id` | Long UUID, 32-hex short UUID, or ASCII text up to 18 characters | Open orders per client, plus the FIX session | Not stated in the guide | ID is free: second order |
| OKX `clOrdId` | Up to 32 alphanumerics | Pending orders; reusable after filled or canceled | `51016` Client order ID already exists. | ID is free: second order |

Kraken's `userref` is a non-unique integer, so it cannot do this job. Hyperliquid has a different guard: its nonce page says a signer's nonce must never have been used before, so an identical signed message should not execute twice (a reading, not a test), and it warns that nonce state can be pruned after an API wallet is deregistered.

Binance Spot's wording leaves a gap: one sentence says unique among open orders, the next says accepted only when the previous one is filled, and the order-list endpoints say filled or completely expired. The page does not settle what happens after a plain cancel, so do not build on reuse.

## Client order ID on Binance: a retry after a fill can create a second order

A check among open orders only rejects a retry while the first order is still open, so an order that filled during the timeout leaves nothing to collide with.

> **Hypothetical timeline.** A bot sends a market buy of 0.5 with ID A, and the exchange fills it at once. The reply is lost, and the HTTP client times out. The bot resends with ID A, trusting the ID to protect it. Order A is no longer open, so the duplicate check passes. The position is now 1.0 instead of 0.5.

Binance's Spot documents say APIs time out after 10 seconds waiting for the matching engine, and an HTTP 5XX status means the outcome is unknown and could have been a success. The trap needs a signed order, so it was not run against the Spot Testnet; the last section gives the test to run with your own key.

## How to find out what happened after a timeout

Ask by client order ID first, and treat a not-found answer as provisional, because every venue's lookup has a blind spot.

**Lookup by client order ID, documents read 5 November 2026**

| Venue | Lookup | Blind spot |
| --- | --- | --- |
| Alpaca | `GET /v2/orders:by_client_order_id` | Not stated |
| Binance Spot | `GET /api/v3/order` with `origClientOrderId`, weight 4 | The API system is asynchronous, with some delay normal |
| Binance USD-M | `GET /fapi/v1/order` with `origClientOrderId`, weight 1 | Canceled or expired orders without fills are not found after 3 days, any order after 90 |
| Bybit | `orderLinkId` on `GET /v5/order/realtime` | Latest 500 closed orders; after a server restart Unified accounts must use history, with the windows in [position reconciliation](https://fincodelabs.com/en/blog/position-reconciliation) |
| Coinbase | Repeat the create request with the same ID; Get Order marks its `client_order_id` parameter deprecated | How long IDs are remembered; a repeat creates the order if the first never arrived |
| Hyperliquid | `POST /info`, type `orderStatus`, with the cloid in `oid` | The testnet returned `{"status":"unknownOid"}` for an unused ID on 5 November 2026; a malformed ID got HTTP 422 |
| Kraken | `cl_ord_id` on open and closed orders | Query Orders Info marks `txid` as required, which a lost reply never gave you |
| OKX | `GET /api/v5/trade/order` with `clOrdId` | Returns only the latest match if an ID was reused |

In FIX the equivalent is OrderStatusRequest (35=H), and PossResend (97) flags a message that may have gone under another sequence number. Check the private stream too, wait out the venue's own timeout and query more than once. Only then resend, with the same ID rather than a new one: if the first order surfaces while still open, the venue rejects the repeat.

## Designing IDs that survive a restart

Derive the ID from the intent and store it before sending, so a restarted process regenerates the same ID instead of inventing a new one.

- **Format.** 32 hex characters fit every format in the table above, with `0x` added for Hyperliquid. A dashed UUID fails OKX's alphanumeric rule, and Kraken caps free text at 18 characters, though its own example has 19.
- **Measured.** On the Spot Testnet on 5 November 2026, a 32-hex ID and a 36-character UUID passed the format check and stopped at the key check with `-2015`. A 37-character ID and `a:b` returned `-1100`, whose message gives the pattern quoted above, so USD-M's extra characters do not carry over to Spot.
- **Time.** For FIX orders that live across days, add a date, and check the venue's rule first: a hash does not meet Kraken's FIX rule.

## Tested Python: persist before send, query before retry

This sketch runs without a network: a synthetic venue accepts the order, loses the reply and fills it, while SQLite keeps each intent across a simulated crash.

```python
import hashlib, re, sqlite3

def make_id(bot, intent_seq):
    """Same intent, same ID, even after a restart: 32 hex characters."""
    return hashlib.sha256(f"{bot}:{intent_seq}".encode()).hexdigest()[:32]

FORMATS = {                               # rules read 5 November 2026
    "Binance Spot": r"[a-zA-Z0-9_-]{1,36}",
    "Binance USD-M": r"[.A-Z:/a-z0-9_-]{1,36}",
    "Bybit": r"[A-Za-z0-9_-]{1,36}",
    "Kraken short UUID": r"[0-9a-f]{32}",
    "OKX": r"[A-Za-z0-9]{1,32}",
}
cid = make_id("bot1", 42)
print(cid, "same after restart:", cid == make_id("bot1", 42))
print("valid on all:", all(re.fullmatch(p, cid) for p in FORMATS.values()))

class Venue:
    """Synthetic. 'open' follows Binance Spot's text, 'always' Coinbase's."""
    def __init__(self, scope):
        self.scope, self.orders = scope, []
    def place(self, cid, lose_reply=False):
        same = [o for o in self.orders if o["cid"] == cid]
        if same and self.scope == "always":
            return same[-1]                   # nothing new is created
        if any(o["status"] == "NEW" for o in same):
            raise ValueError("Duplicate order sent.")
        self.orders.append({"cid": cid, "status": "NEW"})
        if lose_reply:
            raise TimeoutError                # accepted, reply lost
        return self.orders[-1]
    def query(self, cid):
        return next((o for o in self.orders if o["cid"] == cid), None)
    def fill_all(self):
        for o in self.orders:
            o["status"] = "FILLED"

def blind_retry(venue, cid):
    try:
        venue.place(cid, lose_reply=True)
    except TimeoutError:
        venue.fill_all()                      # it fills during the timeout
        venue.place(cid)                      # same ID, no lookup first

def submit(db, venue, cid, crash=False):
    db.execute("INSERT OR IGNORE INTO orders VALUES (?, 'INTENT')", (cid,))
    db.commit()                               # durable before anything is sent
    if crash:
        return                                # the process dies here
    try:
        venue.place(cid, lose_reply=True)
    except TimeoutError:
        venue.fill_all()
        db.execute("UPDATE orders SET state='UNKNOWN' WHERE cid=?", (cid,))
        db.commit()

def recover(db, venue):
    """Run at startup and after every timeout: ask first, then decide."""
    rows = db.execute("SELECT cid FROM orders WHERE state IN ('INTENT','UNKNOWN')")
    for (cid,) in rows.fetchall():
        try:
            if venue.query(cid) is None:      # wait out delays before trusting this
                venue.place(cid)
        except ValueError:                    # duplicate: the order exists
            pass
        except TimeoutError:                  # still unknown: retry next pass
            continue
        db.execute("UPDATE orders SET state='ACKED' WHERE cid=?", (cid,))
    db.commit()

for scope in ("open", "always"):
    v = Venue(scope)
    blind_retry(v, cid)
    print(f"blind retry, {scope} scope: {len(v.orders)} order(s)")

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (cid TEXT PRIMARY KEY, state TEXT)")
v = Venue("open")
submit(db, v, cid)
recover(db, v)
print(f"query first, open scope: {len(v.orders)} order(s)")
submit(db, v, make_id("bot1", 43), crash=True)
recover(db, v)
recover(db, v)
print(f"crash before the send, two restarts: {len(v.orders)} orders for 2 intents")
```

Output on Python 3.11, run 5 November 2026:

```
36d244912785592080298e555758f161 same after restart: True
valid on all: True
blind retry, open scope: 2 order(s)
blind retry, always scope: 1 order(s)
query first, open scope: 1 order(s)
crash before the send, two restarts: 2 orders for 2 intents
```

The two scopes mirror Binance Spot's and Coinbase's documented text; neither is a live result.

## What the documents do not say, and how to test it

Several rules a retry depends on are not published; test them on the venue's test environment first.

- How long Coinbase and Bybit remember an ID.
- Whether Binance Spot accepts an ID again after a cancel or a rejection.
- What a repeated `cloid` on Hyperliquid or a repeated ID on Kraken returns, and whether Alpaca frees an ID once an order ends.

For each venue, run three sequences with one ID: place, fill, repeat; place, cancel, repeat; place, reject, repeat. Record the response and the order count, and rerun after API changes. Retry policy in general is in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration). For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration); naming a venue here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Can I reuse a client order ID?

Some venues allow it after an order ends, including Binance after a fill and OKX after a fill or cancel, but reuse makes lookups ambiguous. Treat every ID as single use.

### How long can a Binance client order ID be?

Up to 36 characters on Spot and USD-M futures. Spot allows letters, digits, dash and underscore, and USD-M also allows dot, colon and slash, as read on 5 November 2026.

### What format does a Hyperliquid cloid use?

An optional 128-bit hex string such as 0x1234567890abcdef1234567890abcdef. The orderStatus info request accepts it in place of the order ID.

### Does a client order ID alone prevent duplicate orders?

No. It gives you something to look up. You still have to store it before sending, query after a timeout and only then decide whether to resend.

## Sources

- [Binance Spot API documentation: REST API (newClientOrderId, query order, timeouts, data sources)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot API documentation: Error codes (Duplicate order sent.)](https://github.com/binance/binance-spot-api-docs/blob/master/errors.md)
- [Binance USD-M Futures: Trade endpoints (newClientOrderId pattern, query order limits)](https://developers.binance.com/en/docs/catalog/core-trading-derivatives-trading-usd-s-m-futures/api/rest-api/trade)
- [Binance USD-M Futures: Error codes (-4116)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/error-code)
- [Bybit V5 API: Place Order (orderLinkId)](https://bybit-exchange.github.io/docs/v5/order/create-order)
- [Bybit V5 API: Error codes (110072, 170141)](https://bybit-exchange.github.io/docs/v5/error)
- [Coinbase Advanced Trade API: Create Order (client_order_id)](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order)
- [Kraken Developers: Client order identifiers (cl_ord_id, userref, FIX)](https://docs.kraken.com/exchange/guides/general/clordid)
- [Hyperliquid Docs: Info endpoint (order status by oid or cloid)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)
- [Hyperliquid Docs: Nonces and API wallets](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets)
- [OKX API v5: Place order (clOrdId rules)](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order)
- [Alpaca Trading API: Create an Order (client_order_id)](https://docs.alpaca.markets/us/reference/postorder)
- [Alpaca Learn: How to fix common Trading API errors, 4 March 2026 (HTTP 422 on a duplicate ID)](https://alpaca.markets/learn/how-to-fix-common-trading-api-errors-at-alpaca)
- [FIX Trading Community, FIXimate: ClOrdID (11), FIX Latest](https://fiximate.fixtrading.org/en/FIX.Latest/tag11.html)
- [FIX Trading Community, FIXimate: DuplicateClOrdIDIndicator (2829), with OrdRejReason 6, FIX Latest](https://fiximate.fixtrading.org/en/FIX.Latest/tag2829.html)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/client-order-id

Cite as: Fincode, "Client order ID: idempotent orders on exchanges and FIX", updated November 6, 2026, https://fincodelabs.com/en/blog/client-order-id
