Skip to main content
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.
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 Events & Callbacks Reference.

How Primer Events Work

Primer Checkout components dispatch 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.

Choosing Where to Listen

The first decision in any integration is where to attach your event listeners. Both options receive the same events - the difference is scope and architecture fit.
Attach listeners directly to the <primer-checkout> element. Each listener is scoped to a single checkout instance.
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:
  • Your page renders more than one checkout (e.g. a multi-cart experience) and each needs independent handling.
  • 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.

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.
// Safe: wait for the DOM to be ready before querying
document.addEventListener('DOMContentLoaded', () => {
  const checkout = document.querySelector('primer-checkout');
  if (checkout) {
    checkout.addEventListener('primer:ready', handleReady);
  }
});
If your framework provides a lifecycle hook that runs after the component mounts (React’s useEffect, Vue’s onMounted, etc.), prefer that over DOMContentLoaded.

Phase 1: Initialization

Every Primer Checkout integration begins with the primer:ready event. This event fires once, after the SDK has fully initialized, and it delivers the PrimerJS instance as event.detail. You need this instance to register payment callbacks, pre-fill form fields, and call SDK methods so primer:ready is the correct place to do all of that setup.
const checkout = document.querySelector('primer-checkout');

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

  // 1. Register payment lifecycle callbacks (required)
  primer.onPaymentSuccess = ({ paymentSummary, paymentMethodType }) => {
    console.log('Payment successful:', paymentSummary.last4Digits);
    window.location.href = `/confirmation?method=${paymentMethodType}`;
  };

  primer.onPaymentFailure = ({ error }) => {
    console.error('Payment failed:', error.message);
    // The checkout UI already displays the error to the user.
    // Use this callback for logging, analytics, or retry logic.
  };

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

  // 3. Access available methods immediately if needed
  const methods = primer.getPaymentMethods();
  console.log(`${methods.length} payment method(s) available at init`);
});
If you skip registering onPaymentSuccess and onPaymentFailure inside primer:ready, there is no built-in fallback: successful and failed payments will complete silently with no redirect or confirmation. Always register these two callbacks.

Why Not Use the DOM Events for Payment Outcomes?

Primer provides both DOM events (primer:payment-success, primer:payment-failure) and PrimerJS callbacks (onPaymentSuccess, onPaymentFailure) for payment outcomes. They carry the same data. The difference is in intent:
  • PrimerJS callbacks are the primary integration point. Use them for the actions that must happen after a payment (redirect to confirmation, server-side order fulfillment, retry logic).
  • DOM events are the observability layer. Use them for side effects that shouldn’t block or alter the primary flow (analytics, logging, monitoring dashboards).
For most integrations, you’ll register callbacks inside primer:ready and optionally add DOM event listeners for analytics.

Phase 2: Payment Method Discovery

Shortly after initialization, 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.
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 covers that pattern in detail.

Phase 3: User Interaction

As the user fills in card details, the SDK emits events that let you respond to their input in real time.

Showing the Card Network

primer:card-network-change fires as the user types their card number. It tells you which card network (Visa, Mastercard, etc.) has been detected so far, and whether detection is still in progress. Use it to display the correct card brand logo or to adjust UI (for example, hiding an Amex-only surcharge notice when the detected network changes).
checkout.addEventListener('primer:card-network-change', (event) => {
  const { detectedCardNetwork, selectableCardNetworks, isLoading } = event.detail;
  const logoEl = document.getElementById('card-logo');
  const cobrandEl = document.getElementById('cobrand-selector');

  if (isLoading) {
    logoEl.src = '/images/card-placeholder.svg';
    return;
  }

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

  // Some cards support co-badging (e.g. Carte Bancaire / Visa).
  // If the SDK returns multiple selectable networks, show a picker.
  cobrandEl.hidden = selectableCardNetworks.length <= 1;
});

Triggering Submission from a Custom Button

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.
document.getElementById('custom-pay-button').addEventListener('click', () => {
  document.dispatchEvent(
    new CustomEvent('primer:card-submit', {
      bubbles: true,
      composed: true,
    }),
  );
});
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.
For vault payment submission from a custom button, dispatch primer:vault-submit in the same way. See the Events Reference and Triggerable Events for the full list of events you can dispatch.

Phase 4: Payment Processing and Outcome

Once the user submits, the SDK moves through a processing → outcome sequence. The primer:state-change event fires at every step, giving you a single stream to drive your UI state.
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 and the onPaymentSuccess callback handles the redirect,
    // but you might also want to update UI immediately (e.g. hide the form).
    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);
  }
});

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 or PrimerJS callbacks for final actions (redirects, confirmations, server notifications).

Intercepting Payments with onPaymentPrepare

Sometimes you need to run a check after the user clicks “Pay” but before the payment is created like for example, confirming terms-of-service acceptance, validating inventory, or applying a last-second promotion code. The onPaymentPrepare callback gives you that interception point.
checkout.addEventListener('primer:ready', (event) => {
  const primer = event.detail;

  primer.onPaymentPrepare = (data, handler) => {
    const termsAccepted = document.getElementById('terms-checkbox').checked;

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

    // Optionally inspect data.paymentMethodType to apply method-specific logic
    handler.continuePaymentCreation();
  };
});
You must call either handler.continuePaymentCreation() or handler.abortPaymentCreation() in every code path. If neither is called, the payment will hang indefinitely.

Working with Vaulted Payment Methods

If your integration supports saved (vaulted) payment methods, two additional events become relevant:
  • 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.
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.size() > 0) {
    savedMethodsSection.hidden = false;
    renderSavedMethodsList(vaultedPayments.toArray());
  } 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.

Event Flow Overview

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

Debugging Tips

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

See Also