# Idempotency Keys in Payment APIs: Prevent Double Charges

> How idempotency keys stop duplicate charges in payment APIs. Learn what idempotency means, how keys work, retry-safe patterns, key scope and TTL, and how to implement them correctly.
- **Author**: Ayush Agarwal
- **Published**: 2026-07-12
- **Category**: Payments, Engineering
- **URL**: https://dodopayments.com/blogs/idempotency-keys-payment-api

---

An idempotency key is a unique value you attach to a payment request so the API treats a retry as the same operation, not a new one. Send the same charge twice with the same key and the payment provider processes it once and returns the original result the second time. That single mechanism is what stands between your customers and accidental double charges.

Double charges are one of the most damaging bugs in payments. They erode trust instantly, generate refund requests, and often turn into chargebacks. And they are surprisingly easy to cause: a network timeout, a user double-clicking a button, a background retry, or an ambiguous failure where you never learned whether the first request succeeded. Idempotency keys make all of these safe.

This post explains what idempotency means in payments, exactly how keys work, the retry patterns they enable, and the practical details of scope, generation, and time-to-live that separate a correct implementation from a broken one. It is written for engineers integrating a payment API.

## The Problem: Ambiguous Failures

The core problem idempotency solves is the ambiguous failure. When you send a charge request and get a clean success or a clean error, you know what happened. The danger is when you get neither.

A network timeout is the classic case. You send a charge, the provider receives it and creates the payment, but the response is lost on the way back to you. From your side, all you know is that the request timed out. Did the charge go through or not? If you retry to be safe, you might charge the customer twice. If you do not retry, you might have failed to charge a customer who actually owes you.

This is not a rare edge case. At any real scale, timeouts, dropped connections, and process restarts happen constantly. Without idempotency, every retry is a gamble between double-charging and under-charging. Idempotency keys remove the gamble by making retries safe to send.

## How Idempotency Keys Work

An idempotency key is a unique string, usually a UUID, that you generate for each logical operation and send with the request, typically in a header. The provider stores the key alongside the result of the operation.

```mermaid
flowchart TD
    A[Client generates
idempotency key] --> B[Send charge
with key]
    B --> C{Provider seen
this key?}
    C -->|No| D[Process charge
store key + result]
    C -->|Yes| E[Return stored result
do NOT re-charge]
    D --> F[Return result]
    E --> F
```

The logic is simple but powerful. When the provider receives a request, it checks whether it has already seen that idempotency key. If not, it processes the charge, stores the key with the result, and returns it. If it has seen the key before, it skips processing entirely and returns the stored result from the first time. The customer is charged exactly once, no matter how many times you send the request.

This is why a retry after a timeout is safe. If the first request actually succeeded, the retry with the same key returns that original success without charging again. If the first request never reached the provider, the retry processes normally. Either way, you end up with exactly one charge and a definitive answer.

## Retry-Safe Patterns

Idempotency keys unlock a clean retry strategy. Because retries are safe, you can retry aggressively on ambiguous failures without fear of duplication.

The pattern is to generate the key once, before the first attempt, and reuse the same key across every retry of that operation. The key must be tied to the logical operation, not to the individual HTTP request. If you generate a new key on each retry, you defeat the entire purpose, because the provider sees each attempt as a distinct operation.

Here is the shape in practice:

```javascript
// Generate ONE key for the logical operation
const idempotencyKey = crypto.randomUUID();

async function chargeWithRetry(payload, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await dodopayments.payments.create(payload, {
        idempotencyKey, // same key on every retry
      });
    } catch (err) {
      if (i === attempts - 1 || !isRetryable(err)) throw err;
      await sleep(backoff(i)); // exponential backoff
    }
  }
}
```

The key is created outside the retry loop, so all attempts share it. Combined with exponential backoff, this gives you a payment call that survives transient failures without ever double-charging. This safety is why machine-driven flows like [agentic commerce](https://dodopayments.com/blogs/agentic-commerce) and [API monetization](https://dodopayments.com/blogs/api-monetization) depend on idempotency: an autonomous agent retrying a request cannot be trusted to avoid duplicates on its own.

## Key Scope and Time-to-Live

Two implementation details decide whether your idempotency works correctly: what the key covers, and how long the provider remembers it.

Scope is about tying the key to the right operation. A key should represent one intended action, like "charge this specific invoice." If you accidentally reuse a key across genuinely different operations, the provider will return the first result for the second, wrong operation. If you generate a fresh key for what should be the same operation, you lose protection. The rule is one key per logical intent, generated at the moment you form that intent.

Time-to-live, or TTL, is how long the provider stores the key and its result. Providers typically keep keys for a window, often around 24 hours. Retries must happen inside that window to be recognized. This is usually plenty for network-level retries, which occur within seconds. It also means you should not reuse a key days later expecting the old behavior, because it may have expired.

Storing your own record of which keys map to which operations, on your side, helps you reason about retries and reconcile against the provider. The cleanest home for that record is an append-only ledger, where the key becomes a uniqueness constraint on the posting itself so a replayed request cannot write a second set of entries; we walk through that in [designing a double-entry payment ledger](https://dodopayments.com/blogs/payment-ledger-design). Reliable idempotency is one piece of the broader [order-to-cash process](https://dodopayments.com/blogs/order-to-cash-process), where every charge needs to be exactly-once to keep billing and revenue accurate.

## Idempotency and Webhooks

Idempotency is not only about outbound requests. Your webhook handlers need it too, for the same reason: providers may deliver the same event more than once to guarantee at-least-once delivery.

If a `payment.succeeded` webhook arrives twice and your handler provisions access or grants credits both times, you have the inbound version of a double charge. The fix mirrors the outbound one: each webhook event carries a unique ID, and your handler should record processed IDs and skip duplicates. Our [webhooks guide](https://docs.dodopayments.com/developer-resources/webhooks/intents/webhook-events-guide) covers this, and treating both directions as idempotent is what makes a payment integration genuinely reliable.

Together, idempotent requests and idempotent webhook handling give you exactly-once semantics across your whole payment flow, even though the underlying network only guarantees at-least-once.

## How Dodo Payments Handles Idempotency

Dodo Payments supports idempotency keys on write operations, so you can safely retry charges, subscription creations, and refunds without risking duplicates. You pass a key with the request, and repeated calls with the same key return the original result rather than performing the action again.

The [official SDKs](https://docs.dodopayments.com) accept an idempotency key parameter directly, so you do not have to manage the header manually. Webhook events carry unique IDs for inbound deduplication, and the [integration guide](https://docs.dodopayments.com/developer-resources/integration-guide) documents the retry-safe patterns. Because Dodo also operates as a [merchant of record](https://dodopayments.com/payments/merchant-of-record), the exactly-once guarantees extend through tax and settlement, not just the initial charge. Reducing duplicate charges this way also keeps your dispute rate down, since a double charge is a common trigger for the disputes we cover in [chargeback vs refund](https://dodopayments.com/blogs/chargeback-vs-refund).

## FAQ

### What is an idempotency key in a payment API?

An idempotency key is a unique value you attach to a payment request so the provider treats a retry as the same operation rather than a new one. If you send the same charge twice with the same key, the provider processes it once and returns the original result the second time, preventing duplicate charges.

### Why do payment APIs need idempotency keys?

They exist to handle ambiguous failures, like a network timeout where you never learn whether a charge succeeded. Without a key, retrying risks double-charging and not retrying risks under-charging. An idempotency key makes retries safe, so you always end up with exactly one charge and a definitive result.

### Should I use a new idempotency key for each retry?

No. Use the same key across every retry of the same logical operation. Generate the key once, before the first attempt, and reuse it. If you generate a new key per retry, the provider sees each attempt as a distinct operation and the protection disappears.

### How long do idempotency keys last?

Providers store keys for a limited window, often around 24 hours, along with the original result. Retries must happen inside that window to be recognized and deduplicated. This is ample for network-level retries, which occur within seconds, but means you should not reuse an old key days later expecting the same behavior.

### Do webhook handlers need idempotency too?

Yes. Providers deliver events with at-least-once semantics, so the same webhook can arrive more than once. Each event carries a unique ID, and your handler should record processed IDs and skip duplicates, so a repeated event does not provision access or grant credits twice.

## Conclusion

Idempotency keys are the mechanism that makes payment requests safe to retry. By attaching a unique key to each logical operation, you turn ambiguous failures from a double-charge gamble into a safe retry, ending with exactly one charge every time.

The details matter: one key per intent, reused across retries, sent within the provider's TTL, and matched by idempotent webhook handling on the inbound side. Get those right and your payment flow achieves exactly-once behavior on top of a network that only promises at-least-once. That reliability is not a nice-to-have in payments. It is the difference between a trustworthy integration and one that quietly double-charges your customers.
---
- [More Payments articles](https://dodopayments.com/blogs/category/payments)
- [All articles](https://dodopayments.com/blogs)