Model the operation, not merely the request
A timeout is not proof that an external mutation failed: the client can lose the reply while the remote service may have received the work. Treat the business action as a durable operation with an identity of its own, rather than allowing an HTTP attempt to be the only record of intent.
For connectivity failures, Stripe advises resubmitting the original key with an unchanged parameter set until the client can obtain a response. This is a way to recover an answer to one logical attempt; it is not an exactly-once guarantee for the surrounding workflow.
Stripe records the first execution-started POST result under its idempotency key, preserving both its HTTP status and response payload. A reuse whose parameters differ is rejected as a mismatch, so a retry path must retain the exact request representation needed for comparison.
Keep an application-owned operation record
The following record is a proposed application convention, not a provider-prescribed schema. Persist it before dispatching the external request and update it transactionally as evidence arrives.
- Operation identity and intent: a local immutable identifier, actor or workflow reference, and the intended business effect.
- Request snapshot: the provider route, canonicalized parameter representation or safely stored fingerprint, and the generated idempotency key.
- Attempt history: dispatch times, transport failures, received status/body, and the most recent recovery decision.
- Reconciliation evidence: webhook event identifiers, matching provider-object references, operator notes, and a final local disposition.
Use locally meaningful labels such as awaiting_reply, indeterminate_500, confirmed, and stopped_for_review. These labels express application policy; they should not be mistaken for Stripe response states.
Generate the key client-side with sufficient randomness, keep it free of sensitive identifiers, and retain it with the operation record. Stripe permits keys with a maximum length of 255 characters.
Use evidence before choosing a new operation
Network ambiguity and a received 500 require different handling. Stripe characterizes a 500 outcome as indeterminate, and repeating an execution-started request under its existing key commonly returns the stored error rather than resolving whether side effects occurred.
For some 500 cases, Stripe may later reconcile changes and send webhooks for created objects. Supplying a local correlation value in metadata can help connect a later event to the durable operation record, but the application still needs a review path when evidence remains inconclusive.
By contrast, validation failures or concurrent-execution conflicts that stop a request before endpoint execution do not create a stored idempotency result. Once the defect is addressed, the application can decide whether the correction is a new logical operation and issue a fresh key accordingly.
Ambiguous-result recovery decisions
This table is a bounded application policy built on the documented behaviors above. “Stop” means cease automated dispatches and preserve the record for reconciliation or review; it does not assert that the remote outcome is known.
| Situation | Local state | Evidence to collect | Permitted recovery action | Bounded stop |
|---|---|---|---|---|
| Reply lost after dispatch | awaiting_reply | Original key, immutable request snapshot, and any later response | Reuse the original key only with an identical parameter set to seek the prior result. | After the local retry window, stop automatic sends and reconcile from retained evidence. |
| Received HTTP 500 | indeterminate_500 | Stored response, correlated events, object references, and local metadata correlation value | Do not create a replacement operation solely because of the 500; reconcile first. | At the review deadline, route the unresolved record to an operator or business-specific exception process. |
| Rejected before execution | needs_correction | Validation or concurrency indication and the corrected intended request | Correct the defect, then treat changed input as a new logical attempt with a fresh key. | Stop when correction requires unavailable input, authorization, or a policy decision. |
| Key may have been removed | retention_boundary | Key age, operation record, prior responses, and reconciliation evidence | Do not assume reuse is safe: after Stripe has removed a key, reuse can initiate another request. | Block automatic redispatch and require an explicit duplicate-risk decision. |
Respect the retention and error boundaries
Stripe allows automatic idempotency-key cleanup after a key has reached 24 hours of age, without defining a guaranteed deletion moment. If cleanup has occurred, a later use of that key is handled as a new request, which is why local retention and escalation windows should be more conservative than an assumed expiry clock.
A received 4xx response also needs classification rather than a blanket retry rule. An execution-started POST can retain a 400 result for its key, while certain failures can arise before the idempotency layer. When changing the request to fix an error, use a fresh key rather than altering the request beneath the old one.
Make the policy reviewable
Choose and document a finite retry budget, a backoff schedule, a reconciliation deadline, and an escalation owner. Those are application decisions. They should be calibrated to the business cost of a duplicate, delayed completion, and manual review—not presented as guarantees supplied by Stripe.
Review this design if the cited Stripe guidance changes, or if a provider version or integration change affects the request path. The supplied material supports idempotency and error-recovery behavior, but it cannot establish that a particular local state machine, webhook consumer, or reconciliation process is complete for every workflow.
What makes this recovery action safe enough to automate when the reply is lost but the operation record is resumed later? A concrete failure path is: dispatch succeeds, the client loses the reply, recovery is delayed past the provider’s retention boundary, and the worker resends the old key. Stripe states that a pruned key is treated as a new request, so that resend can create a second mutation rather than recover the first result. The same documentation only supports same-key retry with the same parameters while the original idempotency record remains available.
Could the policy make the gate explicit: permit automated reuse only when the immutable request snapshot is available and the key is still inside a locally enforced window that is safely shorter than the possible pruning boundary; otherwise stop dispatch and reconcile? Parameter reconstruction also needs to be treated as a containment check: Stripe rejects a same-key request whose parameters differ, which prevents an accidental altered retry but does not itself establish the outcome of the first dispatch. Stripe’s idempotency documentation and its network-error guidance support retrying the same key and parameters to obtain a result, but neither turns an aged or non-reproducible retry into a safe automated recovery.