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.
Not all payment methods are currently compatible with Headless Checkout.
Please refer to this table to learn more about the payment methods available for Headless Checkout.
Before you start
Before you start, make sure:
- you are ready to process a payment
- Universal Checkout is properly configured in the dashboard
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:
Field | Description |
---|---|
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. | |
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", }}
Get Started
Primer Headless Universal Checkout works in a simple way:
- 1Get a
clientToken
from your server. - 2Start Primer Headless Universal Checkout with the client token.
- 3Primer 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. - 4You show the user the list of available payment methods.
- 5When 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.
- 6Primer'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.
Install the SDK
Add the following to your app/build.gradle
file:
1234567
repositories { mavenCentral()} dependencies { implementation 'io.primer:android:latest.version'}
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' }}
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, fulfil 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 } }}
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
orRAW_DATA
).supportedPrimerSessionIntents
a list ofPrimerSessionIntent
which defines what intents can be used with this payment method (i.e.CHECKOUT
orVAULT
).- [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, ) }}
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 reference 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}
You can render your own UI for each payment method, or take advantage of the PrimerHeadlessUniversalCheckout.AssetsManager
helper to get access to payment methods' resources.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
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 paymentMethodResource = try { // 👇 Retrieve the assets of a specific payment method PrimerHeadlessUniversalCheckoutAssetsManager.getPaymentMethodResource( this, paymentMethodType ) } catch (e: SdkUninitializedException) { null } when (paymentMethodResource) { is PrimerPaymentMethodAsset -> { // 👇 Example of setting background color for your button paymentMethodResource.paymentMethodBackgroundColor.colored?.let { color -> setBackgroundColor(color) } // 👇 Example of setting drawable for your button paymentMethodResource.paymentMethodLogo.colored?.let { drawable -> setImageDrawable(drawable) } } is PrimerPaymentMethodNativeView -> { val paymentMethodView = paymentMethodResource.createView( requireContext() ) } null -> { // this should never happen in case SDK is initialized } }}
PrimerPaymentMethodAsset
The payment method asset contains the following:
paymentMethodType
a unique string identifier for the payment methodpaymentMethodName
a user friendly English localized string identifier for the payment method (e.g.Google Pay
)paymentMethodLogo
an instance of thePrimerPaymentMethodLogo
paymentMethodBackgroundColor
an instance of thePrimerPaymentMethodBackgroundColor
The PrimerPaymentMethodLogo
holds Drawable
objects for different scenarios:
- [Optional]
colored
aDrawable
to be used anywhere - [Optional]
dark
aDrawable
to be used on dark mode - [Optional]
light
aDrawable
to be used on light mode
The PrimerPaymentMethodBackgroundColor
holds @ColorInt
values 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.
PrimerPaymentMethodNativeView
Using the provided payment method views is required for certain payment methods, such as Google Pay. The Primer SDK ensures these views are seamlessly integrated and configured based on the PrimerPaymentMethodOptions and your client session details, where applicable.
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, 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 raw data manager is responsible for payment methods that need to receive data from the user (e.g. card form).PrimerHeadlessUniversalCheckoutNativeUiManager
The native UI manager is responsible for payment methods that need to present their own UI (e.g. Google Pay).
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.
12345678910111213141516171819202122
class CheckoutActivity : AppCompatActivity() { // other code goes here private fun onPaymentMethodSelected( paymentMethodType: String, sessionIntent: PrimerSessionIntent ) { try { // 👇 Create the payment method manager val nativeUiManager = PrimerHeadlessUniversalCheckoutNativeUiManager.newInstance(paymentMethodType) // 👇 Show the payment method nativeUiManager.showPaymentMethod(this, sessionIntent) } catch (e: SdkUninitializedException) { // handle exception } catch (e: UnsupportedPaymentIntentException) { // handle exception } catch (e: UnsupportedPaymentMethodException) { // handle exception } }}
Make sure that the sessionIntent
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.
Only when the data is valid, you can send it to Primer for further processing by calling submit
.
Configure the PrimerHeadlessUniversalCheckoutRawDataManager
and subscribe to callbacks
PrimerHeadlessUniversalCheckoutRawDataManager
and subscribe to callbacksAssuming 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 } }}
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 a List 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( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.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()}
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, // e.g. 4242424242424242 val expiryDate: String, // e.g. 01/2024, 1/2024, 12/2024... val cvv: String, // e.g. 244 val cardholderName: String? = null,) // 👇 Set your raw data on the managerrawDataManager.setRawData(getRawData(paymentMethodType))
When you set the data on the raw data manager, the manager validates it and notifies you via the listener 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 } }
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() }
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()
Step 5: Handle Listener Callbacks (Optional)
Handle onPreparationStarted
Callback (Optional)
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. }}
Handle onTokenizationStarted
Callback (Optional)
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. }}
Handle onPaymentMethodShowed
Callback (Optional)
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. }}
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) }}
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 SDK 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 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:
Field | Description |
---|---|
The customer's email address | |
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.