docs
Integrate

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

  1. You generate a unique key for one logical operation and send it in the Idempotency-Key header.
  2. XPay records the response of the first request that used that key.
  3. Every later request with the same key returns that stored response, with an Idempotent-Replayed: true header. 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 400 with error.code idempotency_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. See idempotency_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.

AspectRequest idempotency (this page)Webhook event idempotency
DirectionYou call XPayXPay calls you
KeyIdempotency-Key header you sendevent.id you read
Who handles duplicatesXPayYour handler

For the webhook side, see Verifying signatures → Idempotency.

Where to next

On this page