WalletD developers

Idempotency

Every money-moving POST takes an Idempotency-Key, so a timeout is safe to retry and a replay returns the first response byte for byte.

Networks fail after the server has already acted. Your request created a transfer, the response never arrived, and now your code has to decide: retry and risk sending twice, or give up and risk not sending at all. Idempotency keys remove the choice.

Every money-moving POST requires an Idempotency-Key header. It is not optional. A request without one is rejected before anything happens.

Idempotency-Key: transfer-order-8891

What a replay does

Once an operation has committed its stored result, a replay returns that response body and status code. The domain rows, outbox events and stored response commit together in walletd. The accounting posting commits separately in ledgerd: no database transaction spans the two services.

A timeout or failure can therefore occur after the ledger accepted a posting but before walletd recorded the response. Money paths use deterministic posting keys and stored-result probes to recover that result on replay. A 5xx does not prove that no money moved. Keep the original request and key, retry within a bounded budget, and escalate an unresolved result through your agreed support channel; do not create a replacement payment with a new key.

That is what makes a retry safe. If you do not know whether a request succeeded, send it again with the same key.

Choosing a key

A key must be stable for the operation and unique across operations. Derive it from your business object and integration namespace, or persist a once-generated UUID.

KeyVerdict
payout-invoice-8891Good. Retries reuse it; a different invoice cannot collide
transfer-{your_order_id}Good
topup-{user_id}-{cart_id}Good
uuid4() generated per attemptBroken. Every retry gets a new key, so every retry moves money
transfer-{timestamp}Broken, same reason
transferBroken. Your second, unrelated transfer is refused as a replay

Generate the key when you decide to do the thing, persist it alongside the business object, and reuse it for every attempt including ones after a process restart.

Keys are scoped per tenant. User-token calls add a user namespace, but machine and staff calls retain the supplied key. Merchant-bound API keys do not add a merchant namespace. Include your merchant or integration identifier, operation and business-object identifier, for example merchant-42-transfer-order-8891. Two merchants using only order-8891 can collide within the same tenant.

A random UUID is also suitable if you generate it once, persist it with the operation and reuse it. The error is generating a new value for each attempt. Idempotency records currently have no automatic expiry or cleanup; do not assume a key becomes reusable after 30 days.

Same key, different body

A replay must be the same request. WalletD stores a hash of the operation and its parameters with the key. Reuse a key with a different body and you get:

{
  "type": "about:blank",
  "title": "Conflict",
  "status": 409,
  "code": "idempotency_key_reuse",
  "detail": "idempotency key reused with different parameters"
}

That is a bug in your code, not a transient failure. Retrying will not fix it. It usually means a key derived from something too coarse: two different amounts sharing one order id, for example.

Retrying correctly

Retry on 429, on 5xx, and on a network timeout. Always with the same key. Back off exponentially with jitter, and respect Retry-After when it is present.

Do not retry on other 4xxs: they mean the request itself is wrong, and it will be just as wrong the second time.

In each language

The pattern is the same everywhere: pass the key from your business object.

curl -sS -X POST "$WALLETD_API/v1/transfers" \
  -H "Authorization: Bearer $WALLETD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: transfer-order-8891" \
  -d '{"from_user":"'$SENDER'","to_user":"'$RECIPIENT'","amount":1500}'

Which calls need a key

Anything that moves money or creates a financial object: transfers, top-ups, payments, captures, voids, refunds, subscriptions, point conversions, marketing funding, payouts, purchases, and subscribes.

Reads never need one. Configuration changes (fee plans, tier limits, reward rules) do not take one either; they are audited instead, and applying the same configuration twice is harmless by construction.

Next

  • Errors for the full code table and what is retryable.
  • Transfers to put this to work.

On this page