Skip to content
Skip the menu

Concepts

Idempotency

How to send the same write twice without it counting twice, which routes need a key, and how to choose one.

View as Markdown

On this page

The problem

You call GameCoin to grant 100 gems. The connection drops before the answer arrives. Did the grant happen? If you send it again and it did, the player has 200 gems.

An idempotency key removes the doubt. You send a unique key with the write. If GameCoin already saw that key with the same request, it does not repeat the write: it sends back the original answer. You can retry as often as you like.

How it works

Send the key in the Idempotency-Key header: 1 to 100 printable ASCII characters (letters, digits, spaces and punctuation, no accents).

You sendGameCoin answers
A new keyDoes the write, remembers the answer
The same key with the same requestThe original answer, with the header Idempotent-Replayed: true. No second effect.
The same key with another request409 IDEMPOTENCY_CONFLICT. Nothing changes.
No key on a route that needs one400 IDEMPOTENCY_KEY_REQUIRED
A key that is not 1 to 100 printable ASCII characters400 VALIDATION_FAILED

Keys are kept for 30 days.

LangageLanguage
curl -i -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: level-3-reward-user-42" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":10,"reason":"level-3"}'
# Run it twice. The second answer is identical and carries "Idempotent-Replayed: true".
# The player received 10 gems once.

The second request returns the same entry. The balance moved once.

Which routes need a key

Every POST that changes a wallet or an inventory, or that creates a checkout, requires a key. Reads, PUT, and the refund do not.

RouteKey
POST /players/{player}/wallet/grantrequired
POST /players/{player}/wallet/spendrequired
POST /players/{player}/inventory/grantrequired
POST /players/{player}/inventory/consumerequired
POST /players/{player}/purchasesrequired
POST /players/{player}/checkoutsrequired
POST /client/me/wallet/spendrequired
POST /client/me/purchasesrequired
POST /client/me/inventory/consumerequired
POST /client/me/checkoutsrequired
PUT /players/{player}, POST …/tokens, every GETnot used
POST /orders/{orderId}/refundnot used: see below

A replayed checkout is the one exception to "the original answer": it returns the order in its current state (it may have been paid since) and the same payment URL, with status 200 instead of 201. No second order is created.

Choose a good key

The key must be the same for the same event, and different for every other event. Build it from the thing you are paying for, not from a random value or the clock:

GoodWhy
level-3-reward-user-42One reward per player per level, however many times the call is retried
daily-user-42-2026-10-05One daily reward per player per day
order-1001One checkout for your own order number 1001
BadWhy
A random value made at each attemptA retry gets a new key, so it counts twice
rewardEvery player and every level shares it: the second use is a conflict

A key belongs to your game and environment, not to a player. The same key sent for two different players is the same key with another request, and answers 409 IDEMPOTENCY_CONFLICT. Put the player in the key.

A request that fails does not use up its key. If a spend answers INSUFFICIENT_FUNDS and you retry the same key after the player earned gems, the spend goes through.

What the SDKs do for you

  • The Node.js server SDK and the other server SDKs make a UUID for every call and reuse it for every automatic retry of that call. Pass your own with idempotencyKey when a retry of your code must be safe too, as above.
  • The browser SDK does the same: one random key per call, reused for every replay (network error, 5xx, expired token). To make a call safe across a page reload, pass your own key as the last argument: gc.spend("gems", 5, "level-3", { idempotencyKey: "level-3-entry" }).
  • A result that comes from a replay has replayed: true in the server SDKs.

The refund has no key

POST /orders/{orderId}/refund takes no idempotency key. It is protected by the state of the order instead: a refunded order cannot be refunded again (409 CONFLICT). After a network error or a 5xx, read the order with GET /orders/{orderId} before you try again. See Refunds.

Where next