Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To prevent a client retry from creating a second logical payment, give each payment operation a durable identity and make your ASP.NET Core API enforce it before starting side effects. Store an idempotency key and a fingerprint of the request in shared persistent storage, let a database uniqueness constraint decide which concurrent request owns the operation, and define how retries retrieve a completed or pending result. Also pass a corresponding key to the payment processor and reconcile uncertain outcomes: your API’s guarantee and the processor’s guarantee are separate.

What idempotency does—and does not—guarantee

An operation is idempotent when repeating it has the same effect as performing it once. The response itself does not have to be identical: for example, a repeated delete can leave the resource deleted even if the second response has a different status. Microsoft’s Azure Architecture Center distinguishes operations that are naturally idempotent from those that need application-level duplicate handling.

Creating a payment with POST is not naturally safe to repeat. If a client times out, it may not know whether the server received the request, whether the processor accepted it, or whether the response was lost. A useful guarantee is therefore narrower than “exactly once”: retries identified as the same operation must not create a second logical payment. Durable state and recovery procedures make that guarantee possible even when the network cannot tell either side what happened.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why ASP.NET Core needs an application-level design

ASP.NET Core hosts the HTTP endpoint; it does not automatically persist a payment operation’s idempotency state or coordinate retries across application instances. Put payment orchestration in an application or service layer, persistence behind an abstraction, and processor calls behind a gateway interface. Controllers and Minimal APIs can both use this design.

A canceled HTTP request is not evidence that the processor did not execute the payment. Cancellation tokens help stop work when appropriate, but a disconnected client or canceled request can leave the remote outcome uncertain. Preserve the operation’s durable state and reconcile it rather than treating cancellation as permission to issue a fresh charge.

How to process a payment request safely

  1. Authenticate and validate. Establish the caller’s tenant or customer scope and validate the payment request before any payment side effect. Determine which fields define the operation, such as amount, currency, order or payment-intent identity, and relevant options.
  2. Normalize and fingerprint the operation. Compute a stable fingerprint from those semantic fields. Exclude transport-only values such as a trace ID, which can change between retries without changing the payment being requested.
  3. Require an operation key. Define how the client supplies an idempotency key, commonly in an Idempotency-Key header. Scope the key to the authenticated tenant or customer and the operation so unrelated callers or operation types do not collide. A random, high-entropy client key is a common contract. Stripe recommends a v4 UUID or similarly random value and documents a 255-character maximum for its own API; that limit is Stripe-specific, not an ASP.NET Core rule.
  4. Claim ownership in durable storage. In a database transaction, try to insert a record for the scope and key with the fingerprint and an initial state such as InProgress. Enforce uniqueness on the scoped key in shared storage. That constraint—not an in-memory lock—arbitrates ownership across multiple ASP.NET Core instances.
  5. Handle an existing record. If another request already claimed the key, load its record and compare fingerprints. A mismatch is key misuse: return the documented conflict or validation response without changing the existing operation. If the fingerprints match and the operation is complete, return its persisted outcome or a stable payment resource reference. If it is still in progress, follow the API’s documented pending or bounded-wait behavior; do not start another charge.
  6. Call the processor for a newly claimed operation. Use a processor idempotency key derived from or durably associated with the local operation. Persist processor identifiers and the outcome as the operation advances. Do not assume the processor retains its key for as long as your API retains its own record.
  7. Reconcile uncertain outcomes. If the processor may have accepted the request but the application crashes before recording completion, use the processor key and/or a queryable operation identifier to discover the outcome before attempting another effect. Treat recovery as an explicit state transition, not as deletion of an uncertain record.
  8. Handle processor events separately. Verify webhook authenticity according to the selected processor’s rules, persist each event identity, and make the resulting business-state transition safe to repeat.

This is an architectural flow, not database-specific or tested ASP.NET Core code. Choose transaction boundaries, locking or concurrency behavior, and recovery transitions for the database and processor you actually deploy.

What should the API return on a retry?

Document the contract from the client’s point of view. A completed retry should identify the same logical payment—either by returning the persisted outcome or by returning a stable resource reference the client can retrieve. An in-progress retry needs an explicit policy: the API might report that the operation is pending or wait for a bounded period. Whichever behavior you choose, it must not launch a second payment while the first outcome is unresolved.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Also specify when the API’s own idempotency record stops being authoritative. Keep it long enough to protect against realistic client retries and operational recovery, and define what happens to late retries. The appropriate retention period is an API design decision; the processor’s key-retention policy is not a substitute for it.

A repeated operation need not produce the same HTTP status every time to remain idempotent. What matters is that it refers to the same payment effect and does not cause another one. Make mismatch and pending responses unambiguous so clients know whether they may correct input, wait, or retrieve an existing payment.

How local idempotency and processor keys differ

A local record protects the boundary between your client and your API. A processor key protects the separate boundary between your API and the payment processor. Relying only on the processor leaves your API without its own durable record for client retries, replay behavior, and recovery after a crash.

Design Client-to-API retry protection Retention and response control Concurrency and recovery
Local durable idempotency record Can associate retries with one local payment operation. Your API controls record durability, retention policy, and the outcome or resource reference it replays. A shared unique constraint can arbitrate across application instances; the record also supports explicit reconciliation when local and processor state disagree.
Processor key only Does not itself define your API’s client-facing retry contract. Depends on the processor’s retention and replay behavior; it does not give your API independent control of its own stored response or resource reference. Concurrency and recovery depend on processor behavior and available operation identifiers; your API lacks a local operation record as its durable coordination point.
Local record plus processor key Protects the client-to-API operation and gives it a durable identity. Your API can retain its record separately from processor key retention. Coordinates ownership locally while the processor key helps prevent a second effect during remote retries and reconciliation.

What Stripe’s idempotency behavior illustrates

Stripe documents one provider-specific implementation, not a universal payment-processing rule. Once endpoint execution begins, Stripe saves the first request’s resulting status code and body for a key, including a 500 response, and replays that result on subsequent requests with the same key. It compares parameters associated with the key and reports an error if a later request differs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Stripe says it may prune keys after they are at least 24 hours old. Reuse after pruning can initiate a new request. Stripe does not save an idempotent result when validation fails or when a concurrent request conflicts before endpoint execution begins. Its documentation says POST requests accept idempotency keys, while GET and DELETE are idempotent by definition and do not need them. These are Stripe’s documented semantics; check the current documentation and SDK behavior for the processor you select.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How to handle the failure cases that cause duplicate charges

The client times out after the processor succeeds

The client should retry using the same operation identity. Your local record should route that retry to the existing payment or its current status. If the application lost the processor response, reconcile using the processor key or a queryable operation identifier before attempting any new effect.

The same key arrives with a different amount or currency

Compare the new request fingerprint with the one stored for the key. Reject a mismatch rather than silently returning a payment created for different parameters or changing the original operation.

Two requests with one key arrive at once

Use a uniqueness constraint in shared durable storage to decide which request claims the operation. A process-local lock cannot coordinate separate application instances. The losing request should load the owner’s record and follow the same mismatch, completed, or in-progress handling as any other retry.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Validation fails before execution

Validate before creating payment side effects and define whether the client may correct the request and try again. Stripe, specifically, does not cache a result when validation fails before endpoint execution, so a later valid request can execute. Do not assume another processor behaves the same way.

Best Value
Sale
Programming ASP.NET Core (Developer Reference)
  • Applying all key ASP.NET Core components, including MVC for HTML generation, .NET Core, EF Core, ASP.NET Identity, dependency injection, and more
  • Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap
  • ASP.NET Core code for implementing business logic and data transformations
  • Handling configuration, routing, controllers, views, and common tasks (including posting forms and presenting data)
  • Performing complementary tasks: error handling, logging, application design, authentication, localization, and more

The processor returns a cached failure

Under Stripe’s documented behavior, a response such as a 500 can be saved and replayed for that key. Repeating the same call may therefore replay the failure instead of starting a new execution. Give clients a way to check or reconcile the operation rather than implying that another retry necessarily means a fresh attempt.

A processor key has expired

A processor may allow an old key to be pruned, after which reuse can initiate a new request; Stripe documents this possibility after keys are at least 24 hours old. Your local record must still protect a late client retry from accidentally creating another payment. Verify the selected processor’s current retention and reuse rules.

A webhook is delivered more than once

Use the provider’s event identifier as a processed-message identity, persist it, and make business-state transitions repeat-safe. Microsoft’s Azure Architecture Center recommends tracking processed message IDs when an operation is not naturally idempotent. Confirm the selected processor’s event identity, delivery, and signature-verification rules rather than assuming they match another provider’s.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

Bestseller No. 2
SaleBestseller No. 3
SaleBestseller No. 5
Programming ASP.NET Core (Developer Reference)
Programming ASP.NET Core (Developer Reference)
Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap; ASP.NET Core code for implementing business logic and data transformations
$24.99

What to decide before implementing the endpoint

  • Which authenticated scope and operation type are part of a key’s identity.
  • Which request fields define the payment and how their normalized values produce a stable fingerprint.
  • How clients provide keys, and what happens when a key is omitted or reused with different parameters.
  • Which shared durable store enforces uniqueness across application instances, and how the chosen database handles the transaction and contention.
  • What clients receive for completed and in-progress operations, and how they retrieve a payment’s status.
  • How long local records remain authoritative and how late retries are treated.
  • How processor keys, processor operation identifiers, crashes, and uncertain outcomes are reconciled.
  • How webhook authenticity is verified and duplicate events are prevented from repeating business effects.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.