> ## 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.

# Events guide

> Learn when, why, and how to use Primer Checkout events to build robust payment flows

This guide explains the event-driven model behind Primer Checkout and walks through the integration decisions you'll face when wiring events into your application. It is organized around the questions you'll encounter as you build: where to listen, what to set up first, and how to handle each phase of the payment lifecycle.

<Tip title="Guide vs Reference">
  This guide focuses on **when and why** to use each event. For the complete API surface, every event name, payload shape, type definition, and callback signature see the platform-specific SDK references.
</Tip>

## How events work

<Tabs>
  <Tab title="Web">
    Primer Checkout components dispatch [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) objects. Every Primer event is created with `bubbles: true` and `composed: true`, which means events propagate up through shadow DOM boundaries and can be caught at any ancestor element, including `document`. This design gives you two choices for where to listen, covered in the next section.
  </Tab>

  <Tab title="Android">
    The Android SDK communicates through two channels:

    * **State** (`PrimerCheckoutState`): UI lifecycle -- loading, ready
    * **Events** (`PrimerCheckoutEvent`): Payment outcomes -- success, failure, token created

    State is a `StateFlow` you observe in Compose. Events are delivered via the `onEvent` callback on `PrimerCheckoutSheet` or `PrimerCheckoutHost`.
  </Tab>

  <Tab title="iOS">
    The iOS SDK communicates through two mechanisms:

    * **AsyncStream** (`PrimerCheckoutScope.state`): Continuous state observation -- initializing, ready, success, failure, dismissed
    * **onCompletion callback**: Fires once when the checkout reaches a terminal state

    State is observed via Swift's `AsyncStream` inside the `scope` closure. The `onCompletion` callback delivers the final result.
  </Tab>
</Tabs>

## Choosing where to listen

Each platform has its own mechanism for receiving SDK events and state changes.

<Tabs>
  <Tab title="Web">
    <Tabs>
      <Tab title="Component Level">
        Attach listeners directly to the `<primer-checkout>` element. Each listener is scoped to a single checkout instance.

        ```javascript theme={"dark"}
        const checkout = document.querySelector('primer-checkout');

        checkout.addEventListener('primer:ready', (event) => {
          // This listener only fires for this checkout instance
          const primer = event.detail;
          configureCallbacks(primer);
        });
        ```

        **Choose this when:**

        * You want to co-locate event logic with the component that owns it, such as inside a framework component's lifecycle hook.
        * You need to tear down listeners cleanly when a checkout is removed from the DOM.
      </Tab>

      <Tab title="Document Level">
        Attach listeners to `document`. Because Primer events bubble and are composed, they reach the document root regardless of where the component sits in the DOM.

        ```javascript theme={"dark"}
        document.addEventListener('primer:payment-success', (event) => {
          // Fires when any Primer event bubbles up to document
          analytics.track('payment_completed', event.detail);
        });
        ```

        **Choose this when:**

        * You have a single checkout per page and prefer centralized event handling.
        * You're building cross-cutting concerns like analytics, logging, or error monitoring.
        * Your architecture already uses a global event bus pattern.
      </Tab>
    </Tabs>

    ### Ensuring the component exists

    When using component-level listeners, make sure the element is in the DOM before attaching them. If your checkout renders dynamically (through a router transition, a conditional template, or lazy loading), the element may not exist at script-execution time.

    ```javascript theme={"dark"}
    // Safe: wait for the DOM to be ready before querying
    document.addEventListener('DOMContentLoaded', () => {
      const checkout = document.querySelector('primer-checkout');
      if (checkout) {
        checkout.addEventListener('primer:payment-success', handlePaymentSuccess);
        checkout.addEventListener('primer:payment-failure', handlePaymentFailure);
      }
    });
    ```

    If your framework provides a lifecycle hook that runs after the component mounts (React's `useEffect`, Vue's `onMounted`, etc.), prefer that over `DOMContentLoaded`.
  </Tab>

  <Tab title="Android">
    Events are delivered via the `onEvent` callback on `PrimerCheckoutSheet` or `PrimerCheckoutHost`. State is observed from `checkout.state`:

    ```kotlin theme={"dark"}
    val checkout = rememberPrimerCheckoutController(clientToken)
    val state by checkout.state.collectAsStateWithLifecycle()

    when (state) {
        is PrimerCheckoutState.Loading -> {
            CircularProgressIndicator()
        }
        is PrimerCheckoutState.Ready -> {
            val clientSession = (state as PrimerCheckoutState.Ready).clientSession
            Text("Total: ${checkout.formatAmount(clientSession.totalAmount ?: 0)}")
            PrimerCheckoutSheet(checkout = checkout)
        }
    }
    ```

    ### PrimerCheckoutState

    | State     | Description                                 | Properties                           |
    | --------- | ------------------------------------------- | ------------------------------------ |
    | `Loading` | SDK is initializing, fetching configuration | --                                   |
    | `Ready`   | Checkout is ready for payment               | `clientSession: PrimerClientSession` |

    ### PrimerClientSession

    Available in the `Ready` state:

    | Property        | Type                    | Description                             |
    | --------------- | ----------------------- | --------------------------------------- |
    | `customerId`    | `String?`               | Customer identifier                     |
    | `orderId`       | `String?`               | Order identifier                        |
    | `currencyCode`  | `String?`               | Currency code (e.g., `"USD"`)           |
    | `totalAmount`   | `Int?`                  | Total amount in minor units (cents)     |
    | `lineItems`     | `List<PrimerLineItem>?` | Order line items                        |
    | `orderDetails`  | `PrimerOrder?`          | Order details (country, shipping)       |
    | `customer`      | `PrimerCustomer?`       | Customer details (email, name, address) |
    | `paymentMethod` | `PrimerPaymentMethod?`  | Payment method configuration            |
    | `fees`          | `List<PrimerFee>?`      | Associated fees                         |
  </Tab>

  <Tab title="iOS">
    State is observed inside the `scope` closure on `PrimerCheckout`. The `onCompletion` callback handles the terminal result:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        Task {
          for await state in checkoutScope.state {
            switch state {
            case .initializing:
              print("Loading...")
            case .ready(let totalAmount, let currencyCode):
              print("Ready: \(totalAmount) \(currencyCode)")
            case .success(let result):
              print("Payment ID: \(result.payment?.id ?? "")")
            case .failure(let error):
              print("Failed: \(error.errorDescription ?? "")")
            case .dismissed:
              print("Dismissed")
            }
          }
        }
      }
    )
    ```

    ### PrimerCheckoutState

    | State           | Description                   | Associated Values                          |
    | --------------- | ----------------------------- | ------------------------------------------ |
    | `.initializing` | SDK is loading configuration  | --                                         |
    | `.ready`        | Checkout is ready for payment | `totalAmount: Int`, `currencyCode: String` |
    | `.success`      | Payment completed             | `PaymentResult`                            |
    | `.failure`      | Payment failed                | `PrimerError`                              |
    | `.dismissed`    | Checkout was dismissed        | --                                         |
  </Tab>
</Tabs>

## Phase 1: Initialization

Every Primer Checkout integration begins with initialization. The SDK must fully initialize before you can pre-fill form fields or call SDK methods.

<Tabs>
  <Tab title="Web">
    The `primer:ready` event fires once after the SDK has fully initialized. It delivers the `PrimerJS` instance as `event.detail`.

    ```javascript theme={"dark"}
    const checkout = document.querySelector('primer-checkout');

    checkout.addEventListener('primer:ready', (event) => {
      const primer = event.detail;

      // Pre-fill known data (optional)
      const user = getAuthenticatedUser();
      if (user?.fullName) {
        primer.setCardholderName(user.fullName);
      }

      // Access available methods immediately if needed
      const methods = primer.getPaymentMethods();
      console.log(`${methods.length} payment method(s) available at init`);
    });

    // Handle payment outcomes via events
    checkout.addEventListener('primer:payment-success', (event) => {
      const { payment, paymentMethodType } = event.detail;
      console.log('Payment successful:', payment.last4Digits);
      window.location.href = `/confirmation?method=${paymentMethodType}`;
    });

    checkout.addEventListener('primer:payment-failure', (event) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
      // The checkout UI already displays the error to the user.
      // Use this event for logging, analytics, or retry logic.
    });
    ```

    <Warning>
      If you don't listen for `primer:payment-success` and `primer:payment-failure`, successful and failed payments will complete silently with no redirect or confirmation. Always attach these event listeners.
    </Warning>
  </Tab>

  <Tab title="Android">
    The checkout controller initializes automatically when created with `rememberPrimerCheckoutController()`. Event handling is set up via the `onEvent` callback:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> {
                    val payment = event.checkoutData
                    Log.d("Checkout", "Payment ID: ${payment.payment.id}")
                    navigateToConfirmation(payment)
                }
                is PrimerCheckoutEvent.Failure -> {
                    val error = event.error
                    Log.e("Checkout", "Failed: ${error.description}")
                    Log.e("Checkout", "Diagnostics: ${error.diagnosticsId}")
                }
            }
        },
    )
    ```
  </Tab>

  <Tab title="iOS">
    The checkout initializes automatically when the `PrimerCheckout` SwiftUI view appears. Use the `scope` closure to observe state and the `onCompletion` callback for the final result:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        Task {
          for await state in checkoutScope.state {
            if case .ready(let totalAmount, let currencyCode) = state {
              print("Ready: \(totalAmount) \(currencyCode)")
            }
          }
        }
      },
      onCompletion: { state in
        switch state {
        case .success(let result):
          print("Payment ID: \(result.payment?.id ?? "")")
          navigateToConfirmation(result)
        case .failure(let error):
          print("Failed: \(error.errorDescription ?? "")")
          print("Diagnostics: \(error.diagnosticsId)")
        case .dismissed:
          navigateBack()
        default:
          break
        }
      }
    )
    ```

    <Warning>
      If you don't provide an `onCompletion` callback, successful and failed payments will complete with no redirect or confirmation. Always provide this callback.
    </Warning>
  </Tab>
</Tabs>

## Phase 2: Payment method discovery

Shortly after initialization, the SDK provides information about available payment methods for the current session.

<Tabs>
  <Tab title="Web">
    The SDK dispatches `primer:methods-update` with the list of payment methods available for the current session. This event fires once on load and may fire again if the session changes (e.g. after calling `primer.refreshSession()` in response to a cart update).

    You should use this event when you want to build a custom payment method selector, conditionally render UI based on what's available, or route users to a specific method.

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:methods-update', (event) => {
      const methods = event.detail;

      // Conditionally show an "Express Checkout" section only if Apple Pay or Google Pay is available
      const hasExpressMethod = methods.toArray().some(
        (m) => m.type === 'APPLE_PAY' || m.type === 'GOOGLE_PAY'
      );

      document.getElementById('express-checkout-section').hidden = !hasExpressMethod;
    });
    ```

    If you're building a fully custom payment method layout (headless), you'll also iterate over the methods to create `<primer-payment-method>` elements dynamically. The [Headless Vault Guide](/sdk/primer-checkout-web/components/primer-vault-manager#headless-vault-implementation) covers that pattern in detail.
  </Tab>

  <Tab title="Android">
    Payment methods are managed automatically by the SDK components (`PrimerPaymentMethods`, `PrimerVaultedPaymentMethods`). You can access the client session details from the `Ready` state:

    ```kotlin theme={"dark"}
    val state by checkout.state.collectAsStateWithLifecycle()

    when (state) {
        is PrimerCheckoutState.Ready -> {
            val clientSession = (state as PrimerCheckoutState.Ready).clientSession
            Text("Total: ${checkout.formatAmount(clientSession.totalAmount ?: 0)}")
        }
    }
    ```
  </Tab>

  <Tab title="iOS">
    Payment methods are available through the `PrimerPaymentMethodSelectionScope`. Observe its state to track available and selected methods:

    ```swift theme={"dark"}
    Task {
      for await state in checkoutScope.paymentMethodSelection.state {
        print("Methods: \(state.paymentMethods.count)")
        if let selected = state.selectedPaymentMethod {
          print("Selected: \(selected.name)")
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Phase 3: User interaction

As the user fills in payment details, the SDK provides real-time feedback.

### Showing the Card Network

<Tabs>
  <Tab title="Web">
    `primer:bin-data-available` fires as the user types their card number. It provides the detected card network, co-badged network alternatives, and additional BIN data such as issuer information when available. Use it to display the correct card brand logo, show a co-badge network picker, or adjust UI based on card attributes.

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:bin-data-available', (event) => {
      const { preferred, alternatives, status } = event.detail;
      const logoEl = document.getElementById('card-logo');
      const cobrandEl = document.getElementById('cobrand-selector');

      if (preferred) {
        logoEl.src = `/images/${preferred.network.toLowerCase()}.svg`;
        logoEl.alt = preferred.displayName;
      }

      // Some cards support co-badging (e.g. Carte Bancaire / Visa).
      // If there are multiple allowed networks, show a picker.
      const selectableNetworks = [preferred, ...alternatives].filter(n => n?.allowed);
      cobrandEl.hidden = selectableNetworks.length <= 1;

      // When status is 'complete', additional issuer data is available
      if (status === 'complete' && preferred?.issuerCountryCode) {
        console.log(`Card issued in: ${preferred.issuerCountryCode}`);
      }
    });
    ```
  </Tab>

  <Tab title="Android">
    The Android SDK handles card network detection and display automatically within the `PrimerCardForm` component. Card brand icons update in real-time as the user types.
  </Tab>

  <Tab title="iOS">
    Card network detection is available via `PrimerCardFormState`. Observe the `selectedNetwork` and `availableNetworks` properties as the user types:

    ```swift theme={"dark"}
    Task {
      let cardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self)!
      for await state in cardFormScope.state {
        if let network = state.selectedNetwork {
          print("Detected network: \(network)")
        }
        // Co-badged cards expose multiple networks
        if state.availableNetworks.count > 1 {
          print("Co-badge options: \(state.availableNetworks)")
        }
      }
    }
    ```
  </Tab>
</Tabs>

To show a loading indicator while BIN data is being fetched, listen for `primer:bin-data-loading-change`:

```javascript theme={"dark"}
checkout.addEventListener('primer:bin-data-loading-change', (event) => {
  const { loading } = event.detail;
  document.getElementById('card-logo').src = loading
    ? '/images/card-placeholder.svg'
    : document.getElementById('card-logo').src;
});
```

<Note>
  `primer:bin-data-available` replaces the older `primer:card-network-change` event with a richer payload that includes issuer details and card attributes. See the [Events Reference](/sdk/primer-checkout-web/events-reference#primerbin-data-available) for the full payload shape.
</Note>

### Triggering Submission from a Custom Button

<Tabs>
  <Tab title="Web">
    If you're using your own "Pay" button instead of the built-in one, dispatch `primer:card-submit` to tell the SDK to validate and submit the card form.

    ```javascript theme={"dark"}
    document.getElementById('custom-pay-button').addEventListener('click', () => {
      document.dispatchEvent(
        new CustomEvent('primer:card-submit', {
          bubbles: true,
          composed: true,
        }),
      );
    });
    ```

    <Warning>
      `bubbles: true` and `composed: true` are required so the event crosses shadow DOM boundaries and reaches the card form component inside `<primer-checkout>`. Omitting either option will silently prevent submission.
    </Warning>

    For vault payment submission from a custom button, dispatch `primer:vault-submit` in the same way. See the [Events Reference and Triggerable Events](/sdk/primer-checkout-web/events-reference#triggerable-events) for the full list of events you can dispatch.
  </Tab>

  <Tab title="Android">
    The Android SDK handles user interaction through Compose components. Card form state is managed via `PrimerCardFormController`:

    ```kotlin theme={"dark"}
    val cardFormController = rememberCardFormController()
    ```

    The SDK provides built-in components for card input (`PrimerCardForm`), payment method selection (`PrimerPaymentMethods`), and vaulted methods (`PrimerVaultedPaymentMethods`). These handle user interaction automatically.
  </Tab>

  <Tab title="iOS">
    If you're building a custom card form UI, use the `PrimerCardFormScope` to update fields and trigger submission programmatically:

    ```swift theme={"dark"}
    Task {
      let cardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self)!

      // Update fields as the user types
      cardFormScope.updateCardNumber("4242424242424242")
      cardFormScope.updateCardholderName("John Doe")

      // Submit when your custom button is tapped
      cardFormScope.submit()
    }
    ```

    Use `cardFormScope.cancel()` to cancel and return to payment method selection.
  </Tab>
</Tabs>

## Phase 4: Payment processing and outcome

Once the user submits, the SDK moves through a processing to outcome sequence.

<Tabs>
  <Tab title="Web">
    The `primer:state-change` event fires at every step, giving you a single stream to drive your UI state.

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:state-change', (event) => {
      const { isLoading, isProcessing, isSuccessful, paymentFailure, primerJsError } = event.detail;
      const submitBtn = document.getElementById('custom-pay-button');
      const spinner = document.getElementById('loading-overlay');

      // Disable the button during loading or processing
      submitBtn.disabled = isLoading || isProcessing;

      // Show a loading overlay while the payment is in flight
      spinner.hidden = !isProcessing;

      if (isSuccessful) {
        // Payment succeeded — primer:payment-success will also fire
        document.getElementById('checkout-form').hidden = true;
      }

      if (paymentFailure) {
        // The checkout component already displays the error to the user.
        // Use this for supplementary logging or analytics.
        console.error(`Payment error [${paymentFailure.code}]: ${paymentFailure.message}`);
      }

      if (primerJsError) {
        // An SDK-level error (network failure, configuration issue, etc.)
        // Unlike paymentFailure, this may not be shown in the checkout UI.
        reportToErrorService(primerJsError);
      }
    });
    ```
  </Tab>

  <Tab title="Android">
    | Event     | When              | Key Properties                     |
    | --------- | ----------------- | ---------------------------------- |
    | `Success` | Payment completed | `checkoutData: PrimerCheckoutData` |
    | `Failure` | Payment failed    | `error: PrimerError`               |

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> {
                    val payment = event.checkoutData
                    Log.d("Checkout", "Payment ID: ${payment.payment.id}")
                    navigateToConfirmation(payment)
                }
                is PrimerCheckoutEvent.Failure -> {
                    val error = event.error
                    Log.e("Checkout", "Failed: ${error.description}")
                    Log.e("Checkout", "Diagnostics: ${error.diagnosticsId}")
                }
            }
        },
    )
    ```
  </Tab>

  <Tab title="iOS">
    Observe `PrimerCheckoutState` for every phase of the payment lifecycle. The `onCompletion` callback fires once at the terminal state:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        Task {
          for await state in checkoutScope.state {
            switch state {
            case .initializing:
              showSpinner()
            case .ready:
              hideSpinner()
            case .success(let result):
              print("Payment ID: \(result.payment?.id ?? "")")
              print("Status: \(result.payment?.status ?? "")")
            case .failure(let error):
              print("Error: \(error.errorDescription ?? "")")
              print("Diagnostics: \(error.diagnosticsId)")
            case .dismissed:
              break
            }
          }
        }
      },
      onCompletion: { state in
        if case .success(let result) = state {
          navigateToConfirmation(result)
        }
      }
    )
    ```
  </Tab>
</Tabs>

### State change vs. outcome events

`primer:state-change` fires multiple times during a single payment. The outcome events (`primer:payment-success`, `primer:payment-failure`) fire exactly once at the end. Use `primer:state-change` for continuous UI updates (spinners, button states, progress indicators). Use the outcome events for final actions (redirects, confirmations, server notifications).

### Event timing

Events may fire multiple times during a single checkout session (for example, if the user retries after a failure).

<Tabs>
  <Tab title="Web">
    * `primer:state-change` fires at every step — use it for continuous UI updates (spinners, button states)
    * `primer:payment-success` and `primer:payment-failure` fire exactly once at the end — use them for final actions
  </Tab>

  <Tab title="Android">
    * `Success` fires immediately — the default success screen auto-dismisses after 3 seconds
    * `Failure` fires immediately — the error screen stays open for retry
  </Tab>

  <Tab title="iOS">
    * `AsyncStream` emits at every state transition -- use it for continuous UI updates (spinners, button states)
    * `onCompletion` fires once at the terminal state -- use it for final actions (navigation, confirmation)
    * `.dismissed` fires when the user dismisses the checkout without completing payment
  </Tab>
</Tabs>

### Dismissal and session refresh

<Tabs>
  <Tab title="Web">
    Call `primer.refreshSession()` to sync the client-side SDK with your server session after a cart change. The SDK will re-dispatch `primer:methods-update` with the updated method list.
  </Tab>

  <Tab title="Android">
    Handle checkout dismissal via the `onDismiss` callback:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onDismiss = {
            navigateBack()
        },
    )
    ```

    Call `checkout.refresh()` to reinitialize the checkout after updating the order on your server.

    Use `checkout.formatAmount()` to display currency-formatted values:

    ```kotlin theme={"dark"}
    val formattedTotal = checkout.formatAmount(1000) // "$10.00"
    ```
  </Tab>

  <Tab title="iOS">
    Handle dismissal by observing the `.dismissed` state in `onCompletion`:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      onCompletion: { state in
        if case .dismissed = state {
          navigateBack()
        }
      }
    )
    ```
  </Tab>
</Tabs>

## Intercepting payments with primer:payment-start

Sometimes you need to run a check *after* the user clicks "Pay" but *before* the payment is created — for example, confirming terms-of-service acceptance, validating inventory, or applying a last-second promotion code.

<Tabs>
  <Tab title="Web">
    The `primer:payment-start` event gives you that interception point.

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:payment-start', (event) => {
      const { paymentMethodType, continuePaymentCreation, abortPaymentCreation } = event.detail;

      // Prevent automatic continuation
      event.preventDefault();

      const termsAccepted = document.getElementById('terms-checkbox').checked;

      if (!termsAccepted) {
        showInlineError('Please accept the terms of service before paying.');
        abortPaymentCreation();
        return;
      }

      // Optionally inspect paymentMethodType to apply method-specific logic
      continuePaymentCreation();
    });
    ```

    <Warning>
      If you call `event.preventDefault()`, you must call either `continuePaymentCreation()` or `abortPaymentCreation()` in every code path. If neither is called, the payment will hang indefinitely. If you don't call `preventDefault()`, the payment continues automatically.
    </Warning>
  </Tab>

  <Tab title="Android">
    <Note>Payment interception is not available in the composable module.</Note>
  </Tab>

  <Tab title="iOS">
    Use `onBeforePaymentCreate` on `PrimerCheckoutScope` to intercept payments after the user taps "Pay" but before the payment is created:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        checkoutScope.onBeforePaymentCreate = { handler in
          guard termsAccepted else {
            handler.abort()
            return
          }
          handler.continuePaymentCreation()
        }
      }
    )
    ```

    <Warning>
      You must call either `handler.continuePaymentCreation()` or `handler.abort()` in every code path. If neither is called, the payment will hang indefinitely.
    </Warning>
  </Tab>
</Tabs>

`continuePaymentCreation` accepts an optional `{ idempotencyKey }` parameter. When provided, this key is sent with the payment request to prevent duplicate payments from being created. See the [Events Reference](/sdk/primer-checkout-web/events-reference#preparehandler) for the full type definition.

## Working with Vaulted Payment Methods

If your integration supports saved (vaulted) payment methods, additional events become relevant.

<Tabs>
  <Tab title="Web">
    * **`primer:vault-methods-update`** — fires when vaulted payment methods are loaded or when the vault state changes. Use it to render saved cards, show a "Pay with saved card" section, or determine whether CVV re-entry is required.
    * **`primer:vault-selection-change`** — fires when the user selects or deselects a saved method. Use it to enable or disable your submit button, or to toggle between "pay with saved card" and "pay with new card" views.

    ```javascript theme={"dark"}
    let cvvRequired = false;

    checkout.addEventListener('primer:vault-methods-update', (event) => {
      const { vaultedPayments, cvvRecapture } = event.detail;
      cvvRequired = cvvRecapture;

      const savedMethodsSection = document.getElementById('saved-methods');
      if (vaultedPayments.length > 0) {
        savedMethodsSection.hidden = false;
        renderSavedMethodsList(vaultedPayments);
      } else {
        savedMethodsSection.hidden = true;
      }
    });

    checkout.addEventListener('primer:vault-selection-change', (event) => {
      const { paymentMethodId } = event.detail;
      const vaultSubmitBtn = document.getElementById('vault-pay-button');

      // Enable the button only when a method is selected
      vaultSubmitBtn.disabled = !paymentMethodId;

      // If CVV re-entry is required, show the CVV input when a method is selected
      if (paymentMethodId && cvvRequired) {
        showCvvInput(paymentMethodId);
      }
    });
    ```

    For full headless vault implementation, including `primer.vault.createCvvInput()`, `primer.vault.startPayment()`, and `primer.vault.delete()` see the [Headless Vault Guide](/sdk/primer-checkout-web/components/primer-vault-manager#headless-vault-implementation).
  </Tab>

  <Tab title="Android">
    Vaulted payment methods are managed by the `PrimerVaultedPaymentMethods` component and `rememberVaultedPaymentMethodsController()`:

    ```kotlin theme={"dark"}
    val vaultController = rememberVaultedPaymentMethodsController()
    ```

    The SDK provides built-in UI for displaying and selecting saved payment methods within `PrimerCheckoutSheet`.
  </Tab>

  <Tab title="iOS">
    Vaulted payment methods appear in the `PrimerPaymentMethodSelectionScope`. Observe the state to track available saved methods and user selection:

    ```swift theme={"dark"}
    Task {
      for await state in checkoutScope.paymentMethodSelection.state {
        let vaultedMethods = state.paymentMethods.filter { $0.isVaulted }
        print("Saved methods: \(vaultedMethods.count)")

        if let selected = state.selectedPaymentMethod {
          checkoutScope.paymentMethodSelection.onPaymentMethodSelected(
            paymentMethod: selected
          )
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Event flow overview

The following diagram shows the typical sequence of events in a successful card payment, from initialization through confirmation.

```mermaid theme={"dark"}
sequenceDiagram
    participant App as Your App
    participant SDK as Primer SDK

    Note over App,SDK: Phase 1 - Initialization
    SDK->>App: SDK ready
    Note right of App: Register callbacks, pre-fill fields

    Note over App,SDK: Phase 2 - Discovery
    SDK->>App: Payment methods available
    SDK->>App: Vaulted methods loaded
    Note right of App: Render payment method UI

    Note over App,SDK: Phase 3 - User Interaction
    App->>SDK: User enters card details
    SDK->>App: Card network detected
    Note right of App: Show card brand logo

    Note over App,SDK: Phase 4 - Processing & Outcome
    App->>SDK: Submit payment
    SDK->>App: State: processing
    Note right of App: Show spinner, disable button
    SDK->>App: State: successful
    SDK->>App: Payment success event
    Note right of App: Navigate to confirmation
```

## Debugging tips

<Tabs>
  <Tab title="Web">
    * **Events not firing?** Confirm the `<primer-checkout>` element is in the DOM before you add listeners. In SPAs, race conditions between route rendering and listener attachment are the most common cause.
    * **Shadow DOM boundary issues?** Triggerable events (`primer:card-submit`, `primer:vault-submit`) must be dispatched with `bubbles: true` and `composed: true` or they won't reach the internal form component.
    * **Stale data after a cart change?** Call `primer.refreshSession()` to sync the client-side SDK with your server session. The SDK will re-dispatch `primer:methods-update` with the updated method list.
  </Tab>

  <Tab title="Android">
    * **State not updating?** Make sure you're using `collectAsStateWithLifecycle()` instead of `collectAsState()`.
    * **Events not received?** Ensure the `onEvent` callback is set on `PrimerCheckoutSheet` or `PrimerCheckoutHost`.
    * **Stale data after a cart change?** Call `checkout.refresh()` to reinitialize the checkout.
    * **Log diagnostics IDs.** Every `PrimerError` includes a `diagnosticsId`. Log it for troubleshooting with Primer support.
  </Tab>

  <Tab title="iOS">
    * **State not updating?** Ensure you're iterating the `AsyncStream` inside a `Task` within the `scope` closure.
    * **onCompletion not firing?** It only fires for terminal states (`.success`, `.failure`, `.dismissed`). Use `AsyncStream` for intermediate states like `.initializing` and `.ready`.
    * **Payment method scope is nil?** Call `getPaymentMethodScope()` only after the checkout reaches the `.ready` state.
    * **Log diagnostics IDs.** Every `PrimerError` includes a `diagnosticsId`. Log it for troubleshooting with Primer support.
  </Tab>
</Tabs>

## See also

<CardGroup cols={2}>
  <Card title="Web events reference" icon="book" href="/sdk/primer-checkout-web/events-reference">
    Every Web event name, payload type, callback signature, and instance method
  </Card>

  <Card title="Android SDK reference" icon="android" href="/sdk/android-checkout/v3.0.0-beta/api/overview">
    Android SDK API documentation
  </Card>

  <Card title="Recipes" icon="utensils" href="/checkout/primer-checkout/guides-and-recipes/overview">
    Use-case driven examples for common integration scenarios
  </Card>

  <Card title="Error handling" icon="triangle-exclamation" href="/checkout/primer-checkout/build-your-ui/error-handling">
    Deep dive into error handling, retries, and failure recovery
  </Card>
</CardGroup>
