Overview

Headless Universal Checkout is currently in beta for Web, iOS, Android and React Native. The following payment methods are unsupported:

ADYEN_IDEAL, ADYEN_DOTPAY, ADYEN_BLIK, XFERS_PAYNOW, RAPYD_PROMPTPAY, RAPYD_FAST, PRIMER_TEST_KLARNA, PRIMER_TEST_PAYPAL, PRIMER_TEST_SOFORT

Web currently only supports PAYMENT_CARD.

Where there is a need for more customization and control over the checkout experience, a headless version of Primer’s Universal Checkout is available. You can use Headless Universal Checkout with your own UI, giving you more flexibility and allowing you to move faster when making design changes, while still having Universal Checkout capture sensitive PCI card data or other form data.

As with all versions of Universal Checkout, any payment method can be added and configured through the Primer Dashboard, meaning Primer handles the logic of when to display each method.

Primer Headless Universal Checkout works in a simple way:

  1. 1
    Get a clientToken from your server
  2. 2
    Start Primer Headless Universal Checkout with the client token
  3. 3
    Primer Headless Universal Checkout will then return the available payment methods for the session initiated. Those payment methods that have been configured in the Dashboard and whose conditions match the current client session will be returned.
  4. 4
    Show the list of available payment methods.
  5. 5
    When the user selects a payment method, show its UI to enable the user to enter their credentials. Depending on the payment method, you will have to either ask the SDK to render it, or build the UI yourself.
  6. 6
    Primer Headless Universal Checkout will then create a payment for you and manage its lifecycle. You will receive a confirmation of payment with a callback to indicate the checkout flow has completed.
This documentation is only relevant for v2.16.0 and upward.
❗️
Currently, the only supported payment methods are card, Apple Pay, Google Pay, and PayPal.

Step 1: Install

Our Web SDK is available on npm under the name @primer-io/checkout-web.

This package includes TypeScript definitions.

12345
# With yarnyarn add @primer-io/checkout-web # With npmnpm install --save @primer-io/checkout-web
bash
copy

Step 2: Initialize Primer’s Headless Universal Checkout

Generate a client token

Request a client token from your backend by creating a client session.

💡

Check our guide on how to create a client session here.

❗️
Remember that based on your client token different payment methods will be available for display.

Configure Headless Universal Checkout

Once you have a client token, initialize Primer’s headless checkout with Primer.createHeadless(clientToken).

1
const headless = await Primer.createHeadless(clientToken)
typescript
copy

Then, configure headless checkout by calling headless.configure(options). Make sure to implement at least the following callbacks:

  • onAvailablePaymentMethodsLoad(paymentMethodTypes)
    returns the available payment methods for the client session. Use it to render a list of payment methods.
  • onCheckoutComplete(data)
    is called when the payment has been successfully completed. It returns a reference to the payment.
  • onCheckoutFail(error, data, handler)
    is called if the payment fails to be created or processed.

Payment methods are added and configured through your Primer Dashboard. onAvailablePaymentMethodsLoad will return the payment methods whose conditions match the current client session.

Finally, call headless.start() to retrieve the list of payment methods, and start the checkout flow.

1
await headless.start()
typescript
copy

Here is a full example to configure and start Headless Universal Checkout:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
window.addEventListener('load', onLoaded) async function onLoaded() {    // Create a client session via your backend    const clientSession = await fetch('/client-session', {        method: 'post',        headers: { 'Content-Type': 'application/json' },    }).then(data => data.json())     const { clientToken } = clientSession    const { Primer } = window     // Create an instance of the headless checkout    const headless = await Primer.createHeadless(clientToken)     // Configure headless     // Configure headless    await headless.configure({        onAvailablePaymentMethodsLoad(paymentMethodTypes) {            // Called when the available payment methods are retrieved             for (const paymentMethodType of paymentMethodTypes) {                switch (paymentMethodType) {                    case 'PAYMENT_CARD': {                        // Configure your card form (see Step 3.a)                        // await configureCardForm();                        break;                    }                    case 'PAYPAL': {                        // Render the payment method button (see Step 3.b)                        // configurePayPalButton();                        break;                    }                    case 'APPLE_PAY': {                        // Render the payment method button (see Step 3.b)                        // configureApplePayButton();                        break;                    }                    case 'GOOGLE_PAY': {                        // Render the payment method button (see Step 3.b)                        // configureGooglePayButton();                        break;                    }                    // More payment methods to follow                }            }        },         onCheckoutComplete({ payment }) {            // Notifies you that a payment was created            // Move on to next step in your checkout flow:            // e.g. Show a success message, giving access to the service, fulfilling the order, ...            console.log('onCheckoutComplete', payment)        },         onCheckoutFail(error, { payment }, handler) {            // Notifies you that the checkout flow has failed and a payment could not be created            // This callback can also be used to display an error state within your own UI.             // ⚠️ `handler` is undefined if the SDK does not expect anything from you            if (!handler) {                return            }             // ⚠️ If `handler` exists, you MUST call one of the functions of the handler             // Show a default error message            return handler.showErrorMessage()        },    })     // Start the headless checkout    await headless.start()     console.log('Headless Universal Checkout is loaded!')}
typescript
copy

See more options and events in the SDK API Reference

Step 3: Handle payment methods

Headless Universal Checkout enables you to create any UI that suits your needs, using the components and data we provide.

Currently, the only supported payment method is credit and debit card.

Step 3.a: Handle cards

When PAYMENT_CARD is available as a payment method and provided via onAvailablePaymentMethodsLoad, build your own card form using Primer input elements.

Get started by creating a payment method manager for cards.

1
const cardManager = await headless.createPaymentMethodManager('PAYMENT_CARD')
typescript
copy

Show card components

Headless Universal Checkout securely captures payment method data while fully embedded in your app. By communicating directly with Primer's PCI-L1 tokenization service, Universal Checkout transforms sensitive customer data into a secure uniform string called a payment method token.

First, prepare containers in the DOM for the Primer hosted inputs. You would need three containers for the card number, the expiry date, and the CVV.

123456789101112131415
const container = document.getElementById('my-container') const cardNumberInputId = 'checkout-card-number-input'const cardNumberInputEl = document.createElement('div')cardNumberInputEl.setAttribute('id', cardNumberInputId) const cardExpiryInputId = 'checkout-card-expiry-input'const cardExpiryInputEl = document.createElement('div')cardExpiryInputEl.setAttribute('id', cardExpiryInputId) const cardCvvInputId = 'checkout-card-cvv-input'const cardCvvInputEl = document.createElement('div')cardCvvInputEl.setAttribute('id', cardCvvInputId) container.append(cardNumberInputEl, cardExpiryInputEl, cardCvvInputEl)
typescript
copy

Then, create the hosted card inputs:

1
const { cardNumberInput, expiryInput, cvvInput } = cardManager.createHostedInputs()
typescript
copy

Finally, render your inputs into the relevant containers:

1234567891011121314
await Promise.all([    cardNumberInput.render(cardNumberInputId, {        placeholder: '1234 1234 1234 1234',        ariaLabel: 'Card number',    }),    expiryInput.render(cardExpiryInputId, {        placeholder: 'MM/YY',        ariaLabel: 'Expiry date',    }),    cvvInput.render(cardCvvInputId, {        placeholder: '123',        ariaLabel: 'CVV',    }),])
typescript
copy
Customize card components

Card components are rendered with individual iframes in order to remain PCI-L1 compliant. One key consequence is that the CSS of your page will not be propagated to the card components. This includes color, and font.

Pass a style object to the render function to configure colors and font options.

1234567891011121314151617
const style = {    input: {        base: {            height: 'auto',            border: '1px solid rgb(0 0 0 / 10%)',            borderRadius: '2px',            padding: '12px',            boxShadow: '0 4px 8px 0 rgba(0,0,0,0.2)',        },    },} cardNumberInput.render(cardNumberInputId, {    placeholder: '1234 1234 1234 1234',    ariaLabel: 'Card number',    style,})
typescript
copy

Check the "Styling Inputs" section of the Customize Universal Checkout guide to learn how to adapt the style to your requirements.

Reset card form

If needed, the card form can be cleared by calling reset():

1
cardManager.reset()
typescript
copy
Remove card elements

Call removeHostedInputs() to remove the hosted card input fields from the DOM:

1
cardManager.removeHostedInputs()
typescript
copy

Detect card type

When the user enters the card credentials, Headless Universal Checkout automatically detects the possible types of the card.

Listen to the callback onCardMetadataChange on the card manager to receive the type of card.

12345
const cardManager = await headless.createPaymentMethodManager('PAYMENT_CARD', {    onCardMetadataChange({ type }) {        console.log('Card type: ', type)    },})
typescript
copy

Capture cardholder name

You are free to render the cardholder name input however you want.

As the user enters their cardholder name, call setCardholderName(cardholderName) to pass the cardholder name to the cardManager:

123
cardholderNameInput.addEventListener('change', e => {    cardManager.setCardholderName(e.target.value)})
typescript
copy

If the cardholder name is required, its content will be validated by Headless Universal Checkout.

You can specify whether the cardholder name is required by setting an option when initializing Headless Universal Checkout:

1234567
await headless.configure({    card: {        cardholderName: {            required: true,        },    },})
typescript
copy

Handle input errors

The event change, available on each input, reacts to input changes. This returns if the input is valid or not, and the error type.

123
cardNumberInput.addEventListener('change', (...args) => {    console.log('cardNumberInput changed', ...args)})
typescript
copy

Validate and Submit

When the user submits the card information, follow the following flow:

  • First, validate all the inputs using the validate() function that does basic validations on the hosted inputs.
  • Then, submit the validated data using the submit() function. This triggers the payment creation.

For example, if the user clicks submit, you can handle it as follows:

12345678
submitButton.addEventListener('click', async () => {    // Validate your card input data    const { valid } = await cardManager.validate()    if (valid) {        // Submit the card input data to Primer for tokenization        await cardManager.submit()    }   }})
typescript
copy

Calling submit() triggers the creation and handling of the payment.

  • If onCheckoutComplete is called, show a success message and reset the inputs.
  • If onCheckoutFail is called, show a failure message and allow the customer to try again with the same details.

Integration example snippet

Below is an example code snippet of how it all fits together.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
const container = document.getElementById('my-container') // Create containers for your hosted inputsconst cardNumberInputId = 'checkout-card-number-input'const cardNumberInputEl = document.createElement('div')cardNumberInputEl.setAttribute('id', cardNumberInputId) const cardExpiryInputId = 'checkout-card-expiry-input'const cardExpiryInputEl = document.createElement('div')cardExpiryInputEl.setAttribute('id', cardExpiryInputId) const cardCvvInputId = 'checkout-card-cvv-input'const cardCvvInputEl = document.createElement('div')cardCvvInputEl.setAttribute('id', cardCvvInputId) const cardHolderInputId = 'checkout-card-holder-input'const cardHolderInputEl = document.createElement('input')cardHolderInputEl.setAttribute('id', cardHolderInputId)cardHolderInputEl.setAttribute('placeholder', 'Cardholder Name') const submitButton = document.createElement('input')const buttonId = 'submit-button'submitButton.setAttribute('type', 'button')submitButton.setAttribute('id', buttonId)submitButton.value = 'Submit' // Add them to your containercontainer.append(cardNumberInputEl, cardExpiryInputEl, cardCvvInputEl, cardHolderInputEl, submitButton)async function configureCardForm() {    const baseStyles = {        height: 'auto',        border: '1px solid rgb(0 0 0 / 10%)',        borderRadius: '2px',        padding: '12px',        boxShadow: '0 4px 8px 0 rgba(0,0,0,0.2)',    }     // Create the payment method manager    const cardManager = await headless.createPaymentMethodManager('PAYMENT_CARD')     // Create the hosted inputs    const cardNumberInput = cardManager.createHostedInput('cardNumber')    const cvvInput = cardManager.createHostedInput('cvv')    const expiryInput = cardManager.createHostedInput('expiryDate')     await Promise.all([        cardNumberInput.render(cardNumberInputId, {            placeholder: '1234 1234 1234 1234',            ariaLabel: 'Card number',            styles: baseStyles,        }),        expiryInput.render(cardExpiryInputId, {            placeholder: 'MM/YY',            ariaLabel: 'Expiry date',            styles: baseStyles,        }),        cvvInput.render(cardCvvInputId, {            placeholder: '123',            ariaLabel: 'CVV',            styles: baseStyles,        }),    ])     // Set the cardholder name if it changes    document.getElementById(cardHolderInputId).addEventListener('change', e => {        cardManager.setCardholderName(e.target.value)    })     // Configure event listeners for supported events    cardNumberInput.addEventListener('change', (...args) => {        console.log('cardNumberInput changed', ...args)    })     cardNumberInput.focus()     submitButton.addEventListener('click', async () => {        // Validate your card input data        const { valid } = await cardManager.validate()        if (valid) {            // Submit the card input data to Primer for tokenization            await cardManager.submit()        }    })}
typescript
copy

See more options and events in the SDK API Reference

Step 3.b: Handle payment methods with managed buttons

This applies to PayPal, Apple Pay and Google Pay.

Some payment methods require Primer to manage the payment method’s button and implementation, and only require you to display the button to your customer.

Follow this approach when PAYPAL, APPLE_PAY or GOOGLE_PAY is available as a payment method and provided via onAvailablePaymentMethodsLoad.

Render the button

Get started by creating the payment method manager:

1
const paymentMethodManager = await headless.createPaymentMethodManager('PAYPAL') // or APPLE_PAY / GOOGLE_PAY
typescript
copy

Then, create an instance of a payment method button and render it:

1234567891011
// Create the button containerconst payPalButton = document.createElement('div')const payPalButtonId = 'paypal-button'payPalButton.setAttribute('type', 'button')payPalButton.setAttribute('id', payPalButtonId) // Create and render the buttonconst button = paymentMethodManager.createButton();button.render(payPalButtonId, {    buttonColor: 'silver'});
typescript
copy

See additional style options in the SDK API Reference.

Handle button clicks (optional)

When the payment method button is clicked, Headless Universal Checkout automatically handles the rendering of the payment method screen and the payment. Based on the result of the payment, you should handle different callbacks.

  • If onCheckoutComplete is called, show a success message and hide the button.
  • If onCheckoutFail is called, show a failure message and allow the customer to try again.

You can also listen to the click event for logging or analytics purposes:

1234
button.addEventListener("click", () => {    // React to click    // E.g. send off analytics});
typescript
copy

Other button methods

The button object also supports other methods:

1234567891011
// Hide the buttonbutton.clean(); // Set the button to disabledbutton.setDisabled(true | false); // Focus the button (not supported for PayPal)button.focus(); // Unfocus the button (not supported for PayPal)button.blur();
typescript
copy

Integration example snippet

Below is an example code snippet of how it all fits together.

123456789101112131415
// Create your button containerconst payPalButton = document.createElement('div')const payPalButtonId = 'paypal-button'payPalButton.setAttribute('type', 'button')payPalButton.setAttribute('id', payPalButtonId) function configurePayPalButton() {    // Create the payment method manager    const button = paymentMethodManager.createButton();     // Render the button    button.render(paypalButtonId, {        buttonColor: 'silver'    });}
typescript
copy

Prepare 3DS

When the user pays by card, the Workflow will decide whether a 3DS challenge is required or not. If so, Headless Universal Checkout will automatically render the 3DS challenge in context.

To improve 3DS success rates, it is recommended to pass the following elements in the Client Session:

  • customer.emailAddress
  • customer.billingAddress

Did It Work?

If everything went well, you should be able to see the payment you just created on your Dashboard under the Payments menu. Payment list