Idempotency key
Retry writes safely with the Idempotency-Key header: scope, replays, the 409 and 422 answers, and the rule for 5xx.
Send an Idempotency-Key header on every call that creates or moves money. If the connection drops
before you read the answer, you resend the same request with the same key and get the original
outcome back instead of a second charge or refund.
curl -X POST https://api-payments-staging.highwirepayments.com/v1/payments/auth-and-capture \
-u "YOUR_API_KEY:YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0193c2e9-4f9a-7c2f-9c83-1b2b3c4d5e6f" \
-d '{
"amount": 7200,
"cardNumber": "5555555555554444",
"cardExpiration": "1133",
"cardCvv": "456",
"cardholderName": "Jordan Example",
"cardholderEmail": "jordan@example.com",
"cardholderPhone": "+14155550101",
"cardholderBillingAddress": "100 Example Street",
"cardholderBillingZipcode": "94105",
"cardholderBillingCity": "Springfield",
"cardholderBillingState": "CA"
}'Sending that request again with the same key and body, within 24 hours, replays the original
response byte for byte: same status, same paymentId.
The header
- Name:
Idempotency-Key. Optional, but strongly recommended on every write that accepts it. - Value: 1 to 255 characters, any string. A fresh UUID (v4 or v7) per logical operation is the recommended format. There is no character-set rule.
- Send it once. A request carrying the header twice answers
400Idempotency-Key header may only appear once per request; a value longer than 255 characters answers400Idempotency-Key must be 1..255 characters. - Scope: your account, the endpoint and the path parameters together. The same key string on
POST /v1/payments/{a}/refundandPOST /v1/payments/{b}/refundis two different operations, and another account's keys never interfere with yours. - Lifetime: 24 hours from the first call. After that the same string can be used for a new operation.
- Never put card data, e-mails or other personal identifiers in the key. It may appear in logs and support tools.
Which endpoints honour it
| Endpoint | Idempotency-Key |
|---|---|
POST /v1/payments/auth-and-capture | Honoured. |
POST /v1/payments/auth | Honoured. |
POST /v1/payments/charge-saved-method | Honoured. A 409 refusal with a reason is not stored, so the same key is evaluated again next time. |
POST /v1/payments/checkout-session | Honoured. A replay answers without checkoutUrl: the link is shown once. See The link. |
POST /v1/payments/{id}/checkout-session/link | Ignored. Every call issues a new link and revokes the previous one. |
POST /v1/payments/{id}/capture | Honoured. |
POST /v1/payments/{id}/refund | Honoured. The 409 for a disputed payment is not stored. |
POST /v1/payments/{id}/void | Ignored. A void is a state transition: a second void of the same payment answers 400 Payment has already been voided., and nothing is charged twice. |
GET /v1/payments/{id} | Not applicable. |
What a repeated call gets
| Situation | Response |
|---|---|
| First call with this key | Processed normally. The response, status included, is stored for 24 hours. |
| Same key, same body, first call finished | The stored response, verbatim. The processor is not called again. Nothing marks it as a replay. |
| Same key, different body | 422 Idempotency-Key was previously used with a different request body. Almost always a client bug. |
| Same key while the first call is still running | 409 A request with this Idempotency-Key is already in progress. Wait, then resend the same key to get the outcome. Do not switch to a new key until you know the outcome. |
First call answered a 4xx | The 4xx is stored too, validation errors and 429 included. A corrected or new attempt needs a new key. |
First call answered a 409 with a reason | Not stored. The same key is evaluated again on the next call. |
First call answered a 5xx | The record is discarded. Retry with the same key, unless the operation was accepted; see After a 5xx. |
First call answered 401 or 403 | Nothing is stored: authentication runs before the key is read. |
After a 5xx
A 500 that says the operation was accepted is the exception: Capture accepted; finalization pending. Poll the payment., and the same message for a void or a refund. The processor has
already taken the operation and only its recording is late. Do not retry it;
poll the payment until it reflects the operation.
A retry would only answer a plain 400 such as This payment has already been captured.
For any other 5xx:
- Retry with the same key and body. The record was discarded, so the retry is processed, and a retry under the same key cannot create a duplicate charge: if the first attempt went through, you get that outcome back.
- Expect that the retry may answer a plain
400such asThis payment has already been captured.orA refund is already in progress for this paymentwhen the first attempt went through but could not be recorded. Read the payment withGET /v1/payments/{id}to confirm. - If retries keep failing, treat it as an outage and contact High Wire with the key and a timestamp so the operation can be reconciled. Do not move to a fresh key: a fresh key has no relation to the first attempt and can charge twice.
Choosing a key
- One key per logical operation: one per charge, one per refund of a given payment, one per checkout. Reuse it only to retry that same operation.
- Generate the key when the operation starts and keep it, with the request, in memory for the retry loop only. Never write card data (number, expiry, security code) to disk, a queue or a log in order to retry later. If your process is lost mid-request, read the payment back; if nothing was recorded, collect the card again.
- Composite keys such as
order-8812-refund-1work, but they make accidental reuse across operations easy, and that reuse answers422or, worse, replays an unrelated stored response.