Idempotency Keys: Make Sure a Payout Never Pays Twice

What an idempotency key is, how to generate and store one, and how to retry a crypto payout safely so a timeout never sends the money twice.

Picture this. Your server asks the API to send a payout to a customer. The request goes out, and then the connection drops. You get no answer. Did the payout happen or not?

If you guess "no" and send the request again, you might pay the customer twice. On a blockchain that is final: there is no "undo" button for an on-chain transfer. If you guess "yes" and do nothing, the customer might never get paid.

An idempotency key removes the guess. This guide explains what it is, how to use it well, and the mistakes that still cause double payouts on systems that "support" it.

#What idempotency means

A request is idempotent when sending it once or sending it ten times has the same effect. Reading a balance is naturally idempotent. Sending money is not — unless the API gives you a way to say "this is the same payout as before".

That is the job of the idempotency key. It is a string you create and send with the request. The API remembers it. If the same key comes again, the API does not create a new payout. It returns the one that already exists.

#Where our API uses it

On our platform every request that moves money accepts an idempotency_key in the request body. It is required on:

It is optional, but strongly recommended on swap and exchange orders. Treat it as required there too. A repeated swap is still money moving twice.

Keys belong to your merchant account, so they can never clash with another merchant's keys. They also do not expire, which means a retry days later is still safe.

#Step 1: create the key before the first attempt

The most important rule: one key per intended movement of money, not per HTTP attempt.

A good flow looks like this:

  1. Your app decides to pay someone. It writes a payout row in your own database.
  2. It creates the key right then and saves it in that same row.
  3. Only then does it call the API.
  4. Every retry of that payout reads the key from the row and sends it again.

A random UUID is a fine default. A key built from your own record id is even better, for example payout-8814 or card-topup-2291. It survives a crash: if your process restarts in the middle of a payout, it can rebuild the exact same key from the row, instead of generating a new random one and paying again.

#Step 2: understand what a replay returns

When you send the same key with the same details, the API returns the original record. No second payout, no second hold on the balance, no second order.

The record comes back as it is now. So if the payout has already confirmed on TRON, the replay shows it as confirmed. That makes a replay a handy status check as well.

You sendWhat happens
New keyA new payout is created
Same key, same detailsThe existing payout is returned, nothing new is sent
Same key, different detailsThe API refuses with 409 idempotency_conflict
Fresh key for a retryA second payout is created — this is the bug

#Step 3: treat a key conflict as a bug in your code

"Same details" means the fields that describe the movement. For a withdrawal that is the user, the asset, the address and the amount. The quoted network fee is not compared, because it can change between honest retries of one payout.

If the details differ, the API will not guess what you meant. It refuses with 409 idempotency_conflict. It never quietly returns the old payout for a new request, and it never quietly creates a second one.

When you see this error, do not retry. It means two different payouts got the same key. Common causes:

#Step 4: resolve the "I don't know" case

The hard case is a request that timed out. You do not know if the payout exists. There are two safe moves, and they always agree:

The one thing you must never do is retry with a new key. That is exactly how customers get paid twice.

#A retry policy that works

Here is a simple policy you can copy into your payout worker:

ResponseRetry?Same key?
Timeout, dropped connection, 500, 502, 504Yes, with backoffAlways
429 too many requestsYes, after waitingYes
503 service unavailableYes, later, unchangedYes
409 idempotency_conflictNo — fix key generation—
Other 409, such as low balanceOnly after the state changesYes
400, 401, 403No — fix the request—

When you finally stop retrying, check the payout with the key lookup or the withdrawal read before you tell your customer or your support team anything final.

#Cards: pending is not failed

Card money calls return 202 Accepted. Our ledger has moved, but the card program has not confirmed yet. The final answer comes later through a card.topup webhook or our reconciler.

Do not read 202 as a failure, and do not "fix" it by sending a new request. Refund your user only on a clear failure: a 409 provider_rejected at request time, or an order whose status becomes failed. Our cards page shows how loads work end to end.

#Idempotency on your side, too

The key protects the API call. Your own system needs the same habit. Your payout job can run twice after a deploy. A user can double-click "Withdraw". Two workers can pick up the same row.

#Test it before real money moves

The sandbox behaves like production, so you can rehearse every case above without risk. Try these at least once:

#Common mistakes

MistakeWhat goes wrongFix
New key on every retryThe customer is paid twiceStore the key, reuse it
Key made in memory onlyA crash loses it, the restart pays againSave it with the payout row first
Key from user id or dateReal payouts collide and get refusedBuild it from a unique record id
Retrying a 409 conflictThe loop never endsStop and fix key generation
Treating a timeout as failureCustomer is refunded and also paidLook the key up before deciding
No key on swapsA repeated swap converts twiceAlways send one

#In summary

An idempotency key turns "did it go through?" into a question with a safe answer. Create it once, save it before the first call, reuse it on every retry, and look it up when you are unsure. Do that, and a dropped connection becomes a small delay instead of a double payout.

Read the full rules in the idempotency docs, and see the developer overview for the rest of the API.

Ready to send on-chain payouts that never pay twice, under your own brand? Sign up free and try it in the sandbox today.

Launch your own crypto brand.

Wallets, payouts, cards and swaps under your name. Start free and test in the sandbox.