Idempotency-Key header removes that risk: send the same key with the same request and we do the work once, then hand you the same answer every time.
The header is optional. A request without it behaves exactly as it always has.
Routes that honor the header
Every cancel route cancels at the end of the current billing period. Reads, lists, and updates are not listed because they either do not touch money or are safe to repeat.
Key format
Send any string you like, as long as it is unique to one operation. A UUID works well:422 with the code idempotency_key_invalid, and nothing runs.
Keys are tied to you as well as to the request. Another caller who sends the same string gets their own independent record and never sees your result. For the admin routes, the team owner’s token and a team API key count as the same caller.
Refund and cancel
For these routes we remember the key per team, per operation, and per target (the payment or the subscription).
A few details worth knowing:
- A replay returns what the first request returned, not the payment or subscription as it looks now.
- Only successful results are stored. If a request fails with an error, the key stays free and a retry runs the request again.
- We never expire these keys.
- You can still see
409with the codeidempotency_request_in_progressin one case: you retried after losing a response while Stripe was still working on the original request. Wait a few seconds and retry with the same key.
Checkout
Checkout has honored the header for longer, and the rules are close to the table above. The differences:- The record is tied to the team, the hub, and the buyer: the contact when the request is signed in, otherwise the email address (trimmed and case-insensitive).
- The request is compared on the price, the coupon code,
success_url, andcancel_url. Change any of them under the same key and you getidempotency_key_conflict. - A finished checkout replays the stored order, session URL, and status, even if the hub or price has changed since.
- If the first attempt failed, a retry with the same key picks it up again. If it created the order before Stripe returned a session, the retry continues with that same order.
- While an attempt is still creating its checkout session, a retry with the same key gets
409withidempotency_request_in_progressinstead of starting a second attempt. If that attempt stopped without finishing, for example because the server restarted, the key frees up after about five minutes and the next retry continues with the same order. - Checkout records expire after 24 hours. After that, the same key is treated as new.
Recovering from a timeout
- Generate a key before the first attempt and store it with the operation on your side.
- If the request times out or the connection drops, send it again with the same key and the same body.
- If you get
idempotency_request_in_progress, wait a few seconds and retry with the same key. - If you get
idempotency_key_conflict, you changed the request. Send the original one again, or, if you really mean a different operation, look up the current state of the payment or subscription first and then use a new key. - A
422or other4xxmeans the request itself was rejected. Fix it and retry.
One limit applies. If our own request failed after Stripe accepted the call, Stripe only remembers the key for about 24 hours. Retry within that window, because after that we cannot guarantee the work is not repeated.
Example: a refund you can retry
$KEY. You get the original response back with 200, trimmed here to the fields that matter:
"amount": 4900, you get this instead:
code set to idempotency_request_in_progress. Quote meta.request_id (also sent as the X-Request-ID header) if you need to contact support.