Skip to content

Idempotency

Creating, confirming and cancelling a payment intent (Payment intents), and creating a refund (Refunds), each require an Idempotency-Key header. Use a value unique to the operation, of 1 to 255 characters, such as your order number and attempt. Without one the request is refused with validation_failure and nothing is done.

Terminal window
curl https://api.example.com/v1/payment_intents \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: order-1042-1" \
-H "Content-Type: application/json" \
-d '{"amount": 10000, "currency": "php"}'

Retry a request that timed out or failed with the same key and the same body, and it is done at most once:

  • If the first request succeeded, you get its response again, byte for byte, with an Idempotent-Replayed: true header.
  • If the first request is still running, you get timeout_or_in_progress with code idempotency_key_in_use. Retry shortly.
  • If the first request was refused, or failed with dependency_failure, the key is free and the retry runs afresh. A refusal changed nothing, so fix what it named and retry with the same key. A payment intent is never paid twice however often a request is retried: it is confirmed once, and the customer is given one code or page to pay.

While a key is held by a request that succeeded or is still running, the same key with a different body, or on a different payment intent, is a conflict with code idempotency_key_reused. The body is compared as JSON, so the order of its fields and its spacing make no difference.

A key belongs to one merchant, one mode and one operation: the same key may be used once to create, once to confirm and once to cancel, and a test key's keys never meet a live key's. Keys are remembered for 7 days; after that the same key starts a new request.

A 503 with code service_unavailable (type dependency_failure) does not say the call was not done: it is also what you get when something failed after the call had taken effect. On a read, nothing was changed and you retry. On a write, whether it took effect is not known.

Retry the same request with the same Idempotency-Key, and never make a new key for the retry: a new key is a new request, and can pay, confirm or refund a second time.

  • A 503 frees the key, so the retry runs again; it is not a replay, and its answer carries no Idempotent-Replayed header. What stops it doing anything twice is what the call finds. Creating a payment intent or a refund finds the one this key made and answers with it. Confirming or canceling an intent already confirmed or canceled, and simulating an outcome for an intent already settled, answer the intent as it is.
  • If the retry is refused with conflict and code intent_state, the intent is no longer in a state that allows the call. The first call may have taken effect, or the customer may have acted on the intent since, for example by paying at checkout. Read the intent with GET /v1/payment_intents/{id} rather than send it again.
  • The one case in which the key is not freed: the failure came while the answer was being stored. The key is then held for five minutes, and a retry in that time answers 409 with idempotency_key_in_use; retry shortly, and after five minutes the retry runs again.
  • Calls without a key are the webhook endpoint calls. Updating or disabling an endpoint is safe to send again: the endpoint ends up as the first call would have left it. Creating one is not repeated for you: a second create makes a second endpoint. List your endpoints first; if it is there, its signing secret was shown only in the answer you did not get, so rotate the secret to read a new one. Rotating again makes another secret, so use the one in the answer you receive; an earlier one keeps signing for 24 hours and then stops. See Webhooks.
  • A terminal records cash with its own idempotency_key in the body, not this header. See At the counter.