Skip to main content

Overview

A request times out. You do not know whether the order was placed. Retry, and you might end up with two positions. Do not retry, and you might have none. Neither guess is acceptable when the difference is real money, and “check first, then retry” does not close the gap either: the order can land between your check and your retry. Idempotency-Key removes the guess entirely. This guide covers using it correctly, the one mistake that silently defeats it, and the specific case where the usual advice is wrong.
Read time: about 10 minutes. Short by design. This is one idea applied carefully, not a large surface to learn.

TL;DR

  • Every mutating request accepts an Idempotency-Key. Retry with the same key and you get the original response back instead of a second order.
  • The key identifies one intended action, not one HTTP attempt. Generate it outside your retry loop.
  • Generating it inside the loop defeats the mechanism completely and silently.
  • On 429, honour Retry-After and add jitter.
  • Provisioning a user is the one place a deterministic key beats a random one.
  • A pending withdrawal is not a failed one. Do not resubmit it.

What you will do

  • Attach an idempotency key to a mutating request
  • Write a retry loop that is actually safe
  • See why the common mistake is invisible until it costs you
  • Choose between random and deterministic keys deliberately
  • Handle 429 without extending your own rate limiting
  • Handle the withdrawal case, which behaves differently

What you will need

Knowledge
  • Basic HTTP retry patterns
Tools and access
  • A Bravado API key with trade.execute

The mechanism

Send that twice and one order exists. The second response is a replay of the first, and carries a header marking it as such, so you can distinguish a fresh execution from a replay if your accounting cares. The key is bound to the original request. Reusing it with a different payload is not a way to update an order; it is a mistake the API will reject.

A retry loop that works

The mistake that defeats it

Every attempt now carries a different key, so the server treats each as a new intention. Three attempts that all reach the server produce three orders.
This is the most expensive mistake in this guide, and the worst part is that it is invisible in testing. It only surfaces under the conditions retries exist for: timeouts, degraded networks, and load. By then it is producing duplicate positions in production.
The rule: the key belongs to the intention, and the intention exists before the first attempt. If you can construct the key inside the loop, you have made it an attribute of the attempt.

Random or deterministic

When deterministic is better

Provisioning a user is the clear case:
A duplicate order is bad. A duplicate wallet is worse: one user with two wallets, funds split, and no clean way to merge them. Keying on your own user id makes that impossible no matter how many times the call is retried, from how many workers. The distinction is whether two identical requests could ever be two real intentions. For orders, yes: someone may genuinely want to buy the same thing twice. For provisioning a specific user, never. See White-label sub-accounts.

Which calls need one

Anything that changes state:
  • Orders: POST /v2/trade/order, POST /v2/trade/order/batch
  • Cancels: DELETE /v2/trade/orders/{order_id}, POST /v2/trade/orders/cancel-batch, POST /v2/trade/orders/cancel-all
  • Positions: POST /v2/trade/positions/redeem, /split, /merge
  • Copytrade: POST /v2/trade/copytrade, PATCH, DELETE
  • Combos: POST /v2/trade/combo/quote, /accept, /redeem
  • Users: POST /v2/trade/users
  • Withdrawals: POST /v2/trade/withdraw
Reads do not need one. Cancels are worth calling out: retrying a timed-out cancel with the same key returns the original outcome rather than erroring because the order is already gone.

Handling 429

Rate limiting is where retries commonly make things worse.
Retrying a 429 immediately, without honouring Retry-After, will not succeed and can extend how long you stay limited. The jitter matters too: without it, every client that hit the limit at the same moment retries at the same moment.
Read your budget rather than guessing at it:

The withdrawal case

Withdrawals have a state that no other endpoint has. If the transaction does not confirm inside the latency budget, you get:
That is not a failure. The transfer was broadcast and is in flight.
Do not resubmit a pending withdrawal expecting a fresh attempt. Poll GET /v2/trade/activity?type=WITHDRAW and match on transaction_hash to find the final outcome. Treating pending as failed is how people double-withdraw.

Wrapping up

One header, one rule: the key identifies the intention, so it must exist before the first attempt. Everything else follows. Random keys for actions that could legitimately repeat, deterministic keys for actions that never should, Retry-After honoured with jitter, and pending treated as pending rather than failed.

Frequently asked questions

You get the original response back, marked as a replay. No second order is created.
No. The key is bound to the original request. To change an order, cancel it and place a new one with a new key.
Long enough to cover a retry window. Do not rely on it as a long-term deduplication store for your own business logic; keep your own record of what you intended.
No. Only mutating requests change state, and only they can be duplicated harmfully.
Rarely. It works, but it means two genuinely separate identical orders collapse into one. Only choose it when that is precisely the behaviour you want.
No. It was broadcast but not confirmed within the latency budget. Poll GET /v2/trade/activity?type=WITHDRAW and match the transaction_hash rather than resubmitting.

Resources