Skip to main content
This guide is framework-agnostic. For React, Vue, Svelte, and Angular patterns, see the Framework Migration Guide.
Not every section will apply — skip sections for features you don’t use (e.g., vault, Apple Pay).

Package and imports

Installation

# Remove legacy package
npm uninstall @primer-io/checkout-web

# Install new package
npm install @primer-io/primer-js

Import changes

Legacy:
import { Primer as PrimerSDK } from '@primer-io/checkout-web';
New:
import { loadPrimer } from '@primer-io/primer-js';

Type imports

If you use TypeScript, the type names have changed:
import type {
  CheckoutElement,
  PrimerCheckoutOptions,
  PrimerJS,
} from '@primer-io/primer-js';
Legacy typeNew typePurpose
PrimerCheckoutPrimerJSMain SDK instance
UniversalCheckoutOptionsPrimerCheckoutOptionsCheckout configuration
N/ACheckoutElementWeb component element type

Initialization

The initialization pattern changes from a single function call to a multi-step process involving a Web Component and event listeners.

Legacy pattern

import { Primer as PrimerSDK } from '@primer-io/checkout-web';

const checkout = await PrimerSDK.showUniversalCheckout(clientToken, {
  container: '#checkout-container',
  onCheckoutComplete: (data) => { /* ... */ },
  onCheckoutFail: (error, data, handler) => { /* ... */ },
});

New pattern

import { loadPrimer } from '@primer-io/primer-js';

// 1. Load the SDK (registers the <primer-checkout> web component)
loadPrimer();

// 2. Get or create the checkout element
const checkout = document.querySelector('primer-checkout');

// 3. Set the client token (component property — use setAttribute)
checkout.setAttribute('client-token', clientToken);

// 4. Set SDK options (object — assign directly)
checkout.options = {
  locale: 'en-GB',
};

// 5. Listen for payment events
checkout.addEventListener('primer:payment-success', (event) => {
  const { payment, paymentMethodType } = event.detail;
  window.location.href = `/confirmation?order=${payment.orderId}`;
});

checkout.addEventListener('primer:payment-failure', (event) => {
  const { error } = event.detail;
  console.error('Payment failed:', error.message);
});
Or declare the element in HTML:
<primer-checkout client-token="your-client-token"></primer-checkout>

Key differences

AspectLegacyNew
Entry pointPrimerSDK.showUniversalCheckout()loadPrimer() + <primer-checkout> element
Containercontainer: '#checkout-container' (CSS selector)The <primer-checkout> element is the container
ConfigurationSingle options object passed to functionTwo types: component properties (setAttribute) and SDK options (direct assignment)
Payment handlingCallbacks in options objectDOM events (primer:payment-success, primer:payment-failure)
Component properties vs SDK options — these are different and cannot be mixed.
  • Component properties (client-token, custom-styles, loader-disabled): Use setAttribute().
  • SDK options (locale, card, vault, applePay, etc.): Assign directly to .options.
Using setAttribute('options', ...) will not work. See the SDK Options guide for details.

Accessing the PrimerJS instance

Some SDK methods require access to the PrimerJS instance. This instance is provided via the primer:ready event, which fires when the checkout is fully initialized.

Getting the instance

const checkout = document.querySelector('primer-checkout');

checkout.addEventListener('primer:ready', (event) => {
  const primer = event.detail;
  
  // Now you can call SDK methods
  await primer.refreshSession();
});

Storing for later use

If you need to call SDK methods outside of the event handler (e.g., in response to user actions or after a session expires), store the reference:
let primer;

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

// Later, when needed
async function handleSessionRefresh() {
  if (primer) {
    await primer.refreshSession();
  }
}

Available methods

The PrimerJS instance provides:
MethodDescription
refreshSession()Sync the SDK with server-side session changes. Returns Promise<void>.
getPaymentMethods()Returns the list of available payment methods.
setCardholderName(name)Programmatically set the cardholder name field.
vault.startPayment(id, options?)Start payment with a vaulted method.
vault.createCvvInput(options)Create a CVV input for re-capture.
vault.delete(id)Delete a vaulted payment method.
Most integrations don’t need to access the PrimerJS instance directly. The primary use cases are session refresh, programmatic cardholder name updates, and headless vault implementations.

Event-driven architecture

The new SDK uses DOM events as the primary API for payment lifecycle handling. This replaces the callback-based approach from the legacy SDK.

Migration table

Legacy callbackNew eventNotes
onCheckoutComplete(data)primer:payment-successUnified payment property
onCheckoutFail(error, data, handler)primer:payment-failureSimplified — no handler
onBeforePaymentCreate(data, handler)primer:payment-startUse preventDefault() + handlers in event.detail
onPaymentCreationStart()primer:payment-startSame event, check paymentMethodType
onClientSessionUpdate(session)Call primer.refreshSession()Now a method, not event
submitButton.onDisable(disabled)primer:state-changeCheck isProcessing / isLoading

Payment success

Legacy:
onCheckoutComplete: (data) => {
  console.log('Payment ID:', data.payment.id);
  console.log('Customer email:', data.customer.email);
}
New:
checkout.addEventListener('primer:payment-success', (event) => {
  const { payment, paymentMethodType, timestamp } = event.detail;
  
  // payment is a PaymentSummary — contains partial card data
  console.log('Last 4:', payment.last4Digits);
  console.log('Network:', payment.network);
  console.log('Method:', paymentMethodType);
  
  window.location.href = '/confirmation';
});
The PaymentSummary contains partial card data (last 4 digits, network, cardholder name) but not full PII like customer email or full card number. Use server-side webhooks for complete payment data.

Payment failure

Legacy:
onCheckoutFail: (error, data, handler) => {
  handler.showErrorMessage('Your card was declined');
}
New:
checkout.addEventListener('primer:payment-failure', (event) => {
  const { error, payment, paymentMethodType, timestamp } = event.detail;
  
  // error has: code, message, diagnosticsId, data
  console.error('Failed:', error.message, error.diagnosticsId);
  
  // The SDK shows the error in the UI automatically.
  // Use this event for logging, analytics, or retry logic.
});

Pre-payment validation (intercepting payments)

The legacy onBeforePaymentCreate callback is replaced by the primer:payment-start event. Use event.preventDefault() to intercept the payment flow, then call continuePaymentCreation() or abortPaymentCreation() from event.detail. Legacy:
onBeforePaymentCreate: (data, handler) => {
  if (!termsAccepted) {
    handler.showErrorMessage('Please accept the terms');
    handler.abort();
  } else {
    handler.continuePaymentCreation();
  }
}
New:
checkout.addEventListener('primer:payment-start', (event) => {
  const { paymentMethodType, continuePaymentCreation, abortPaymentCreation } = event.detail;
  
  // Prevent automatic continuation
  event.preventDefault();
  
  if (!document.getElementById('terms-checkbox').checked) {
    // Show your own error UI
    alert('Please accept the terms');
    abortPaymentCreation();
  } else {
    // Optionally pass an idempotency key
    continuePaymentCreation({ idempotencyKey: 'your-key' });
  }
});
If you don’t call event.preventDefault(), the payment continues automatically. Only use preventDefault() when you need to run validation or async checks before payment creation.

Session refresh

This is no longer a callback. It’s a method you call when you need to sync the SDK with server-side session changes. Legacy:
onClientSessionUpdate: (session) => {
  console.log('Session updated:', session);
}

checkout.updateClientSession(newClientToken);
New:
// Update the client token, then call refreshSession()
checkout.setAttribute('client-token', newClientToken);

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

Options changes

Options that work the same

These options are identical in both SDKs:
  • locale — Language/region code (e.g., 'en-GB'). Falls back to en-GB if the locale is not supported.
  • merchantDomain — Your website domain (used for Apple Pay validation).
  • submitButton.useBuiltInButton — Show or hide the built-in submit button.
  • submitButton.amountVisible — Display the payment amount on the button.

Options that changed

Legacy optionNew optionNotes
containerRemovedThe <primer-checkout> element is the container
vault.visiblevault.enabledRenamed — same purpose
form.inputLabelsVisibleRemovedLabels are always visible for accessibility
successScreen.visibleRemovedHandle success UI in your application
successScreen.messageRemovedUse primer:payment-success event or the checkout-complete slot
apiVersionsdkCoresdkCore defaults to true; set to false only for legacy compatibility

New options

OptionDescription
vault.headlessHide default vault UI for custom implementations
vault.showEmptyStateShow a message when no saved payment methods exist

Success screen replacement

The legacy SDK had a built-in success screen. The new SDK gives you two options: Option 1: Use the checkout-complete slot (declarative)
<primer-checkout client-token="your-client-token">
  <primer-main slot="main">
    <div slot="checkout-complete">
      <h2>Thank you for your order!</h2>
      <p>Your payment was processed successfully.</p>
    </div>
  </primer-main>
</primer-checkout>
Option 2: Handle via event (imperative)
checkout.addEventListener('primer:payment-success', (event) => {
  const { payment } = event.detail;
  window.location.href = `/confirmation?order=${payment.orderId}`;
});

Styling changes

The styling system has fundamentally changed from JavaScript objects to CSS custom properties.

Legacy: CSS-in-JS objects

PrimerSDK.showUniversalCheckout(clientToken, {
  style: {
    input: {
      base: { fontSize: 16, color: '#000000' },
      invalid: { color: '#DC3545', borderColor: '#DC3545' }
    },
    button: {
      base: { backgroundColor: '#007BFF', color: '#FFFFFF' }
    }
  }
});

New: CSS custom properties

Option 1: CSS stylesheet (recommended)
primer-checkout {
  --primer-color-brand: #007BFF;
  --primer-color-text-primary: #333333;
  --primer-typography-brand: 'Inter', sans-serif;
  --primer-radius-medium: 8px;
}
Option 2: custom-styles attribute (JSON string with camelCase keys)
checkout.setAttribute('custom-styles', JSON.stringify({
  primerColorBrand: '#007BFF',
  primerColorTextPrimary: '#333333',
  primerTypographyBrand: 'Inter, sans-serif',
  primerRadiusMedium: '8px',
}));
The conversion rule for the custom-styles attribute: --primer-color-brandprimerColorBrand (remove --, convert kebab-case to camelCase). Option 3: Inline CSS
checkout.style.setProperty('--primer-color-brand', '#007BFF');

Key differences

AspectLegacyNew
LocationJavaScript options objectCSS stylesheet, custom-styles attribute, or inline styles
SyntaxNested JS objectsCSS variables with --primer- prefix
ThemingManual recreation for dark modeBuilt-in dark theme via class="primer-dark-theme"
DebuggingDifficult to inspectLive editing in browser DevTools
PerformanceRuntime style injectionNative CSS cascade

Design token categories

The new SDK organizes styling into token categories:
  • Colors--primer-color-brand, --primer-color-text-primary, --primer-color-background-primary, and more
  • Typography--primer-typography-brand (font family), --primer-typography-body-medium-size, etc.
  • Spacing--primer-space-xxsmall (2px) through --primer-space-xlarge (20px)
  • Border radius--primer-radius-xsmall (2px) through --primer-radius-large (12px)
  • Sizing--primer-size-small through --primer-size-xxxlarge
  • Animation--primer-animation-duration, --primer-animation-easing
  • Border width--primer-width-default and state variants
→ See the Styling Variables Reference for the complete list of available tokens.
Don’t attempt a 1:1 mapping from your legacy CSS-in-JS styles. Instead, start with your brand color (--primer-color-brand) and font (--primer-typography-brand), then use browser DevTools to inspect which tokens affect which elements and adjust from there.

Payment method options

Card form

The card form options have been simplified. The legacy requireCvv and requireCardholderName top-level options no longer exist. Use the cardholderName sub-object instead. Legacy:
card: {
  requireCvv: true,
  requireCardholderName: true,
}
New:
card: {
  cardholderName: {
    required: true,
    visible: true,
    defaultValue: 'John Doe', // Optional: pre-fill the field
  }
}
CVV is always required by the card form — there is no option to disable it. For runtime updates to the cardholder name after initialization, use primer.setCardholderName('name') instead of updating the options.

Apple Pay

The Apple Pay options have been restructured. The legacy merchantId and merchantName are handled server-side, not in client options. Legacy:
applePay: {
  merchantId: 'merchant.com.example',
  merchantName: 'Example Store',
  captureBillingAddress: true,
}
New:
applePay: {
  buttonOptions: {
    type: 'buy',
    buttonStyle: 'black',
  },
  billingOptions: {
    requiredBillingContactFields: ['postalAddress'],
  },
  shippingOptions: {
    requiredShippingContactFields: ['postalAddress', 'name', 'phone', 'email'],
    requireShippingMethod: true,
  },
}
Key changes:
  • merchantId and merchantName — removed from client options (configured server-side).
  • captureBillingAddress — deprecated. Use billingOptions.requiredBillingContactFields instead. The only supported billing field is 'postalAddress'.
  • Shipping fields — use 'phone' (not 'phoneNumber') and 'email' (not 'emailAddress').
  • Button styling — new buttonOptions object with type and buttonStyle.
→ See the Apple Pay reference for all button types and complete configuration.

Google Pay

Google Pay options are largely the same with some additions. Legacy:
googlePay: {
  merchantId: 'merchant-id-from-google',
  merchantName: 'Example Store',
  buttonType: 'buy',
  buttonColor: 'black',
}
New:
googlePay: {
  buttonType: 'buy',
  buttonColor: 'black',
  buttonSizeMode: 'fill',
  buttonRadius: 4,
  buttonLocale: 'en',
  captureBillingAddress: true,
  captureShippingAddress: true,
  emailRequired: true,
  requireShippingMethod: true,
  shippingAddressParameters: {
    allowedCountryCodes: ['US', 'CA'],
    phoneNumberRequired: true,
  },
}
Key changes:
  • merchantId and merchantName — removed from client options (configured server-side).
  • New button options: buttonSizeMode, buttonRadius, buttonLocale.
  • New address capture: captureBillingAddress, captureShippingAddress, emailRequired.
  • New shipping: requireShippingMethod, shippingAddressParameters.
→ See the Google Pay reference for complete configuration.

Klarna

The Klarna configuration has changed significantly from the legacy SDK. There is no klarna.locale option — the Klarna locale is derived from the top-level locale option. Legacy:
klarna: {
  locale: 'en-GB',
}
New:
klarna: {
  paymentFlow: 'DEFAULT',           // 'DEFAULT' or 'PREFER_VAULT'
  allowedPaymentCategories: ['pay_now', 'pay_later', 'pay_over_time'],
  recurringPaymentDescription: '...', // Required for recurring payments
  buttonOptions: {
    text: 'Pay with Klarna',
  },
}
Set the locale at the top level instead:
checkout.options = {
  locale: 'en-GB',
  klarna: {
    paymentFlow: 'DEFAULT',
  },
};
→ See the SDK Options Reference for all Klarna options.

PayPal

PayPal options have been restructured with a style sub-object. Legacy:
paypal: {
  buttonColor: 'blue',
  buttonShape: 'rect',
  buttonHeight: 45,
  buttonLabel: 'pay',
  buttonTagline: false,
  paymentFlow: 'PREFER_VAULT',
}
New:
paypal: {
  style: {
    color: 'gold',    // Default is 'gold', not 'blue'
    shape: 'rect',
    height: 45,
    label: 'pay',
    tagline: false,
    layout: 'vertical',
    borderRadius: 4,
  },
  vault: true,  // Replaces paymentFlow: 'PREFER_VAULT'
}
Key changes:
  • Button properties moved into style sub-object and shortened (buttonColorstyle.color).
  • Default color is 'gold', not 'blue'.
  • buttonSize removed — use style.height with a pixel value.
  • paymentFlow: 'PREFER_VAULT'vault: true.
→ See the PayPal reference for all options.

Vault changes

Configuration

Legacy:
vault: {
  visible: true,
  deletionDisabled: false,
}
New:
vault: {
  enabled: true,       // Required — must be true to use vault
  headless: false,     // New: hide default UI for custom implementations
  showEmptyState: true, // New: show message when no saved methods
}
Key changes:
  • visibleenabled (renamed, required).
  • deletionDisabled — no longer a client-side option. Deletion capability is managed through the vault UI or programmatically via primer.vault.delete().
  • New: headless mode for building fully custom vault UIs.
  • New: showEmptyState to show a message when the user has no saved methods.

Vault API methods

The new SDK provides programmatic vault management:
checkout.addEventListener('primer:ready', async (event) => {
  const primer = event.detail;

  // Create CVV input for re-capture
  const cvvInput = await primer.vault.createCvvInput({
    cardNetwork: 'VISA',         // Card network, not paymentMethodId
    container: '#cvv-container',
    placeholder: 'CVV',
  });

  // Start payment with a saved method
  await primer.vault.startPayment(paymentMethodId);

  // Or with CVV for cards requiring re-capture
  await primer.vault.startPayment(paymentMethodId, { cvv: '123' });

  // Delete a saved method
  await primer.vault.delete(paymentMethodId);
});
vault.createCvvInput() takes a cardNetwork string (e.g., 'VISA', 'MASTERCARD'), not a paymentMethodId. It returns a Promise — always use await.

Vault events

EventPayloadDescription
primer:vault-methods-update{ vaultedPayments, cvvRecapture, timestamp }Vaulted methods loaded or changed
primer:vault-selection-change{ paymentMethodId, timestamp }User selected or deselected a saved method
primer:vault-submit{ source? }Dispatch this to trigger vault payment submission
The vaultedPayments payload is an array of VaultedPaymentMethodSummary objects.
checkout.addEventListener('primer:vault-methods-update', (event) => {
  const { vaultedPayments, cvvRecapture, timestamp } = event.detail;
  
  // vaultedPayments is already an array
  console.log(`${vaultedPayments.length} saved methods`);
  
  vaultedPayments.forEach(method => {
    console.log(method.paymentInstrumentData?.last4Digits);
  });
  
  if (cvvRecapture) {
    // CVV re-entry is required for vaulted card payments
  }
});

Headless vault mode

Build a completely custom vault UI while retaining full vault functionality:
checkout.options = {
  vault: {
    enabled: true,
    headless: true,
  },
};

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

  // Render your custom vault UI
  vaultedPayments.forEach(method => {
    if (method.paymentInstrumentType === 'PAYMENT_CARD') {
      const { network, last4Digits } = method.paymentInstrumentData;
      // Render: Visa •••• 4242
    }
  });

  // Create CVV input if re-capture is required
  if (cvvRecapture) {
    const primer = /* get from primer:ready */;
    const cvvInput = await primer.vault.createCvvInput({
      cardNetwork: vaultedPayments[0].paymentInstrumentData.network,
      container: '#cvv-container',
    });
  }
});
→ See the Vault Manager component reference for the complete headless vault implementation guide.

Custom submit button

The mechanism for triggering payment submission from an external button has changed from an imperative method call to a dispatched event.

Card form submission

Legacy:
document.getElementById('my-button').addEventListener('click', () => {
  checkout.submit();
});
New:
document.getElementById('my-button').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('primer:card-submit', {
    bubbles: true,
    composed: true,
  }));
});

Vault payment submission

For vault payments, dispatch primer:vault-submit instead:
document.getElementById('vault-pay-button').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('primer:vault-submit', {
    bubbles: true,
    composed: true,
  }));
});
An alternative approach for card form submission: you can call cardForm.submit() directly on the <primer-card-form> element if you have a reference to it.
const cardForm = document.querySelector('primer-card-form');
await cardForm.submit();

Button state management

Monitor the primer:state-change event to enable/disable your custom button:
checkout.addEventListener('primer:state-change', (event) => {
  const { isProcessing, isLoading } = event.detail;
  document.getElementById('my-button').disabled = isProcessing || isLoading;
});
For card form validation state, listen to primer:card-success and primer:card-error:
checkout.addEventListener('primer:card-success', () => {
  // Card form is valid — enable submit
});

checkout.addEventListener('primer:card-error', (event) => {
  // Card form has validation errors
  console.log('Errors:', event.detail.errors);
});
→ See the External Submit Button recipe for a complete example.

Events

The new SDK uses a comprehensive DOM event system as the primary API for payment lifecycle handling.

Complete event reference

Lifecycle events

EventPayloadDescription
primer:readyPrimerJS instanceSDK initialized and ready
primer:state-change{ isProcessing, isLoading, isSuccessful, primerJsError, paymentFailure }State changes during payment lifecycle
primer:methods-updateInitializedPaymentMethod[]Available payment methods loaded or changed

Payment events

EventPayloadDescription
primer:payment-start{ paymentMethodType, abortPaymentCreation, continuePaymentCreation, timestamp }Payment creation begins — use preventDefault() to intercept
primer:payment-success{ payment, paymentMethodType, timestamp }Payment completed successfully
primer:payment-failure{ error, payment?, paymentMethodType, timestamp }Payment failed

Card form events

EventPayloadDescription
primer:card-network-changeCardNetworksContextTypeCard network detected from user input
primer:card-success{ result }Card form validation succeeded
primer:card-error{ errors }Card form has validation errors

Vault events

EventPayloadDescription
primer:vault-methods-update{ vaultedPayments, cvvRecapture, timestamp }Vaulted methods loaded or changed
primer:vault-selection-change{ paymentMethodId, timestamp }User selected a saved method
primer:show-other-payments-toggle{ action?, source? }Dispatch to toggle the “other payments” section
primer:show-other-payments-toggled{ expanded }”Other payments” toggle state changed

Triggerable events

You can dispatch these to control the SDK. Both require bubbles: true and composed: true.
EventPurpose
primer:card-submitTrigger card form submission
primer:vault-submitTrigger vault payment submission
primer:show-other-payments-toggleToggle the “other payment methods” section
→ See the Events Reference for full payload types and usage examples.

Migration checklist

Verify your migration is complete:
  • Replaced package — Uninstalled @primer-io/checkout-web, installed @primer-io/primer-js
  • Updated imports — Using loadPrimer and new type names
  • Updated initialization — Using <primer-checkout> element with setAttribute('client-token', ...) and .options = {}
  • Updated to events — Using primer:payment-success, primer:payment-failure events
  • Updated pre-payment validation — Using primer:payment-start with preventDefault()
  • Updated styling — Replaced CSS-in-JS objects with --primer-* CSS custom properties
  • Updated payment method config — Corrected Apple Pay, PayPal, Klarna options
  • Updated vault config — Using vault.enabled and vault events
  • Updated custom submit — Dispatching primer:card-submit / primer:vault-submit events
  • Tested in your framework — See Framework Migration Guide

Next steps