Skip to main content
A network failure can leave you not knowing whether a charge, refund, or cancellation went through. If you simply send the request again, you risk doing it twice. The 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:
We trim leading and trailing whitespace. After trimming, the key must be 1 to 255 characters. An empty or longer key gets a 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 409 with the code idempotency_request_in_progress in 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, and cancel_url. Change any of them under the same key and you get idempotency_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 409 with idempotency_request_in_progress instead 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

Never mint a new key for a request whose outcome you do not know. A new key is a new request, and it can refund or charge twice.
  1. Generate a key before the first attempt and store it with the operation on your side.
  2. If the request times out or the connection drops, send it again with the same key and the same body.
  3. If you get idempotency_request_in_progress, wait a few seconds and retry with the same key.
  4. 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.
  5. A 422 or other 4xx means 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

The connection drops before the response arrives. Run the exact same command again, with the same $KEY. You get the original response back with 200, trimmed here to the fields that matter:
The refund happened once. If you send the same key with a different amount, for example "amount": 4900, you get this instead:
If Stripe is still working on the original request, you get the same shape with 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.