Overview

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

ADYEN_IDEAL, ADYEN_DOTPAY, ADYEN_BLIK, XFERS_PAYNOW, GOCARDLESS, PRIMER_TEST_KLARNA, PRIMER_TEST_PAYPAL, PRIMER_TEST_SOFORT

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 Dashboard, meaning Primer handles the logic of when to display each method.

Primer Headless Universal Checkout works in a simple way:

  1. 1
  2. Get a clientToken from your server
  3. 2
  4. Start Primer Headless Universal Checkout with the client token
  5. 3
  6. 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.
  7. 4
  8. Show the list of available payment methods.
  9. 5
  10. 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.
  11. 6
  12. Primer Headless Universal Checkout will then create a payment for you and manage its lifecycle. You will receive a confirmation of payment with the checkout completed callback.

Headless Universal Checkout is currently available in beta on Android. This documentation is only relevant for v2.4.0 and upward. The documentation for version 2 before v2.4.0 is available here.

Prerequisites

Step 1: Prepare the Headless Universal Checkout Listener

Prepare the PrimerHeadlessUniversalCheckoutListener that will handle callbacks that happen during the lifecycle of the payment methods and the payment. Import the PrimerHeadlessUniversalCheckout SDK and set its listener as shown in the following example:

1234567891011121314151617181920
class CheckoutActivity : AppCompatActivity() {   private val listener = object : PrimerHeadlessUniversalCheckoutListener {     override fun onAvailablePaymentMethodsLoaded(paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>) {      // Primer will return available payment methods based on configuration      // use it to enable users to select a payment method    }     override fun onCheckoutCompleted(checkoutData: PrimerCheckoutData) {      // Primer checkout has been completed      // checkoutData contains the payment that has been created      // show an order confirmation screen, fulfil the order...    }     override fun onFailed(error: PrimerError, checkoutData: PrimerCheckoutData?) {      // your custom method to show an error view    }  }}
kotlin
copy

Step 2: Start Headless Universal Checkout

Once you have a client token, you can initialize Primer’s Headless Checkout by calling PrimerHeadlessUniversalCheckout.current.start(...)

123456789101112131415161718192021222324
class CheckoutActivity : AppCompatActivity() {   // other code goes here  private val listener = object : PrimerHeadlessUniversalCheckoutListener {    // ...  }   private fun setupObservers() {    viewModel.clientToken.observe(this) { clientToken ->      // Start the headless checkout session upon reception of the client token      startHeadlessUniversalCheckout(clientToken)    }  }   private fun startHeadlessUniversalCheckout(clientToken: String) {    // Start with default settings    PrimerHeadlessUniversalCheckout.current.start(      this,      clientToken,      PrimerSettings(),      listener    )  }}
kotlin
copy

Once Primer’s Headless Universal Checkout is configured, its listener functions will notify you about Headless Universal Checkout events.

Additionally, you can pass the listener as an argument in the start function, or set it with PrimerHeadlessUniversalCheckout.current.setListener(listener)

🚀

Check the SDK API here to customize your SDK settings.

Step 3: Show available payment methods

When the checkout is done initialising, the onAvailablePaymentMethodsLoaded method in the listener will be invoked. Use this event to show the list of payment methods to the user. You can add your buttons for them, or use Primer SDK’s buttons.

12345678910111213
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onAvailablePaymentMethodsLoaded(    paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>  ) {    paymentMethods.forEach { setupPaymentMethod(it.paymentMethodType) }  }} // your custom method to render list of payment method buttons UI.private fun setupPaymentMethod(paymentMethodType: String) {  // implement payment method UI}
kotlin
copy

You can configure payment methods through the Dashboard. Only payment methods whose conditions match the current client session will be returned.

You can construct your own payment method, or use the function makeView to create a payment method button.

123456789101112131415161718192021
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onAvailablePaymentMethodsLoaded(    paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>  ) {    paymentMethods.forEach { setupPaymentMethod(it.paymentMethodType) }  }} // your custom method to render the list of payment method buttonsprivate fun setupPaymentMethod(paymentMethodType: String) {  val containerView = findViewById<ViewGroup>(R.id.container)  val view = headlessUniversalCheckout.makeView(paymentMethodType)   view?.apply {    containerView.addView(this)    setOnClickListener {      // TODO    }  }}
kotlin
copy

Step 4: Handle payment method selection

When the user selects a payment method, it is time to present the relevant payment form.

Headless Universal Checkout currently handles payment methods in three ways:

  • With the function showPaymentMethod that renders a view all handled by Primer. This is the case today for web views and native payment methods like Google Pay.
  • With a Primer card manager that provides you with the list of data to capture so that you can build your on UI. This is the case today for card payments.
  • With a “Raw Data Manager” that provides you with the list of data to capture so that you can build your on UI. This is the case today for card payments.
123456789101112131415161718192021
// your custom method to render the list of payment method buttonsprivate fun setupPaymentMethod(paymentMethodType: String) {  val containerView = findViewById<ViewGroup>(R.id.container)  val view = headlessUniversalCheckout.makeView(paymentMethodType)   view?.apply {    containerView.addView(this)    setOnClickListener {      // for card, render your own UI, see more details below.      if (paymentMethodType == "PAYMENT_CARD") {        // createForm()      } else {        // for other available payment methods        headlessUniversalCheckout.showPaymentMethod(          this.context,          paymentMethodType        )      }    }  }}
kotlin
copy

Step 4.1: Handle showPaymentMethod

Use the function showPaymentMethod to display payment method that must be handled by Primer.

1234
PrimerHeadlessUniversalCheckout.current.showPaymentMethod(    this.context,    paymentMethodType)
kotlin
copy
🚀

The user can now interact with Universal Checkout, and the SDK will create the payment. The payment’s data will be returned on onCheckoutCompleted(checkoutData) configured in Step 1.

Step 4.2: Show card form

ℹ️

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. Universal Checkout automatically uses this token to create a payment.

If the user wants to pay with a card, you should use a card form.

Primer will provide you with the required inputs for a given payment method. Use this to create your inputs.

Step 4.2.1: Primer Card Manager

With the card manager, you can show your fully customized UI and still use all of the features that make Primer great.

Render input elements

First, use the PrimerCardManager object and render the card form input elements like this:

1234567891011121314151617181920
class CheckoutActivity : AppCompatActivity() {   private val cardManager by lazy { PrimerCardManager.newInstance() }   private fun createForm() {    val elementTypes = cardManager.getRequiredInputElementTypes().orEmpty()     val inputElements = elementTypes.map { type ->      PrimerEditTextFactory.createFromType(this, type).apply {        setPrimerInputElementListener(inputElementListener)      }    }     val cardFormView = findViewById<ViewGroup>(R.id.cardForm)     inputElements.forEach { cardFormView.addView(it) }     cardManager.setInputElements(inputElements)  }}
kotlin
copy

Subscribe to Validation Listener

Then, listen to the card form validation state and enable your form’s submit button accordingly:

1234567891011
// val submitButton = findViewById<Button>(R.id.pay_with_card) private val cardManagerListener = object : PrimerCardManagerListener {  override fun onCardValidationChanged(isCardFormValid: Boolean) {    submitButton.isEnabled = isCardFormValid  }} private fun setupListeners() {    cardManager.setCardManagerListener(cardManagerListener)}
kotlin
copy

Handle Input Updates (optional)

When displaying a card form, you may want to trigger UI changes based on the updates and/or validation status of an active input field. To achieve this please implement the following listener interface and pass it to the input field:

12345678910111213141516
val inputListener = object : PrimerInputElementListener {     override fun inputElementValueChanged(inputElement: PrimerInputElement) {            /* TODO */    }     override fun inputElementValueIsValid(inputElement: PrimerInputElement, isValid: Boolean) {            /* TODO */    }    override fun inputElementDidDetectCardType(type: CardType.Type) {            /* TODO */    }} cardNumberField.setListener(inputLister)
kotlin
copy
Configure submit button

Finally, configure the on click listener of the submit button:

1
submitButton.setOnClickListener { cardManager.tokenize() }
kotlin
copy
🚀

When the function tokenize is called, the SDK will attempt to create a payment. The payment’s data will be returned on onCheckoutCompleted(checkoutData) configured in Step 1.

Step 4.2.2: Primer Raw Data Manager

With the raw (card) manager, you can use your fully customized UI and still use all of the features that make Primer great.

At any point, you can validate your data by calling setRawData and by listening to onValidationChanged. Primer will return errors specific to each required input.

When the data is valid, you can send it to Primer for further processing by calling submit.

Configure the PrimerHeadlessUniversalCheckoutRawDataManager and subscribe to callbacks

First, configure the PrimerHeadlessUniversalCheckoutRawDataManager, and optionally set the onValidationChanged and the onMetadataChanged callbacks.

123456789101112131415161718192021222324252627
class CheckoutActivity : AppCompatActivity() {   // val submitButton = findViewById<Button>(R.id.pay_with_card)   private val cardManager by lazy {    PrimerHeadlessUniversalCheckoutRawDataManager.newInstance("PAYMENT_CARD")  }   private val rawDataManagerListener: PrimerHeadlessUniversalCheckoutRawDataManagerListener =    object : PrimerHeadlessUniversalCheckoutRawDataManagerListener {      override fun onValidationChanged(        isValid: Boolean,        errors: List<PrimerInputValidationError>      ) {        // modify your UI        submitButton.isEnabled = isValid      }       override fun onMetadataChanged(metadata: PrimerPaymentMethodMetadata) {        // show card icon      }    }} private fun setupListeners() {  cardManager.setManagerListener(rawDataManagerListener)}
kotlin
copy

Listen to the card data validation state and enable your form’s submit button accordingly.

Render Input Elements

Use the PrimerHeadlessUniversalCheckoutRawDataManager object and render the card form input elements like this:

1234567891011121314151617181920212223242526272829303132
class CheckoutActivity : AppCompatActivity() {   private fun createForm() {    val requiredInputElementTypes = cardManager.getRequiredInputElementTypes()    // simple example of creating inputs based on type    val inputElements = requiredInputElementTypes.map { type ->      TextInputLayout(this).apply {        tag = type        hint = type.name        addView(TextInputEditText(context).apply {          id = View.generateViewId()          doAfterTextChanged {            cardManager.setRawData(getCardData())          }          layoutParams = LinearLayout.LayoutParams(            RelativeLayout.LayoutParams.MATCH_PARENT,            RelativeLayout.LayoutParams.WRAP_CONTENT,          )        })      }    }     val cardFormView = findViewById<ViewGroup>(R.id.cardForm)     inputElements.forEach { cardFormView.addView(it) }  }   private fun getInputTypeValue(inputElementType: PrimerInputElementType) =    findViewById<ViewGroup>(R.id.cardForm).findViewWithTag<TextInputLayout>(      inputElementType    ).editText?.text?.trim().toString()}
kotlin
copy

getRequiredInputElementTypes will return a list of PrimerInputElementType. Possible values are CARD_NUMBER, EXPIRY_DATE, CVV, CARDHOLDER_NAME.

Primer Headless Universal Checkout requires input data to be sent for further processing. For PAYMENT_CARD, you should pass PrimerRawCardData:

1234567
data class PrimerRawCardData(    val cardNumber: String, // ex. 4242424242424242    val expiryMonth: String, // ex. 01, 1, 12...    val expiryYear: String, // ex. 2023    val cvv: String, // ex. 244    val cardholderName: String? = null,)
kotlin
copy

Submit input data changes

In order to validate input data, and do further processing, submit input data changes:

1
cardManager.setRawData(getCardData())
kotlin
copy

All changes will be propagated to PrimerHeadlessUniversalCheckoutRawDataManagerListener

Configure submit button

Finally, configure the on click listener of the submit button

1
submitButton.setOnClickListener { cardManager.submit() }
kotlin
copy
🚀

When the function submit is called, the SDK will attempt to create a payment. The payment’s data will be returned on onCheckoutCompleted(checkoutData) configured in Step 1.

Step 5: Handle Listener Callbacks (Optional)

Handle onPreparationStarted Callback (Optional)

This function will notify you that the tokenization preparation has started. At this point the SDK has not yet started to communicate with Primer's backend. It may be useful in case you want to show a loader or notify user about progress in any other way.

123456
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onPreparationStarted(paymentMethodType: String) {    // e.g. show a loading screen.  }}
kotlin
copy

Handle onTokenizationStarted Callback (Optional)

This function will notify you that the tokenization API call has been fired. It may be useful in case you want to show a loader or notify user about progress in any other way.

123456
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onTokenizationStarted(paymentMethodType: String) {    // e.g. show a loading screen.  }}
kotlin
copy

Handle onPaymentMethodShowed Callback (Optional)

This function will notify you that the payment method you requested to show has been presented.

123456
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onPaymentMethodShowed(paymentMethodType: String) {    // e.g. hide a loading screen.  }}
kotlin
copy

Handle errors

Use the onFailed method to handle any errors emitted by the SDK during the checkout flow.

1234567
private val listener = object : PrimerHeadlessUniversalCheckoutListener {   override fun onFailed(error: PrimerError, checkoutData: PrimerCheckoutData?) {    // your custom method to show an error view    showErrorView(error)  }}
kotlin
copy

Prepare 3DS

  • In order to make the SDK lightweight, 3DS requires the addition of the Primer 3DS library. Check out our guide on how to add the Primer 3DS library to your Android app.
  • When the user pays by card, the Workflow will decide whether or not a 3DS challenge is required. If so, Headless Universal Checkout will automatically render the 3DS challenge.
  • To improve 3DS success rates, it is recommended to pass the following elements in the Client Session:
FieldDescription
  1. customer
The customer's email address
  1. customer
The customer's billing address

Learn more about how to configure 3DS!

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