Double credits are one of the fastest ways to lose trust (and money) in an online casino. They usually don’t come from “bad math”, they come from distributed systems behaving exactly as designed: clients retry, payment providers resend webhooks, networks drop responses, and your cashier gets the same request twice.
Idempotency is the engineering pattern that turns those inevitable duplicates into safe no-ops, so a deposit is credited exactly once even if it’s requested, confirmed, or reported multiple times.
What idempotency means (in casino payment terms)
An operation is idempotent if performing it multiple times has the same effect as performing it once.
In casino payments, the critical idempotent operations are:
- Creating a deposit intent (the “I want to deposit $50” request)
- Posting the ledger credit that updates the player wallet
- Applying any side effects tied to the deposit (bonuses, VIP points, affiliate attribution)
The goal is not “no retries”. The goal is: retries are safe.
It helps to be explicit about delivery semantics:
- Most PSP webhooks are effectively at-least-once delivery.
- Your own microservices and queues will also behave like at-least-once unless you build for exactly-once.
- So your business logic must tolerate duplicates.
If you want a good mental model, compare it to a service business booking flow: the customer clicks “book” twice, the network lags, and you must not schedule two trucks or charge twice. Reliable operators build predictable booking flows (for example, the kind you see on a trusted moving company in the Bay Area with a clear, step-by-step process). Your casino cashier needs the same “safe duplicate click” behavior.
Where double credits actually come from
Most teams only think about the user double-clicking “Deposit”. In practice, duplicates show up across multiple layers.
1) Client-side retries and rage clicks
- Mobile network drops the response but the request reached your API
- The cashier UI times out and retries
- The player taps twice
2) Gateway behavior and asynchronous confirmations
- PSP sends the same webhook multiple times until it gets a 2xx
- PSP sends “authorized”, then “captured”, then “settled”, sometimes with duplicates
- Webhooks arrive out of order
3) Internal duplication
- Message queue redelivery
- Worker crashes after posting a credit but before acknowledging the message
- Two instances process the same callback concurrently
4) Crypto and on-chain quirks
- You ingest the same transaction from multiple node providers
- Reorgs and replaced transactions create confusing event sequences
- A deposit is detected “pending” and later “confirmed”, but your code credits twice
The cashier architecture that makes idempotency achievable
Idempotency is easiest when you stop thinking in terms of “credit the player” and start thinking in terms of a state machine + a ledger of record.
A practical pattern is:
- Create a Payment Intent (or “Deposit Intent”) as the single source of truth for the deposit’s lifecycle.
- Process payment rail events (PSP, bank, crypto) as inputs that transition intent state.
- When the intent reaches a terminal “success” state, post exactly one ledger credit.
- Make all side effects dependent on the ledger posting (or on an internal “DepositCredited” event).
Here is a simplified view.

Recommended state model (minimal)
You can implement many variants, but try to keep the lifecycle unambiguous.
| Intent state | What it means | Allowed next states | Should wallet be credited? |
|---|---|---|---|
CREATED |
Intent exists, payment not confirmed | PENDING, FAILED, EXPIRED |
No |
PENDING |
External payment in progress | SUCCEEDED, FAILED, EXPIRED |
No |
SUCCEEDED |
Payment confirmed by your policy | Terminal | Yes, once |
FAILED |
Definitive failure | Terminal | No |
EXPIRED |
Abandoned or timed out | Terminal | No |
Your “confirmed by your policy” definition varies by rail:
- Cards: capture success, or a settled confirmation depending on your risk policy
- Open banking / instant bank rails: provider confirmation can be enough
- Crypto: enough confirmations, plus AML/KYT checks as needed
Idempotency keys: the practical rules that prevent duplicates
Idempotency typically uses a key that represents “this operation”. If the same key arrives twice, you return the same result and do not repeat side effects.
What to make idempotent
Make these endpoints idempotent:
POST /deposits(create intent)- Any endpoint that can trigger wallet credits or payouts
- Any “admin action” that changes balances (manual adjustments)
How to choose an idempotency key
A good idempotency key is:
- Unique per logical attempt (one deposit attempt equals one key)
- Stable across retries (same attempt, same key)
- Scoped (keys should not collide across players or merchants)
Most teams do one of these:
- Client-generated UUID stored in the client state and reused on retry
- Server-generated key returned on first call, then reused by client for subsequent confirmation calls
In many payment APIs, the key is sent in an Idempotency-Key header (Stripe popularized this approach, see their idempotency documentation).
Store more than the key
Persist an “idempotency record” that includes:
- Idempotency key
- Request fingerprint (player ID, amount, currency, method)
- Created timestamp
- Response payload (or at least the created intent ID and status)
This lets you safely return the same response on retries.
Enforce it with the database, not only code
Your application code is not a lock. Your database constraints are.
Use a unique constraint like:
UNIQUE(merchant_id, idempotency_key)on the payment intent table
Then the flow becomes:
- Try insert
- If insert succeeds, proceed
- If it conflicts, load the existing intent and return it
That design survives concurrent requests across multiple instances.
The ledger is where you must be absolutely strict
Even if intent creation is idempotent, you can still double-credit if your “credit wallet” step is not idempotent.
Use a ledger posting reference that cannot be duplicated
When you post the wallet credit, include a unique external reference, for example:
ledger_entry.external_ref = deposit_intent_id
Then enforce:
UNIQUE(ledger_account_id, external_ref)
Now, regardless of how many times your credit function runs, only one ledger credit can exist for that deposit.
Separate “payment received” from “player credited”
In regulated environments, and especially in crypto-ready stacks, it’s common to see multiple “confirmations” for the same payment. Treat them as updates to the intent, not as repeated credits.
A clean mental model:
- External world can be noisy and repetitive.
- Your ledger is deterministic and append-only.
Webhooks: design for duplicates and out-of-order events
PSP webhooks are not a nice-to-have, they are a distributed system integration point. You should assume:
- Duplicate deliveries
- Delayed deliveries
- Out-of-order deliveries
Deduplicate webhooks by provider event ID
Most PSPs include an event ID. Store it.
| Problem | Symptom | Fix |
|---|---|---|
| Duplicate webhook | Same “payment_succeeded” arrives twice | UNIQUE(psp_name, psp_event_id) and ignore conflicts |
| Out-of-order events | “succeeded” arrives before “pending” | Make transitions monotonic and ignore invalid regressions |
| Partial data | Early webhook lacks metadata | Allow intent enrichment updates without changing credit logic |
Make state transitions monotonic
Don’t allow “SUCCEEDED” to go back to “PENDING” because a late “pending” webhook arrived.
A simple rule:
- Assign each state a rank (
CREATED=0,PENDING=1,SUCCEEDED=2,FAILED=2,EXPIRED=2) - Only apply a transition if the incoming rank is greater than the stored rank
Acknowledge webhooks only after durable processing
If you return 200 before you commit the event record and intent update, you will lose events.
A robust pattern:
- Persist webhook event (dedupe)
- Update payment intent
- Emit internal event (optional)
- Return 2xx
Crypto deposits: idempotency is harder than “tx hash only”
Crypto rails add edge cases that break naive dedupe.
What to dedupe on
- If you ingest from multiple providers, dedupe on
(chain_id, tx_hash)at minimum. - For token transfers, you often need
(chain_id, tx_hash, log_index)because a single tx can contain multiple transfers.
Confirmations and reorgs
Your policy might be:
- Detect deposit at 0 confirmations, mark intent
PENDING - At N confirmations, mark intent
SUCCEEDEDand credit
If a reorg happens (rare but possible depending on chain), your system must be able to:
- Mark the intent as “reorg risk” (operationally)
- Prevent immediate withdrawal if your risk policy requires it
The key idempotency principle still applies: credit exactly once when your intent crosses the success threshold.
Fiat-to-crypto onramps
Onramps can emit multiple statuses (created, KYC required, paid, crypto delivered). Treat these as state transitions on the same intent. Credit only when you have the final “delivered” signal that your business policy requires.
Don’t forget side effects: bonuses, VIP, affiliates
Many casinos fix deposit double credits but accidentally double-apply side effects.
Common examples:
- Deposit bonus issued twice
- VIP points doubled
- Affiliate CPA triggered twice
The safest pattern is:
- Only emit one internal event like
DepositCreditedafter the ledger post succeeds - Make all side-effect services consume that event idempotently (with their own dedupe keys)
This is one reason modern iGaming platforms bundle payments, wallet, bonus engine, and analytics. Fewer integration seams means fewer places to accidentally repeat side effects.
Operational guardrails and monitoring
Idempotency isn’t “set and forget”. You should monitor for the signals that tell you duplicates are happening and whether your controls are working.
Track these KPIs:
- Duplicate request rate: percent of
POST /depositswith reused idempotency keys - Duplicate webhook rate: percent of webhook events rejected by dedupe
- Ledger conflict rate: count of prevented duplicate ledger posts (should be non-zero)
- Time-to-credit (P50/P95): idempotency shouldn’t add latency if designed well
- Manual correction volume: how often support or finance needs to intervene
A good sign is when you see duplicates being blocked automatically, without tickets.
How Spinlab approaches payment correctness (what to look for)
If you’re evaluating an iGaming platform or white label casino platform, ask directly how they prevent double credits across fiat and crypto rails. In particular, look for:
- An auditable wallet ledger (ideally designed for high volume)
- Clear payment intent states and webhooks
- Built-in fraud prevention and compliance hooks (KYC/AML)
- Open API integration so you can instrument and verify behavior end-to-end
Spinlab is built as a modular iGaming platform with integrated payments (crypto and fiat), compliance, and analytics, which makes implementing idempotent cashier behavior much easier than stitching together separate vendors.
Frequently Asked Questions
What is idempotency for casino payments? Idempotency for casino payments means a deposit, withdrawal, or balance update has the same outcome even if the request or webhook is processed multiple times, preventing double credits.
Is an idempotency key enough to prevent double credits? Not by itself. You also need idempotent ledger posting (unique constraints on ledger references) and webhook deduplication, otherwise duplicates can still credit the wallet.
How long should you store idempotency keys? Long enough to cover realistic retries and delayed webhooks. Many teams store intent-level idempotency records for days or weeks, while keeping a shorter TTL for raw request replay caches.
How do you handle idempotency with PSP webhooks arriving out of order? Store each webhook event with a unique event ID, apply monotonic state transitions, and make wallet credits depend only on the intent entering a terminal success state.
Does crypto need different idempotency rules than fiat? The principles are the same, but the identifiers differ. Crypto often requires dedupe on (chain_id, tx_hash, log_index) and careful handling of confirmation thresholds and rare chain reorganizations.
Want an idempotent cashier without rebuilding your stack?
If you’re launching or scaling an online casino and want deposit and withdrawal flows that are safe under retries (across cards, APMs, and crypto), Spinlab’s modular platform is designed to reduce payment edge cases with an integrated wallet, compliance layer, and open APIs.
Explore Spinlab at spinlab.studio and request a walkthrough to discuss your payment flows and idempotency requirements.