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

If you're looking for a simpler way of integrating Universal Checkout, consider integrating our Drop-In checkout.

Before you start

Before you start, make sure:

Create a client session

A client session is the starting point for integrating payments at Primer. You can attach any data associated with the order to your client session.

Creating a client session provides you with a client token, a temporary key used to initialize the Universal Checkout.

The information you include in the client session is used in the Dashboard:

  • to conditionally route payments with Workflows
  • to activate payment methods and other features in Universal Checkout

So pass as much information as you can!

Generate an API key

Requests to our API are authenticated using an API key in the X-Api-Key header. Create an API key by visiting the developer page of the Primer Dashboard.

Make sure to set the following scopes for your API Key:

  • client_tokens:write
  • transactions:authorize

Make a client session request

On your server, create a client session with POST/client-session.

Make sure to pass at least the following data:

FieldDescription
Your reference for the payment.

Make sure to keep track of orderId - you will later receive updates to the payment via Webhooks. The payment will contain the orderId specified in the client session.
The three-letter currency code in ISO 4217 format.
e.g. use USD for US dollars.
  1. order
The details of the line items of the order.
ℹ️

The body of a successful response contains a clientToken that you will use to initialize the Universal Checkout.

Here is how the client session request to the Primer API should look like:

123456789101112131415161718192021222324252627282930313233343536
curl --location --request \ POST 'https://api.sandbox.primer.io/client-session' \ --header 'X-Api-Key: <YOUR_API_KEY>' \ --header 'X-Api-Version: 2.2' \ --header 'Content-Type: application/json' \ --data '{    "orderId": "<YOUR_ORDER_ID>",    "currencyCode": "GBP",    "amount": 5000,    "order": {      "lineItems": [{        "itemId": "shoes-123",        "amount": 2500,        "quantity": 2      }],      "countryCode": "GB"    } }'
# Here is a (heavily truncated) example response
{  "clientToken": "THE_CHECKOUT_SESSION_TOKEN",  "clientExpirationDate": "2022-03-08T14:00:00Z",  "orderId": "<YOUR_ORDER_ID>",  "currencyCode": "GBP",  "amount": 5000,    "order": {      "lineItems": [{        "itemId": "shoes-123",        "amount": 2500,        "quantity": 2      }],      "countryCode": "GB",    }}
bash
copy

Get Started

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
    You show the user 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's 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 guide is only relevant for v2.5.0 up to 2.16.x.

Install the SDK

Add the following to your app/build.gradle file:

1234567
repositories {  mavenCentral()}
dependencies {  implementation 'io.primer:android:latest.version'}
kotlin
copy

For more details about SDK versions, please see our changelog.

It is highly recommended adding the following settings to your app/build.gradle file:

12345
android {    kotlinOptions {        freeCompilerArgs += '-Xjvm-default=all'    }}
kotlin
copy

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 example below.

123456789101112131415161718192021
class CheckoutActivity : AppCompatActivity() {
  private val listener = object : PrimerHeadlessUniversalCheckoutListener {
    // 👇 [Required] This function will return the list of payment methods    override fun onAvailablePaymentMethodsLoaded(paymentMethods: List<PrimerHeadlessUniversalCheckoutPaymentMethod>) {      // Use it to enable users to select a payment method    }
    // 👇 [Required] This function is called when the checkout has been completed    override fun onCheckoutCompleted(checkoutData: PrimerCheckoutData) {      // Show an order confirmation screen, fullfil the order...      // checkoutData contains the payment that has been created    }
    // 👇 [Required] This function is called if something goes wrong    override fun onFailed(error: PrimerError, checkoutData: PrimerCheckoutData?) {      // your custom method to show an error view    }  }}
kotlin
copy

onAvailablePaymentMethodsLoaded is invoked when Headless Universal Checkout is started with your provided client token. It returns a list of payment methods to render.

Each returned payment method contains the following:

  • paymentMethodType
    a unique string identifier for the payment method (e.g. ADYEN_IDEAL)
  • paymentMethodManagerCategories
    a list which defines the payment method managers that can be used with this payment method (i.e. NATIVE_UI or RAW_DATA).
  • supportedPrimerSessionIntents
    a list of PrimerSessionIntent which defines what intents can be used with this payment method (i.e. CHECKOUT or VAULT).
  • [Optional] requiredInputDataClass
    the class type that should be used with the raw data manager.

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 Headless Universal Checkout with your session's client token    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.

On the PrimerHeadlessUniversalCheckout.current.start you can optionally pass custom settings and/or add the PrimerHeadlessUniversalCheckoutUiListener to listen to UI events.

Additionally, you can set the listener by calling PrimerHeadlessUniversalCheckout.current.setListener(listener)

🚀

Check the SDK API here to customize your SDK settings.

Step 3: Render available payment methods UI

When the checkout is done initializing, the onAvailablePaymentMethodsLoaded method will be invoked.

Use this event to show the list of payment methods to the user.

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 render your own UI for each payment method, or take advantage of the PrimerHeadlessUniversalCheckout.AssetsManager helper to get access to payment methods' assets.

123456789101112131415161718
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 paymentMethodAsset = try {    // 👇 Retrieve the assets of a specific payment method    PrimerHeadlessUniversalCheckoutAssetsManager.getPaymentMethodAsset(this, paymentMethodType)      } catch (e: SdkUninitializedException) {         null      }}
kotlin
copy

The payment method asset contains the following:

  • paymentMethodType
    a unique string identifier for the payment method
  • paymentMethodLogo
    an instance of the PrimerPaymentMethodLogo
  • paymentMethodBackgroundColor
    an instance of the PrimerPaymentMethodBackgroundColor

The PrimerPaymentMethodLogo holds Drawable objects for different scenarios

  • [Optional] colored
    a Drawable to be used anywhere
  • [Optional] dark
    a Drawable to be used on dark mode
  • [Optional] light
    a Drawable to be used on light mode

The PrimerPaymentMethodBackgroundColor holds @ColorInt objects for different scenarios

  • [Optional] colored
    a @ColorInt to be used anywhere
  • [Optional] dark
    a @ColorInt to be used on dark mode
  • [Optional] light
    a @ColorInt to be used on light mode
❗️

colored, dark and light variables are all optional, but it is guaranteed that the objects will contain at least on of them.

With the above images and colors you can build your own payment methods UI 💪

Step 4: Implement a payment method manager

Now that your UI presents the available payment methods, let's use the payment method managers to allow users to go through the payment method flow.

A payment method manager is a class abstracting the state and UI of a given payment method. Currently there are 3 payment methods managers responsible for a different payment method category:

  • PrimerHeadlessUniversalCheckoutRawDataManager
    The native UI manager is responsible for payment methods that need to present their own UI (e.g. Google Pay).
  • PrimerHeadlessUniversalCheckoutNativeUiManager
    The raw data manager is responsible for payment methods that need to receive data from the user (e.g. card form).
💡

Bear in mind that a payment method might be able to be implemented by more than one payment method manager. You can check which managers are supported for each payment method in the paymentMethodManagerCategories of the payment method objects received in Step 1.

❗️

A manager can only be used after Headless Checkout has been initialized. We consider Headless Checkout to be initialized after the onAvailablePaymentMethodsLoaded is triggered and payment methods for the provided clientToken are returned.

If that’s not the case SDK will throw SdkUninitializedException.

If a payment method manager gets initialized with an unsupported payment method type (i.e. the payment method's object doesn't include the manager's category), the SDK will throw an UnsupportedPaymentMethodException error.

Native UI manager

This manager can be used for any payment method that contains NATIVE_UI in the paymentMethodManagerCategories array.

The native UI manager can be used for any payment method that needs to present its own UI, like Google Pay.

123456789101112131415161718
class CheckoutActivity : AppCompatActivity() {    // other code goes here    private fun onPaymentMethodSelected(paymentMethodType: String) {        try {            // 👇 Create the payment method manager            val nativeUiManager = PrimerHeadlessUniversalCheckoutNativeUiManager.newInstance(paymentMethodType)
            // 👇 Show the payment method            nativeUiManager.showPaymentMethod(this, PrimerSessionIntent.CHECKOUT)        } catch (e: SdkUninitializedException) {            // handle exception        } catch (e: UnsupportedPaymentIntentException) {            // handle exception        } catch (e: UnsupportedPaymentMethodException) {            // handle exception        }    }}
kotlin
copy
❗️

Make sure that the intent in the showPaymentMethod function is valid and contained in the supportedPrimerSessionIntents array. If that’s not the case SDK will throw UnsupportedPaymentIntentException.

That’s it! The SDK will present the native UI to the user, and then attempt to create a payment.

The payment’s data will be returned on onCheckoutCompleted configured in Step 1.

Raw Data Manager

This manager can be used for any payment method that contains RAW_DATA in the paymentMethodManagerCategories array.

The raw data manager can be used for payment methods that allow you to pass the data in the SDK (an example would be card data).

With the raw 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

Assuming that the payment method contains RAW_DATA in the paymentMethodManagerCategories, create your raw data manager, and optionally set the onValidationChanged and the onMetadataChanged callbacks.

123456789101112131415161718192021222324252627282930
class CheckoutActivity : AppCompatActivity() {
    // val submitButton = findViewById<Button>(R.id.pay_with_card)
    private lateinit var rawDataManager: PrimerHeadlessUniversalCheckoutRawDataManagerInterface
    private val rawDataManagerListener: PrimerHeadlessUniversalCheckoutRawDataManagerListener =        object : PrimerHeadlessUniversalCheckoutRawDataManagerListener {            override fun onValidationChanged(                isValid: Boolean,                errors: List<PrimerInputValidationError>            ) {            }        }
    private fun setupManager(paymentMethodType: String) {        try {            // 👇 Initialize the raw data manager            rawDataManager =                PrimerHeadlessUniversalCheckoutRawDataManager.newInstance(paymentMethodType)
            // 👇 Set the manager listener            rawDataManager.setListener(rawDataManagerListener)        } catch (e: SdkUninitializedException) {            // handle exception        } catch (e: UnsupportedPaymentMethodException) {            // handle exception        }    }}
kotlin
copy

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

Build a form

Build your own form to let the users enter their data. You can use the getRequiredInputElementTypes of the PrimerHeadlessUniversalCheckoutRawDataManager object to get informed about what input fields you have to render. The getRequiredInputElementTypes is an array that can contain any of the following:

  • CARD_NUMBER
  • EXPIRY_DATE
  • CVV
  • CARDHOLDER_NAME
  • PHONE_NUMBER
  • RETAIL_OUTLET

You can render the form input elements like this:

123456789101112131415161718192021222324252627282930313233
class CheckoutActivity : AppCompatActivity() {
    private fun createForm(paymentMethodType: String) {        // 👇 Get the list of input fields to render        val requiredInputElementTypes = rawDataManager.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 {                        rawDataManager.setRawData(getRawData(paymentMethodType))                    }                    layoutParams = LinearLayout.LayoutParams(                      RelativeLayout.LayoutParams.MATCH_PARENT,                      RelativeLayout.LayoutParams.WRAP_CONTENT,                    )                })            }        }
        val formContainer = findViewById<ViewGroup>(R.id.formContainer)
        inputElements.forEach { formContainer.addView(it) }    }
    private fun getInputTypeValue(inputElementType: PrimerInputElementType) =        findViewById<ViewGroup>(R.id.formContainer).findViewWithTag<TextInputLayout>(            inputElementType        ).editText?.text?.trim().toString()}
kotlin
copy

Build and validate your raw data

Use the class type that has been provided in the requiredInputDataClass of the payment method to build the capture data, and pass it to the manager. Here is an example for card payments:

123456789
data class PrimerCardData(    val cardNumber: String, // ex. 4242424242424242    val expiryDate: String, // ex. 01/2024, 1/2024, 12/2024...    val cvv: String, // ex. 244    val cardholderName: String? = null,)
// 👇 Set your raw data on the managerrawDataManager.setRawData(getRawData(paymentMethodType))
kotlin
copy

When you set the data on the raw data manager, the manager validates it and notifies you via the delegate functions. You can then change the state of your submit button based on the state of validation.

1234567891011
private val rawDataManagerListener: PrimerHeadlessUniversalCheckoutRawDataManagerListener =        object : PrimerHeadlessUniversalCheckoutRawDataManagerListener {          // 👇 Implement the validation function to receive updates on the raw data validation changes          override fun onValidationChanged(                isValid: Boolean,                errors: List<PrimerInputValidationError>            ) {              // Enable your submit button when `isValid == true`                submitButton.isEnabled = isValid            }        }
kotlin
copy

The listener also provides a function onMetadataChanged that will notify you on metadata updates, e.g. that the card type was detected.

Submit the form's data

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

12
// 👇 Submit the data that have been set in prior stepssubmitButton.setOnClickListener { rawDataManager.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.

Perform cleanup

In order to remove the provided listeners and stop any API calls performed by the manager, make sure to call the cleanup function:

1
rawDataManager.cleanup()
kotlin
copy

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.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutUiListener {  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.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutListener {    override fun onTokenizationStarted(paymentMethodType: String) {        // e.g. hide 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.

12345
private val listener = object : PrimerHeadlessUniversalCheckoutUiListener {    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.

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

Advanced Configuration

For more customization on your needs, you can listen to all events posted by Headless Universal Checkout and react to them. Visit the API reference for a full list of the listener functions.

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 iOS 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