Idempotency
Prevent duplicate wallet movement when Phoenix retries
Wallet operations can be delivered more than once. If your backend processes the same idempotency key twice, players can be charged, reserved, released, captured, or credited incorrectly.
Duplicates must be successful no-ops
When a completed operation repeats with the same idempotency key and the same request fingerprint, return the stored result and do not change the player balance again.
Scope
Idempotency is scoped by:
operator_id + environment + operation + idempotency_keyRetries must reuse the same key. A new key means a new intended money movement.
Required Behavior
For every money-moving operation:
Verify the Phoenix signature.
Start a database transaction.
Look up the idempotency key and operation in your wallet transaction table.
If the same key and same request fingerprint completed before, return the stored result.
If the same key is still processing, return 409.
If the same key is reused with a different request fingerprint, return 422.
Lock the player balance or reservation rows, or use an atomic balance update.
Apply reserve_cash, release_cash, capture_cash, or credit_cash once.
Store the request body hash, response body, final balance, operation status, and processed timestamp.
Commit.
Store the Received Key
You do not construct idempotency keys. Phoenix generates the key and sends it in the lowercase idempotency-key header on every money-moving request (POST /wallet/transactions). Store the key Phoenix sent and dedup on it.
Treat the key as opaque
Never parse the idempotency key or branch on its internal format. The key is a stable platform-generated string; its shape is internal and not a contract. Key your dedup on the tuple, not on anything you decode out of the key.
To reconcile a move back to a Phoenix order, join on the small references set Phoenix sends in the body, not on the key:
| Operation | references keys |
|---|---|
reserve_cash, capture_cash, release_cash | order_id |
credit_cash for sell proceeds or maker rebate | taker_order_id, maker_order_id |
credit_cash for settlement payout | claim_side |
A correction is a new credit_cash (or capture_cash) move with its own reason, idempotency key, and references. See Reconciliation for the full join model.
Request Fingerprint
Store a fingerprint for the first request body:
sha256(canonical_json(request_body))The same key with the same fingerprint returns the first completed result. The same key with a different fingerprint returns 422.
Status Lookup
When Phoenix receives an uncertain timeout, it calls POST /wallet/transactions/status with the same envelope as the original money move (same operation, idempotency_key, amount, reason, references), just sent to the status path. Look the operation up by its idempotency key and report its current state.
The status endpoint must not move money. It should report whether the original operation is processing, accepted, rejected, or unknown.
Your ledger should make these states clear:
| State | Meaning |
|---|---|
processing | The first request is still being handled |
accepted | The wallet operation completed successfully |
rejected | The wallet operation reached a terminal business rejection |
unknown | The wallet cannot yet prove the result |
SQL Pattern
Key the lookup on the idempotency key Phoenix sent, scoped by (operator_id, environment, operation, idempotency_key).
BEGIN;
SELECT * FROM wallet_transactions
WHERE operator_id = $1
AND environment = $2
AND operation = $3
AND idempotency_key = $4
FOR UPDATE;
-- if found with same fingerprint and completed, return stored response
-- if found with different fingerprint, return 422
-- if found and processing, return 409
SELECT balance FROM players WHERE external_id = $5 FOR UPDATE;
-- validate and update balance once
INSERT INTO wallet_transactions (
operator_id,
environment,
operation,
idempotency_key,
external_id,
amount_value,
amount_scale,
currency_code,
request_fingerprint,
response_json
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
COMMIT;Use your own schema, but keep the same behavior: one received idempotency key, one wallet effect.