Skip to content

Infrastructure

Idempotency in payment APIs: how to prevent duplicate charges during retries

Understand idempotency keys, deduplication, retries, concurrency, webhooks, and state machines to prevent duplicate financial effects.

21 min readBy Gustavo Sanchez Palencia
Two repeated requests converging on a single financial effect, representing payment idempotency

Distributed APIs fail in uncomfortable ways. A connection can drop after the server has processed a request. A gateway can return a timeout while the provider keeps working. The client can repeat the call because it received no response. A queue can resend the same message. A user can click the pay button twice.

In ordinary operations, repetition may create only duplicate work. In payments, it can create two financial effects.

Idempotency exists to allow the same intent to be submitted more than once without producing a new effect on every repetition. It is one of the most important tools for building resilient financial APIs, but also one of the most misunderstood.

What does it mean for an operation to be idempotent?

An idempotent operation can be repeated without changing the result beyond the effect of the first valid execution.

If the intent is to create a R$ 100 charge, repeating the same request with the same identity should not create two independent charges.

The repetition should be recognized as the same logical operation.

Does idempotency mean the API always responds identically?

Not necessarily in every detail, but the business effect should remain unique.

Some providers store the original response and return it again. Others return the resource created. Others simply ensure that no second effect occurs.

The exact contract needs to be read in the API documentation.

What is an idempotency key?

It is an identifier supplied by the client to represent a specific intent.

The first time the server receives the key, it executes the operation and associates the result with it. On repetition, it recognizes that the intent has already been processed and avoids executing it again.

The key should not represent “this user.” It should represent “this logical operation.”

Who should generate the key?

Usually, the system that knows the business intent.

If the backend creates a payment attempt, it can generate a unique key for that attempt before calling the PSP.

What matters is that the same intent reuses the same key and a new intent uses another.

Can I use order_id as the idempotency key?

It depends.

If an order can have only one operation of that type, it may work.

But an order can have multiple attempts, captures, refunds, or changes. In that case, using only order_id can block different legitimate operations.

The key should incorporate the correct command granularity.

Is a random UUID enough?

It addresses collision risk, but not semantics.

If the system generates a new UUID on every retry, it has lost the benefit of idempotency.

The key needs to be persisted with the intent before the first call so it can be reused.

When should I create the key?

Before performing the external operation.

If the key is persisted only after the PSP responds, a crash in between can prevent the application from knowing which key was used.

Persisting the intent and key first makes recovery easier.

Is idempotency the same as deduplication?

They are related concepts, but not identical.

Deduplication identifies repeated events and prevents processing them more than once.

Idempotency ensures that repeating an operation does not produce additional effects.

A system can deduplicate by event_id and still need idempotency for external calls.

What about exactly-once delivery?

In practice, distributed systems can rarely rely on physical delivery occurring exactly once from end to end.

Queues and networks tend to favor at-least-once delivery with retries.

That is why idempotent consumers are so important.

The goal is to achieve the business effect exactly once even if the message arrives multiple times.

Is HTTP GET already idempotent?

Under HTTP semantics, GET should be a read operation with no significant business effect.

But a poorly designed API can violate that.

Payment idempotency is more relevant to commands such as creation, capture, cancellation, and refund.

Is PUT idempotent?

The HTTP semantics of PUT are idempotent when it replaces or sets the state of a known resource.

Even so, actual behavior depends on implementation.

Financial safety should not be assumed based solely on the HTTP verb.

Can POST be idempotent?

Yes.

Many payment APIs use POST with an idempotency key to make creation and commands safely repeatable.

The verb alone does not define the guarantee.

Should the server compare parameters?

Ideally, yes.

If the same key appears with a different amount or payload, there is probably a client bug.

Some APIs reject key reuse when parameters do not match the first request.

This prevents a key from being used as a generic identifier for different intents.

How long should the key be remembered?

It depends on the provider's contract and your system.

Some services retain the result for a defined window. After that, reusing the key may be treated as a new operation.

The application needs to know this period and not rely on a permanent guarantee if one does not exist.

Should local idempotency last longer than the PSP's?

It can be a good strategy.

Even if the provider forgets a key after a certain period, your system can retain the intent and block dangerous late retries.

The internal rule does not need to be limited by external retention.

What happens on a timeout?

A timeout is the classic case.

The application sent the request but does not know whether the PSP processed it. Instead of generating a new key, it should repeat the same intent with the same key when the contract allows it.

Then, if the first call already had an effect, the server recognizes the repetition.

Does idempotency eliminate unknown outcomes?

Not always.

It reduces duplication risk, but the application may still need to query the resource to discover the final result.

An idempotency key is a mechanism for safe repetition, not a replacement for a state machine.

Does idempotency prevent double charges across different PSPs?

Not automatically.

One PSP's key usually means nothing to another.

If you experience a timeout at PSP A and send a new attempt to PSP B, you may create two effects.

The platform needs to control this at the payment and attempt levels.

How do you model idempotency in multi-PSP?

Use an internal identity for the intent and an external identity for each attempt.

The payment can represent the purchase. The attempt represents a call to a PSP. Each attempt command has its own idempotency key at the corresponding provider.

The state machine decides when a new attempt can be created.

Can the same key be used for authorization and capture?

Generally not.

They are different commands and can have different payloads, endpoints, and effects.

Each financial intent should have its own identity.

What about partial refunds?

Each refund should have its own identity.

If a purchase receives two legitimate R$ 50 refunds at different times, they cannot share the same idempotency key.

But a retry of the first refund should reuse that specific refund's key.

How do you prevent double-clicks on the frontend?

Disabling the button helps UX, but it is not a guarantee.

The user can open two tabs, the network can repeat requests, or an attacker can call the API directly.

Real protection needs to be in the backend.

Can I generate the key in the browser?

You can, provided the backend validates it and correctly associates it with the session or intent.

Many architectures prefer to generate or confirm the key on the server to control scope.

The important thing is not to rely solely on interface behavior.

How do you persist an idempotent intent?

Create a record with the key, operation type, relevant payload, state, result, and timestamps.

When the same key is received, the application queries that record.

If the operation has finished, it returns the corresponding result. If it is processing, it can wait or return an intermediate state.

What if two requests with the same key arrive simultaneously?

This is an important test.

The system needs to guarantee mutual exclusion or a unique constraint so that only one execution wins.

Without concurrency control, two threads can check “does not exist” simultaneously and execute two charges.

Does a unique constraint solve it?

It helps greatly.

A database unique constraint on the key's scope prevents two rows for the same intent.

But it is still necessary to coordinate when the external call happens and how to recover from a crash.

What is the problem with check-then-act?

It is the pattern “check whether it exists; if not, create it.”

Under concurrency, two processes can pass the check before either creates the record.

The decision needs to be atomic or protected by a constraint or lock.

How does the outbox pattern help?

The application can persist the state change and an outgoing message in the same transaction.

A worker then delivers the message to the PSP or another service.

This avoids the case in which the database commits the intent but the process dies before publishing the command.

What about the inbox pattern?

Consumers store IDs of processed messages to deduplicate deliveries.

This is useful for webhooks and queues.

Outbox and inbox solve different parts of the flow and work well with API idempotency.

Should idempotency return an error or the previous result?

It depends on the contract.

When the repetition has exactly the same intent, returning the previous result is convenient.

When the same key arrives with different parameters, rejecting it is safer.

Should I store the full response?

It can be useful, but size, sensitive data, and schema evolution need to be considered.

Another option is to store a reference to the created resource and reconstruct the current response.

The choice depends on the contract you want to offer.

Can I store an error as an idempotent result?

Some providers do this for errors that occur after execution starts.

The logic makes sense: if the operation actually started and ended with a specific error, repeating it should not silently create a new execution.

But validation errors before execution may be handled differently.

How do you distinguish retryable from non-retryable errors?

The classification needs to consider semantics.

Timeouts and 5xx errors may be retryable when the operation is idempotent. A validation error typically requires fixing the payload. A card decline should not be blindly repeated.

HTTP status is only part of the decision.

Should a 500 response be retried?

Not automatically.

First, check the PSP's contract, idempotency, and whether the outcome is unknown.

Some providers store the idempotent result even when it was a 500.

The application needs to follow the specific documentation.

What about 429?

Rate limiting normally calls for a retry with backoff.

But the command still needs to be idempotent.

If the provider rejected it before processing, retrying may be safe; if behavior is unclear, use the documented guarantee.

What is exponential backoff?

It means progressively increasing the interval between retries.

It reduces pressure during incidents.

Jitter adds variation to prevent thousands of clients from retrying at the same time.

Does backoff replace idempotency?

No.

It merely spaces out retries.

If each repetition creates a new effect, you will get duplicates more slowly.

Is idempotency necessary for webhooks?

The consumer needs to be idempotent.

The same webhook can arrive multiple times.

Processing the same event_id again should not duplicate state changes, balances, or fulfillment.

How do you deduplicate webhooks?

Store the unique event identifier or a reliable derived key and use a unique constraint.

Then process the transition safely.

If the provider does not supply an event_id, you may need to use object identifiers and a version or state, carefully.

Are event ordering and idempotency the same problem?

No.

Idempotency prevents duplicates. Ordering prevents state regression.

An older event can be unique and still arrive after a newer one.

The state machine needs to validate transitions.

How does idempotency relate to the ledger?

Each financial event should generate a journal only once.

If a capture webhook arrives twice, the ledger constraint prevents two credits.

The source key for the entry is a form of idempotency.

How does idempotency relate to reconciliation?

Reconciliation detects when protection has failed or external events do not match expectations.

Even with idempotency, bugs can happen.

Reconciliation is the second line of defense.

What is resource-based idempotency?

Instead of an arbitrary key, the operation uses a known resource.

For example, “set charge X's status to canceled” can be naturally repeatable if the final result is the same.

But financial commands that create new objects usually need an explicit key.

How do you choose the key's scope?

The key can be global, per merchant, per endpoint, or per resource.

The scope needs to prevent collisions between legitimate operations.

A key “123” used by two different merchants may be allowed if the scope includes merchant_id.

What is an idempotency key collision?

It occurs when two different intents receive the same key.

If the server does not compare parameters, one operation may receive another's result.

High-entropy random generators reduce accidental risk, but the application also needs to control logical reuse.

How do you audit retries?

Record attempt_id, the idempotency key, HTTP attempt number, timestamps, result, and the provider's request ID when available.

This lets you determine how many calls were made without confusing that with how many financial effects occurred.

Should support see the retry count?

It can be useful when investigating incidents.

Support does not need protocol details, but knowing that a timeout and idempotent recovery occurred helps explain delays without telling the customer to pay again.

How do you test idempotency?

Send the same request simultaneously multiple times.

Force a crash after persisting the intent and before the external call. Force a timeout after the PSP has processed it. Resend a webhook. Reprocess a queue.

The system should converge on a single effect.

How do you test payload changes?

Use the same key with a different amount and check whether the API rejects it.

This case finds bugs in clients that incorrectly recycle keys.

How do you test key expiration?

Simulate a repetition after the provider's documented window.

The internal application needs to decide whether it still blocks the operation or creates an explicit new intent.

Should idempotency be visible in the public API?

Yes, when external clients perform operations with effects.

A payment API should offer a clear mechanism for safe retries.

Hiding idempotency only in the SDK does not address direct integrations.

Header or body field?

Both can work.

Headers are common because the key belongs to the request's semantics, not the business resource.

The most important thing is a consistent contract.

How do you document idempotency?

Explain which endpoints support it, the key's scope, retention time, behavior with a different payload, concurrency, and error types.

Vague documentation leads clients to make dangerous assumptions.

Does idempotency guarantee exactly one charge?

It guarantees safe repetition within the scope in which it is implemented.

If the application creates two intents with different keys, there will be two technically legitimate charges.

That is why business identity also needs to be controlled.

What is the difference between payment_id and idempotency_key?

Payment_id identifies an object.

An idempotency key identifies a command intent.

The same payment can receive several idempotent commands throughout its lifecycle.

Can I reuse a key at another endpoint?

It is usually not a good idea.

Scopes vary by provider, and reuse can cause conflicts.

The key should represent one specific operation.

How does idempotency help AI agents?

Agents can replan and repeat actions after failures.

Without idempotency, an automation can execute the same payment more than once because it received no confirmation.

Any autonomous layer that commands money needs safely repeatable operations.

How does idempotency help traditional automation?

Schedulers, queues, and jobs repeat too.

The rule is the same: infrastructure should assume messages can arrive more than once.

What happens when the database goes down after the PSP responds?

This is a difficult scenario.

The PSP created the effect, but the application could not persist the result.

If the idempotency key was persisted beforehand, recovery can repeat the call and obtain the same effect or resource instead of creating another.

What if the key was not persisted?

The application may lose the ability to recognize the operation.

That is why generating and saving the key before the external call is a strong practice.

How do you reconcile idempotent operations?

Even with the guarantee, maintain query and reconciliation routines.

They confirm that the external resource matches the internal record.

Defense in depth matters when money is involved.

What does idempotency not solve?

It does not solve fraud, event ordering, invalid transitions, incorrect amounts, incorrect routing, or unavailability.

It solves a specific problem: repeating the same intent without duplicating its effect.

What is the difference between a safe retry and a new attempt?

A safe retry repeats the same intent with the same identity.

A new attempt creates a new execution because the previous one ended with a known outcome or policy decided to proceed.

Confusing the two is a common source of duplicates.

Idempotency is part of the API's financial contract

A payment API cannot assume a perfect network.

Connections fail, clients retry, queues resend, and processes restart.

The right design turns repetition into expected behavior.

When idempotency keys, state machines, constraints, and reconciliation work together, the platform can recover from failures without turning resilience into duplicate charges.