# Idempotency (/en/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.

<Callout type="info">
  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.
</Callout>

## How it works [#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 [#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.

<Tabs items="[&#x22;cURL&#x22;, &#x22;Node.js&#x22;]">
  <Tab value="cURL">
    ```bash
    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"
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    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",
      }),
    });
    ```
  </Tab>
</Tabs>

Reuse the same key when you retry the same operation. Generate a fresh key for a genuinely new operation.

## Choosing a key [#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 [#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`](/integrate/errors/api-error-codes#idempotency_key_in_use).

## Requests still in progress [#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 [#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 [#which-requests-support-it]

Send `Idempotency-Key` on the write endpoints that create or change a resource:

* <ApiLink href="/api-reference/objects/refund">Refund</ApiLink> creation, `POST /refunds`. The most
  important one.
* <ApiLink href="/api-reference/objects/checkout-session">Checkout Session</ApiLink> 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 [#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](/integrate/webhooks/verifying-signatures#idempotency).

## Where to next [#where-to-next]

<Cards>
  <Card icon="<RefreshCw />" title="Refunds" href="/integrate/refunds">
    The route where an Idempotency-Key matters most.
  </Card>

  <Card icon="<KeyRound />" title="API error codes" href="/integrate/errors/api-error-codes">
    The `idempotency_key_in_use` error and how to resolve it.
  </Card>

  <Card icon="<Webhook />" title="Verifying signatures" href="/integrate/webhooks/verifying-signatures">
    Dedupe webhook deliveries on `event.id`.
  </Card>

  <Card icon="<Compass />" title="Choose your integration" href="/get-started/choose-your-integration">
    Pick the integration pattern that fits your stack.
  </Card>
</Cards>