ProductOct 4, 2026 · 6 min

Idempotency & Retries in a Returns API

DA
Defne Aksoy
Solutions Engineer

A customer's return gets approved, the refund fires, and the confirmation page hangs. Your support inbox fills with 'is my refund happening?' messages, someone hits the button again, and finance discovers two credits landed on the same order. This is not a hypothetical edge case — it is one of the most common production incidents in returns engineering, and it happens precisely because a network timeout tells you nothing about whether the operation on the other end actually completed.

Retries are unavoidable. Mobile networks drop, load balancers time out, gateways rate-limit, and clients (rightly) retry failed requests to stay resilient. The problem is not that retries happen — it's that most returns APIs, and the merchant integrations built on top of them, were not designed to make retries safe. If your returns API integration treats every incoming refund request as a brand-new instruction rather than a possibly-repeated one, duplicate refunds are a matter of when, not if.

Why 'exactly once' does not exist on a network

Distributed systems theory is blunt about this: over an unreliable network, you can build 'at-least-once' delivery or 'at-most-once' delivery, but true 'exactly-once' execution is not achievable without cooperation from both sides of the call. A payments engineering guide on this exact failure mode puts it plainly — non-idempotent payment operations are the leading cause of duplicate charges and duplicate refunds in production systems, because a client that never receives a response has no way to distinguish 'the server never got my request' from 'the server processed it and the response got lost.' See the community reference at Stripe's idempotency documentation for the canonical treatment of this problem in a payments context, which maps directly onto refund issuance in a returns platform.

In a returns flow specifically, the retry-triggering timeout usually happens at one of three points: the merchant's storefront calling the returns API to approve a return, the returns API calling the payment processor to issue a refund, or a webhook delivery from the returns platform back to the merchant's order management system. Each of these hops needs its own idempotency contract — treating the whole chain as a single black box is how duplicate refunds slip through even when individual services 'looks fine' in isolation.

Idempotency keys: the mechanism that actually works

An idempotency key is a unique token, generated by the caller, attached to a mutating request (create refund, approve return, issue store credit). The server stores the key alongside the result of the first successful execution. If the same key arrives again — because the client retried after a timeout — the server does not re-execute the operation. It returns the stored result from the first call instead. The client gets its answer either way; the underlying refund only ever happens once.

Design choiceWeak patternIdempotent pattern
Key generationServer generates on each requestClient generates a UUID once per logical operation
Key scopeGlobal across all endpointsScoped to operation + merchant + resource
Duplicate handlingRe-executes the refund logicReturns cached result of the original execution
Storage lifetimeNone — key discarded after responsePersisted 24–72h minimum, tied to resource state
Concurrent retriesRace condition, both may executeSecond request blocks or waits on a lock until the first resolves

The critical detail merchants miss when evaluating a returns platform is who generates the key. It must be the client — the storefront, the app, the system initiating the return — not the server. If the server generates the idempotency key, a retry after a dropped response simply gets a new key and the protection is worthless. This is also why idempotency has to be a first-class part of the returns API webhooks contract, not just the synchronous request path: webhook delivery is retried by design, and a merchant's order system needs the same duplicate-suppression logic on the receiving end.

A retry is not a new instruction. It is the same instruction, asked twice, because the answer got lost the first time. Treat it that way at every layer, or the fourth layer will betray the other three.

Safe retry patterns beyond the idempotency key

Idempotency keys solve duplicate execution. They don't, by themselves, solve when and how a client should retry in the first place. A well-behaved returns integration layers a few additional patterns on top:

  • Exponential backoff with jitter — spacing retries out (1s, 2s, 4s, with randomized jitter) prevents thundering-herd retries from overwhelming the returns API right as it recovers from an outage.
  • Explicit retry-safe status codes — a 200 or 201 means don't retry; a 5xx or network timeout means retry is safe; a 4xx (except 429) means fix the request, don't retry blindly.
  • State machines on the resource, not just the request — a return should have a defined state (requested, approved, refund_pending, refunded, failed) so that even a completely out-of-band duplicate request can be rejected because the resource is already in a terminal state.
  • Idempotency key expiry tied to business logic, not an arbitrary TTL — a refund key should live at least as long as any plausible client retry window, commonly 24 to 72 hours, not the 5-minute cache default some teams default to.
  • Dead-letter queues for webhook retries — after N failed delivery attempts, park the event for manual review instead of retrying forever or silently dropping it.

What this looks like from the finance seat

Retail and e-commerce finance teams rarely see the API layer — they see the reconciliation report. Duplicate refunds show up there as unexplained variance between refunds issued and returns processed, and they are expensive to trace after the fact because by the time finance notices, the retry event that caused it is buried in logs from weeks earlier. Industry data on returns volume gives a sense of scale: the National Retail Federation estimates returns cost U.S. retailers hundreds of billions of dollars annually in processed value alone — even a fraction-of-a-percent duplicate-refund rate against that volume is a material line item, and one that is entirely preventable at the engineering layer rather than the audit layer.

This is also a rate-limit and capacity question, not purely a correctness one. When idempotency is missing, retries multiply real load: a single stuck request can become three or four duplicate attempts hitting the API in quick succession, which is one of the fastest ways to trip a poorly-tuned throttle. Understanding your returns API rate limits alongside your retry strategy matters because a retry storm caused by missing idempotency support can itself trigger 429s, which then trigger more retries — a feedback loop that has taken down more than one returns integration during a peak sale weekend.

A practical checklist for evaluating a returns API's idempotency support

  1. 1Does the API accept a client-supplied idempotency key on every mutating endpoint (approve, refund, issue store credit)?
  2. 2Is the key scoped correctly — will two different merchants or two different returns accidentally collide on the same key?
  3. 3What does the API return on a duplicate key — the original result, a 409 conflict, or (incorrectly) a fresh execution?
  4. 4How long are idempotency keys retained, and is that window documented and configurable?
  5. 5Are webhooks idempotent on the receiving side, with delivery IDs merchants can de-duplicate against?
  6. 6Is there a resource-level state machine that rejects operations on a return that's already in a terminal state, independent of the idempotency key mechanism?
  7. 7Does the vendor publish retry guidance (backoff intervals, which status codes are safe to retry) rather than leaving it to trial and error?

If a vendor can't answer these questions clearly during a technical evaluation, assume duplicate refunds are a live risk in your integration, not a theoretical one. This is exactly the kind of detail that separates a returns API built for scale from one that merely works in a demo.

What is an idempotency key in a returns API?

It's a unique token generated by the calling client and attached to a request that changes state, such as issuing a refund or approving a return. If the same request is retried with the same key, the server returns the original result instead of executing the operation again, preventing duplicate refunds.

Who should generate the idempotency key — the client or the server?

The client. If the server generates the key, a retry after a lost response gets a new key by definition, which defeats the purpose. The client must generate the key once per logical operation and reuse it on every retry attempt.

How long should idempotency keys be stored?

Long enough to cover any realistic client retry window — commonly 24 to 72 hours for refund and return-approval operations. A short TTL, like five minutes, leaves a gap where a delayed retry can slip through and cause a duplicate.

Does idempotency also apply to webhooks, not just API requests?

Yes. Webhook delivery is retried by design when a receiving endpoint doesn't acknowledge in time, so merchants need to de-duplicate incoming webhook events using a delivery ID, the same way the returns API de-duplicates incoming refund requests using an idempotency key.

Can missing idempotency support cause rate-limit problems too?

Yes. Without idempotency protection, a single stuck request often turns into multiple retried attempts hitting the API in quick succession, which can trip rate limits and trigger further retries — a feedback loop that's a common cause of integration outages during peak sale traffic.

See it on your own returns.

Start free