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 type | New type | Purpose |
|---|
PrimerCheckout | PrimerJS | Main SDK instance |
UniversalCheckoutOptions | PrimerCheckoutOptions | Checkout configuration |
| N/A | CheckoutElement | Web 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
| Aspect | Legacy | New |
|---|
| Entry point | PrimerSDK.showUniversalCheckout() | loadPrimer() + <primer-checkout> element |
| Container | container: '#checkout-container' (CSS selector) | The <primer-checkout> element is the container |
| Configuration | Single options object passed to function | Two types: component properties (setAttribute) and SDK options (direct assignment) |
| Payment handling | Callbacks in options object | DOM 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:
| Method | Description |
|---|
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 callback | New event | Notes |
|---|
onCheckoutComplete(data) | primer:payment-success | Unified payment property |
onCheckoutFail(error, data, handler) | primer:payment-failure | Simplified — no handler |
onBeforePaymentCreate(data, handler) | primer:payment-start | Use preventDefault() + handlers in event.detail |
onPaymentCreationStart() | primer:payment-start | Same event, check paymentMethodType |
onClientSessionUpdate(session) | Call primer.refreshSession() | Now a method, not event |
submitButton.onDisable(disabled) | primer:state-change | Check 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 option | New option | Notes |
|---|
container | Removed | The <primer-checkout> element is the container |
vault.visible | vault.enabled | Renamed — same purpose |
form.inputLabelsVisible | Removed | Labels are always visible for accessibility |
successScreen.visible | Removed | Handle success UI in your application |
successScreen.message | Removed | Use primer:payment-success event or the checkout-complete slot |
apiVersion | sdkCore | sdkCore defaults to true; set to false only for legacy compatibility |
New options
| Option | Description |
|---|
vault.headless | Hide default vault UI for custom implementations |
vault.showEmptyState | Show 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-brand → primerColorBrand (remove --, convert kebab-case to camelCase).
Option 3: Inline CSS
checkout.style.setProperty('--primer-color-brand', '#007BFF');
Key differences
| Aspect | Legacy | New |
|---|
| Location | JavaScript options object | CSS stylesheet, custom-styles attribute, or inline styles |
| Syntax | Nested JS objects | CSS variables with --primer- prefix |
| Theming | Manual recreation for dark mode | Built-in dark theme via class="primer-dark-theme" |
| Debugging | Difficult to inspect | Live editing in browser DevTools |
| Performance | Runtime style injection | Native 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
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 (buttonColor → style.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:
visible → enabled (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
| Event | Payload | Description |
|---|
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.
The mechanism for triggering payment submission from an external button has changed from an imperative method call to a dispatched event.
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();
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
| Event | Payload | Description |
|---|
primer:ready | PrimerJS instance | SDK initialized and ready |
primer:state-change | { isProcessing, isLoading, isSuccessful, primerJsError, paymentFailure } | State changes during payment lifecycle |
primer:methods-update | InitializedPaymentMethod[] | Available payment methods loaded or changed |
Payment events
| Event | Payload | Description |
|---|
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 |
| Event | Payload | Description |
|---|
primer:card-network-change | CardNetworksContextType | Card network detected from user input |
primer:card-success | { result } | Card form validation succeeded |
primer:card-error | { errors } | Card form has validation errors |
Vault events
| Event | Payload | Description |
|---|
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.
| Event | Purpose |
|---|
primer:card-submit | Trigger card form submission |
primer:vault-submit | Trigger vault payment submission |
primer:show-other-payments-toggle | Toggle 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