CoreInnovateCoreInnovate
← Blog·Engineering

Building Idempotent Payment APIs

February 5, 2025·7 min read

Idempotency is a prerequisite for reliable payment systems, not an optional feature. This guide covers idempotency key design, storage patterns, and the edge cases that break naive implementations.

Idempotency in payment APIs is not a nice-to-have. It is the mechanism that prevents double charges, duplicate refunds, and duplicate transfers — all of which are catastrophic for customer trust and expensive to remediate. Yet idempotency is frequently implemented incorrectly, partially, or not at all.

Why idempotency is necessary in distributed systems

In any distributed system, requests can fail in ways that leave the caller uncertain about the outcome. A client sends a payment request. The request is processed successfully. The response is lost in transit. The client's timeout fires. The client retries.

Without idempotency: the retry creates a second payment. The customer is charged twice.

With idempotency: the retry returns the result of the original request. The customer is charged once.

This is not a theoretical failure mode. Network partitions, application restarts, and DNS failures happen regularly. Your API must be designed for the assumption that clients will retry, and the outcome of a retry must be identical to the outcome of a successful first request.

Idempotency key design

The idempotency key is a client-generated unique identifier for a request. The client is responsible for generating it, sending it with the request, and using the same key for all retries of the same logical operation.

Key properties:

  • Unique per operation: the key must uniquely identify one specific intended operation, not just the request. A key that's derived from only the amount and currency will collide for two legitimate transactions with the same amount.
  • Client-controlled: the server must not generate idempotency keys. Only the client knows what constitutes a retry vs. a new operation.
  • Scoped to the client: idempotency keys should be namespaced to the API client (by API key or customer ID) so two clients can use the same key without conflict.

A reasonable format: UUIDs generated at the point where the user initiates the action. A new UUID for each new user-initiated action; the same UUID for retries.

Related: Ledger Design Principles for Wallet Platforms

Server-side implementation

On receipt of a request with an idempotency key:

  1. Check the idempotency store for the key. If found, return the stored response.
  2. If not found, execute the operation.
  3. Store the key with the result before returning the response.
  4. Return the response.

Step 3 must happen before step 4. If the response is returned before the key is stored and the application crashes, the next request with the same key will process the operation again.

The idempotency store must be durable and consistent. An in-memory cache is not sufficient — a restart would lose all keys, making retries unsafe. Use a persistent store with the same durability guarantees as your transaction data.

The concurrent request problem

A naive implementation has a race condition: two concurrent requests with the same key both miss the idempotency check and both proceed to execute the operation.

Handle this with a distributed lock on the idempotency key. Before checking the store, acquire a lock. After storing the result, release the lock. Concurrent requests with the same key will block until the first request completes, then return the stored result.

Distributed locks are tricky. If the application crashes while holding the lock, the lock must expire automatically. Choose a lock TTL that's longer than your maximum operation duration to avoid lock expiry during normal processing.

Handling PSP idempotency

Your idempotency layer must extend to PSP calls. If your application processes a charge and then crashes before recording the result, the next retry must not charge the card again.

Most PSPs accept an idempotency key on charge requests. Pass a key derived from your internal idempotency key when calling the PSP. If the call is retried, the PSP returns the result of the original charge. Your application records that result and returns it to the client.

Related: The Most Common Architecture Mistakes in Payment Gateways

Key expiry

Idempotency keys should expire after a reasonable period — 24 hours is common. After expiry, the same key can be reused and will be treated as a new request. This prevents the idempotency store from growing indefinitely while still covering all practical retry scenarios (clients typically retry within minutes, not days).

Testing idempotency

Idempotency must be tested explicitly. Your test suite should include:

  • Two identical requests with the same key: only one operation is executed, both return the same response
  • Two concurrent identical requests with the same key: only one operation is executed
  • A request where the application crashes after executing but before returning: the retry returns the correct result without re-executing
  • A request after key expiry: treated as a new operation

Idempotency is one of those properties that's easy to get mostly right but hard to get completely right. If your payment API doesn't have end-to-end idempotency — including at the PSP layer — a platform assessment will surface where the gaps are.

CoreInnovate

Working on a payment platform challenge?

Our specialist engineers work directly with payment gateways, wallet providers, and fintech platforms. Start with a scoped architecture assessment.