cryptoinfraDocs

#Idempotency

Networks drop connections in the middle of payouts. The API is built so that the correct response to that is always the same: retry with the same key.

Open as MarkdownFor AI assistants

#The key

Every mutation that moves money accepts an idempotency_key in the request body — a string you generate, 1–255 characters, unique per merchant per logical operation.

EndpointSummary
POST /v1/withdrawalsRequest a payout
POST /v1/exchange/ordersStart an exchange — sell a held coin, pay another one out
POST /v1/swap/ordersStart a swap — sell one coin the user holds, credit them another
POST /v1/cardsIssue a card
POST /v1/cards/{id}/topupTop up a card
POST /v1/cards/{id}/withdrawRetired — moving part of a card balance back to the wallet

It is required on withdrawals and the three card money mutations (issue, top-up, withdraw). It is optional but strongly recommended on swap and exchange orders — optional only because those rails were added later, not because replay safety matters less there.

A UUID v4 per logical operation is the right default. Deriving it from your own primary key — payout-8814, card-topup-2291 — is better still, because it survives a process restart that a freshly generated UUID would not.

#What a replay returns

A replay with the same key and the same parameters returns the original record, unchanged. No second hold, no second payout, no second order.

The status code follows the resource:

EndpointFirst callReplay
POST /v1/withdrawals202202, with the original withdrawal
POST /v1/cards, /topup, /withdraw201 / 202same code, original order
POST /v1/swap/orders, /v1/exchange/orders201201, with the original order

The body is the record as it stands now, not as it stood when it was created — so a replay of a withdrawal that has since confirmed returns it with status: confirmed. That is exactly what you want: the replay doubles as a status read.

#Reusing a key for different parameters is an error

On withdrawals, card mutations, swaps and exchanges, if the key matches an existing record but the parameters do not, the API refuses rather than guessing:

json
{
  "error": "idempotency_conflict",
  "message": "idempotency key payout-8814 was already used with different parameters"
}

with HTTP 409. It will never silently return the original record for a request that asked for something else, and it will never quietly create a second one. If you see this, your key generation is colliding — two different payouts are being assigned the same key.

"The same parameters" means the fields that describe the movement. For a withdrawal that is the user, the asset, the address and the amount; the quoted fees are not compared, because they may legitimately differ between genuine retries of one payout.

#Resolving an indeterminate request

The hard case is not a rejection. It is a create that timed out — you sent it, the connection dropped, and you do not know whether a payout exists.

There are two safe moves, and they agree with each other.

Re-POST with the same key. This is safe by construction. Either it creates the payout (it never landed) or it returns the existing one (it did). One call, one answer.

Or look the key up. GET /v1/withdrawals?idempotency_key=… is the read path built for exactly this: it returns the withdrawal for that key, or 404 if no withdrawal was ever created. Use this when you would rather not issue a write at all until you know the state.

What you must not do is retry with a fresh key. That is how a customer gets paid twice.

#Cards: what 202 means, and what it does not

The card money endpoints return 202 Accepted. The ledger move has happened; the provider outcome has not been confirmed yet. It arrives later as a card.topup / card.withdraw webhook, or through the reconciler.

Treat 202 as pending, never as failure. Refund your user only on a definitive failure:

  • a 409 provider_rejected at request time, or
  • a webhook whose order status is failed.

A top-up's charge is refunded automatically on definitive failure. A withdrawal credits the user only once the provider confirms, and nothing is held meanwhile. Issuance behaves the same way: the issue_fee is charged at request time and auto-refunded if the application definitively fails.

#Retry policy that works

  1. Generate the key before the first attempt and persist it with your own record.
  2. On a timeout, a 5xx, or a dropped connection: retry with the same key, backing off.
  3. On a 4xx other than 409 idempotency_conflict: do not retry — fix the request.
  4. On 409 idempotency_conflict: do not retry — you have a key collision to fix.
  5. When you stop retrying, resolve the state with the idempotency_key lookup or the resource read before you tell anyone anything final.

Idempotency keys are scoped to your merchant, so they never collide with another merchant's. They do not expire.