Idempotency
Safely retry write requests with an Idempotency-Key. XPay records the first result and replays it, so a retried call never repeats the side effect.
Network calls fail in ways that leave you unsure whether the request went through. An Idempotency-Key lets you retry a write safely: XPay records the result of the first request that used a given key and returns that same result for every retry, so the operation runs once even if you send it five times.
Send an Idempotency-Key on any write you might retry. It matters most on POST /refunds, where
a blind retry after a timeout would otherwise refund the customer twice.
How it works
- You generate a unique key for one logical operation and send it in the
Idempotency-Keyheader. - XPay records the response of the first request that used that key.
- Every later request with the same key returns that stored response, with an
Idempotent-Replayed: trueheader. Your operation runs once.
Keys are scoped to your account and mode (Test or Live), and they expire after 24 hours.
Send an Idempotency-Key
Add the header to any supported write. The key is yours to generate. A version-4 UUID is a good default.
curl -X POST https://api.xpay.app/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: 5f0c9b1a-4e2d-4a6f-9c3b-7d1e0a2f4b88" \
-H "Content-Type: application/json" \
-d '{
"paymentIntentId": "pi_test_xyz789",
"amount": 50000,
"reason": "requested_by_customer"
}'import { randomUUID } from "node:crypto";
const res = await fetch("https://api.xpay.app/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
paymentIntentId: "pi_test_xyz789",
amount: 50000,
reason: "requested_by_customer",
}),
});Reuse the same key when you retry the same operation. Generate a fresh key for a genuinely new operation.
Choosing a key
- Use one key per logical operation: one refund attempt, one Checkout Session create. Store it next to the operation you are retrying so the retry sends the identical value.
- Make it unique and hard to guess. A version-4 UUID works well. A key is at most 255 characters.
- Do not reuse one key for unrelated operations. Two different refunds need two different keys.
Reusing a key
What XPay returns on a repeat depends on whether the request matches the first one that used the key:
- Same key, same request returns the same response it sent the first time, with
Idempotent-Replayed: true. The handler does not run again, so no second refund, charge, or record is created. - Same key, different request returns
400witherror.codeidempotency_key_in_use. A key is bound to the first request it was used with (method, path, query, and body). To send a different request, use a new key. Seeidempotency_key_in_use.
Requests still in progress
If a second request arrives with the same key while the first is still running, XPay returns 409 with a Retry-After header. Wait the number of seconds in Retry-After, then retry. By then the first request has finished, and the retry returns its stored result.
What gets stored, and for how long
XPay stores the first definitive result for each key:
- A success, or an error the request produced after it started processing (for example a declined card), is stored and replayed. To run that operation again, use a new key.
- A request rejected before processing (a validation error), or one that hit a temporary failure, is not stored. Retry it with the same key and it runs fresh.
Stored results expire after 24 hours. After that, the same key is treated as new.
Which requests support it
Send Idempotency-Key on the write endpoints that create or change a resource:
- RefundAPI creation,
POST /refunds. The most important one. - Checkout SessionAPI create, update, and expire.
- Product create and update, Price create and update.
- Payment Link create and update.
- Customer create.
GET and DELETE requests are already idempotent, so the header has no effect on them.
Idempotency vs webhook events
This page is about requests you send to XPay. There is a separate idempotency concern for the webhooks XPay sends you: the same event can arrive more than once, so your handler must skip duplicates by event.id. The two are independent.
| Aspect | Request idempotency (this page) | Webhook event idempotency |
|---|---|---|
| Direction | You call XPay | XPay calls you |
| Key | Idempotency-Key header you send | event.id you read |
| Who handles duplicates | XPay | Your handler |
For the webhook side, see Verifying signatures → Idempotency.