Idempotency
The nightmare scenario of trading APIs: you POST /orders, the connection times out, and you
have no idea whether the order was placed. Retry blindly and you might buy twice; don’t retry
and you might not be in the market at all.
The fix is one header. Send a client-generated UUID as X-Idempotency-Key with every mutating
request — if the same operation arrives again, the server recognizes it and replays the
original response instead of executing twice.
How to use it
Generate one UUID per logical operation (not per attempt!) and attach it:
// JavaScript — reference client from the Authentication page
import crypto from "node:crypto";
const idempotencyKey = crypto.randomUUID();
const order = await lexx.request("POST", "/orders", {
body: {
exchange: "binance-spot", symbol: "BTCUSDT", side: "BUY", type: "LIMIT",
quantity: "0.001", price: "60000.00", timeInForce: "GTC", marketType: "spot",
},
headers: { "X-Idempotency-Key": idempotencyKey },
});
# Python
import uuid
idempotency_key = str(uuid.uuid4())
order = lexx.request("POST", "/orders", body={
"exchange": "binance-spot", "symbol": "BTCUSDT", "side": "BUY", "type": "LIMIT",
"quantity": "0.001", "price": "60000.00", "timeInForce": "GTC", "marketType": "spot",
}, headers={"X-Idempotency-Key": idempotency_key})
On a successful mutating request the key is echoed back in the envelope —
meta.idempotencyKey — so you can correlate logs.
Exactly what the server does
| You send… | Within | Server responds |
|---|---|---|
| Same key + same parameters | 1 hour | Replays the original response (status + body) without re-executing |
| Same key + different parameters | 1 hour | 409 IDEMPOTENCY_CONFLICT — “Idempotency key already used with different request parameters” |
| Same key while the original is still processing | — | 409 IDEMPOTENCY_CONFLICT — “Request is already processing” |
Same key after the original ended in a 5xx |
— | 5xx responses are not cached — the request executes anew |
| Same key more than 1 hour later | — | Treated as a brand-new request |
Note the fine print: every completed response is cached — including 4xx errors. If your
first attempt failed validation with a 400, the retry with the same key replays that 400;
fix the request and use a new key, because the fixed parameters differ.
The safe-retry recipe
Two rules, and they come from different layers — don’t mix them up:
- Idempotency key: same across attempts. It is the identity of the operation.
- Signature: fresh on every attempt. Timestamp + signature are the identity of the HTTP request — replaying an identical signature is rejected as a duplicate (details). The reference client re-signs automatically on each call.
Put together:
// JavaScript — retries network failures and 5xx safely
import crypto from "node:crypto";
async function placeOrderReliably(lexx, order, attempts = 3) {
const idempotencyKey = crypto.randomUUID(); // ONE key per logical order
for (let i = 1; ; i++) {
try {
return await lexx.request("POST", "/orders", {
body: order,
headers: { "X-Idempotency-Key": idempotencyKey },
});
} catch (e) {
const retryable = e.status === undefined // network error / timeout
|| e.status >= 500; // 5xx are not cached — safe to retry
if (retryable && i < attempts) {
await new Promise(r => setTimeout(r, 1000 * i));
continue; // same key, fresh signature
}
throw e;
}
}
}
# Python — same idea
import time, uuid
import requests
def place_order_reliably(lexx, order, attempts=3):
idempotency_key = str(uuid.uuid4()) # ONE key per logical order
for i in range(1, attempts + 1):
try:
return lexx.request("POST", "/orders", body=order,
headers={"X-Idempotency-Key": idempotency_key})
except LexxError as e:
if e.status >= 500 and i < attempts: # 5xx are not cached — safe to retry
time.sleep(i)
continue
raise
except requests.RequestException: # network error / timeout
if i < attempts:
time.sleep(i)
continue
raise
Whatever happened during the timeout — order placed or not — the outcome of this function is exactly one order and the true response for it.
Handling the two 409 flavors:
- “…already used with different request parameters” — a client bug: you reused a key for a different operation. Generate a new UUID for every new logical operation.
- “Request is already processing” — the original attempt is still in flight. Wait a moment and retry with the same key; once the original completes you will get its replayed response.
Scope and caveats
- Applies to POST / PUT / DELETE. The key is ignored on other methods — notably
PATCH /bots/{id}runs without idempotency protection. - The key must be a UUID (e.g.
550e8400-e29b-41d4-a716-446655440000). - The header is optional everywhere — but strongly recommended for order creation and cancellation, and a good habit for every mutating call (bot start/stop, leverage changes).
- The replay window is 1 hour; the cache stores the response exactly as first produced.
Verified against API spec v2.0.0 · 2026-07-11