> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Prevent duplicate payments with idempotency keys

> Pass an idempotency key in Auto flow to prevent duplicate payment creation, and learn when to reuse vs rotate it.

Pass an idempotency key when creating payments to ensure the same payment is never created twice.

For background on how idempotency keys work at the API level, see **Idempotency Key**.

## Rule of thumb

* **Same idempotency key = same payment attempt** (safe retries, no duplicates)
* **New idempotency key = new payment attempt** (creates a new payment)

If you reuse a key for a new attempt, it will trigger a failure with error code `IDEMPOTENCY_KEY_ALREADY_EXISTS`.

## How it works

In Auto flow, the SDK creates the payment by calling the Payments API internally.

<Tabs>
  <Tab title="Web">
    When you provide an idempotency key via `continuePaymentCreation({ idempotencyKey })` in the `primer:payment-start` event, the SDK includes it as the `X-Idempotency-Key` header on:

    * `POST /payments` (create payment)

    If the same request is retried with the same key:

    * The API will not create a second payment.
    * It will fail with a `409 Conflict` response.
  </Tab>

  <Tab title="Android">
    Idempotency key support is not yet available in the Android Components SDK. This feature is coming in a future release.
  </Tab>

  <Tab title="iOS">
    When you provide an idempotency key via `handler.continuePaymentCreation(withIdempotencyKey:)` in the `onBeforePaymentCreate` handler, the SDK includes it as the `X-Idempotency-Key` header on:

    * `POST /payments` (create payment)

    The key is **not** sent on `resumePayment` — only on the initial create call. The SDK automatically clears the key after payment creation (success or failure).

    If the same request is retried with the same key:

    * The API will not create a second payment.
    * It will fail with a `409 Conflict` response.
  </Tab>
</Tabs>

### Why key rotation matters

For redirect and 3DS flows, the payment is created **before** the user completes the external step.

If the user abandons the redirect or retries after a failed authentication, this becomes a **new payment attempt**, and the key must be rotated.

If you do not rotate the key, the retry will fail with an error code `IDEMPOTENCY_KEY_ALREADY_EXISTS`

## When to reuse vs rotate

### Reuse the same key

Reuse the current key when you are retrying the **same attempt**, for example:

* transient network retry
* the SDK retries the same request
* resume of the same attempt

### Rotate the key

Rotate to a new key when the user starts a **new attempt**, for example:

* user abandons a redirect, popup, or external app flow and tries again
* user cancels the payment and tries again
* failed 3DS and the user retries as a new attempt
* user switches payment method (optional but recommended)

## Recipe

<Tabs>
  <Tab title="Web">
    In this recipe we:

    1. Intercept payment creation with `primer:payment-start`
    2. Provide an `idempotencyKey` per attempt
    3. Rotate the attempt when the user abandons a redirect flow using `primer:payment-cancel`
    4. Handle duplicate key errors explicitly

    ```js theme={"dark"}
    let attempt = 1;
    let currentIdempotencyKey = null;

    // Optional: track whether we should rotate on the next attempt
    let shouldRotateOnNextStart = false;

    // You can generate locally (UUID) or fetch from your backend.
    // Backend is recommended if you want stronger guarantees across refresh/outages.
    async function getIdempotencyKeyForAttempt(attemptNumber) {
      // Example 1: local generation
      // return crypto.randomUUID();

      // Example 2: backend generation (recommended)
      const res = await fetch(`/api/idempotency-key?attempt=${attemptNumber}`, { method: 'POST' });
      const data = await res.json();
      return data.idempotencyKey;
    }

    // 1) Inject the key when the payment starts
    checkout.addEventListener('primer:payment-start', async (event) => {
      event.preventDefault();

      // If we detected a new attempt boundary, rotate attempt counter
      if (shouldRotateOnNextStart) {
        attempt += 1;
        currentIdempotencyKey = null;
        shouldRotateOnNextStart = false;
      }

      // Create the key once per attempt and reuse it for retries of the same attempt
      if (!currentIdempotencyKey) {
        currentIdempotencyKey = await getIdempotencyKeyForAttempt(attempt);
      }

      event.detail.continuePaymentCreation({ idempotencyKey: currentIdempotencyKey });
    });

    // 2) Detect the user abandoning a redirect or popup based flow
    // This event is the key signal to know the next click is a new attempt.
    checkout.addEventListener('primer:payment-cancel', () => {
      shouldRotateOnNextStart = true;
    });

    // 3) Handle the duplicate key error explicitly
    checkout.addEventListener('primer:payment-failure', (event) => {
      const { error } = event.detail;

      if (error?.code === 'IDEMPOTENCY_KEY_ALREADY_EXISTS') {
        // This means either:
        // - The cancel event was not handled (and the key was not rotated), or
        // - A payment was already successfully created with this key.

        // Suggested UX:
        // - Check the payment status server-side to see if a previous attempt succeeded.
        //   If so, redirect the user to a completion/confirmation page.
        // - Otherwise, tell the user to retry (after rotating the key).

        // Ensure the next attempt uses a new key.
        shouldRotateOnNextStart = true;
      }
    });
    ```
  </Tab>

  <Tab title="Android">
    Idempotency key support is not yet available in the Android Components SDK. This feature is coming in a future release.
  </Tab>

  <Tab title="iOS">
    In this recipe we:

    1. Intercept payment creation with `onBeforePaymentCreate`
    2. Provide an idempotency key per attempt
    3. Rotate the key when the checkout is dismissed or fails
    4. Handle duplicate key errors explicitly

    ```swift theme={"dark"}
    struct CheckoutView: View {
        let clientToken: String
        @State private var attempt = 1
        @State private var currentIdempotencyKey: String?

        var body: some View {
            PrimerCheckout(
                clientToken: clientToken,
                scope: { checkoutScope in
                    // 1) Inject the key when the payment starts
                    checkoutScope.onBeforePaymentCreate = { _, handler in
                        // Create the key once per attempt and reuse for retries
                        if currentIdempotencyKey == nil {
                            currentIdempotencyKey = UUID().uuidString
                        }
                        handler.continuePaymentCreation(
                            withIdempotencyKey: currentIdempotencyKey
                        )
                    }
                },
                onCompletion: { state in
                    switch state {
                    // 2) Payment succeeded — rotate key for any future attempt
                    case .success:
                        rotateKey()
                    // 3) Handle the duplicate key error explicitly
                    case .failure(let error):
                        if error.errorId == "IDEMPOTENCY_KEY_ALREADY_EXISTS" {
                            rotateKey()
                        }
                    // 4) User dismissed checkout — rotate key for next attempt
                    case .dismissed:
                        rotateKey()
                    default:
                        break
                    }
                }
            )
        }

        private func rotateKey() {
            currentIdempotencyKey = nil
            attempt += 1
        }
    }
    ```
  </Tab>
</Tabs>

## Handling network loss and state recovery

Idempotency prevents duplicate charges, but it does **not** fully manage recovery when the client loses state.

<Tabs>
  <Tab title="Web">
    If the network drops or the page refreshes right after `createPayment` is sent:

    * The payment may have been created on the server, even if the client never received a response.
    * Retrying with the **same idempotency key** is the safest client-side action for the same attempt.
    * If the client lost state (e.g., after a page refresh), it cannot reliably know if the payment succeeded, is pending, or failed.

    If you need bulletproof recovery (across refresh or outages), you should:

    * Keep a server-side order state, and/or
    * Listen to webhooks, and/or
    * Use manual payment flow for full lifecycle control.
  </Tab>

  <Tab title="Android">
    Idempotency key support is not yet available in the Android Components SDK. This feature is coming in a future release.
  </Tab>

  <Tab title="iOS">
    If the app is backgrounded, the process is terminated, or the network drops right after payment creation is initiated:

    * The payment may have been created on the server, even if the app never received a response.
    * If the user re-opens the app and triggers a new payment, using the **same idempotency key** prevents a duplicate charge.
    * The client cannot reliably know if the previous payment succeeded, is pending, or failed.

    If you need bulletproof recovery (across app termination or outages), you should:

    * Persist the idempotency key and attempt counter (e.g., in `UserDefaults` or a local database)
    * Keep a server-side order state, and/or
    * Listen to webhooks
  </Tab>
</Tabs>

***

## Common pitfalls

### Rotating the key on every click

If you generate a new key every time, you reduce the protection against true request retries.

### Reusing the same key after abandon

If the user cancels a redirect flow and tries again with the same key, the API will fail with a `409`.

### Treating idempotency errors as generic failures

`IDEMPOTENCY_KEY_ALREADY_EXISTS` is often expected behavior.\
Handle it explicitly and guide the user instead of showing a generic error.

***

## See also

<CardGroup cols={2}>
  <Card title="Idempotency Key" icon="key" href="/api-reference/get-started/idempotency-key">
    How idempotency keys work at the API level
  </Card>

  <Card title="Events guide" icon="bolt" href="/checkout/primer-checkout/configuration/events">
    Platform-specific events and interception patterns
  </Card>

  <Card title="Redirect after payment" icon="arrow-right" href="/checkout/primer-checkout/guides-and-recipes/redirect-after-payment">
    Navigate to a confirmation page after payment
  </Card>

  <Card title="Log errors for debugging" icon="bug" href="/checkout/primer-checkout/guides-and-recipes/log-errors-debugging">
    Capture and log payment errors for debugging
  </Card>
</CardGroup>
