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:

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:

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:

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",    }}

To use new Workflows and all of its exciting features, make sure to pass the following header in your API request:

Legacy-Workflows: false

See this migration guide for more information.

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 documentation is only relevant for v2.17.0-rc.1 and upward.

The migration guide to update from v2.16.1 is available here.

The documentation for versions v2.5.0 up to v2.16.1 is available here.

Install the SDK

With CocoaPods

Add the following in your Podfile:

use_frameworks! target 'MyApp' do   # 👇 Add this line  pod 'PrimerSDK'   # ... end

Then run pod install to install PrimerSDK on your workspace.

In case you encounter an error that the bundle needs signing on Xcode 14, add the following post-install script in your podfile.

post_install do |installer|  installer.pods_project.targets.each do |target|    if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"      target.build_configurations.each do |config|          config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'      end    end  endend

With Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into Xcode. In order to add PrimerSDK with Swift Package Manager:

  1. 1
    Select your project, and then navigate to Package Dependencies
  2. 2
    Click on the + button at the bottom-left of the Packages section
  3. 3
    Paste https://github.com/primer-io/primer-sdk-ios.git into the Search Bar, or search for primer-sdk-ios in the Search Bar.
  4. 4
    Press Add Package
  5. 5
    Let Xcode download the package and set everything up

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

Step 1: Prepare Headless Universal Checkout

First, import the PrimerSDK. Then call PrimerHeadlessUniversalCheckout.current.start to start the checkout session with your client token and a delegate to receive important events.

The completion handler will return the available payment methods for the current session. We will use them later to present a list of payment methods.

Each available payment method contains the following:

  • paymentMethodType
    a unique string identifier for the payment method (e.g. ADYEN_IDEAL)
  • paymentMethodName
    an user friendly English localized string identifier for the payment method (e.g. Apple Pay)
  • supportedPrimerSessionIntents
    an array of PrimerSessionIntent which defines what intents can be used with this payment method (i.e. .checkout or .vault).
  • paymentMethodManagerCategories
    an array which defines the payment method managers that can be used with this payment method (i.e. .cardComponents, .nativeUI or .rawData).
  • [Optional] requiredInputDataClass
    the class type that should be used with the raw data manager.

Implement the primerHeadlessUniversalCheckoutDidCompleteCheckoutWithData(_:) which will notify you when the checkout is complete with a payment.

import UIKit// 👇 Import the SDKimport PrimerSDK class ViewController: UIViewController {     // ...     var availablePaymentMethods: [PrimerHeadlessUniversalCheckout.PaymentMethod]?     override func viewDidLoad() {        super.viewDidLoad()         // ...         // 👇 Start Headless Universal Checkout with your session's client token        PrimerHeadlessUniversalCheckout.current.start(withClientToken: clientToken, delegate: self) { availablePaymentMethods, err in            if let err = err {                // Handle error            } else if let availablePaymentMethods = availablePaymentMethods {                // Payment methods that are available for this session                self.availablePaymentMethods = availablePaymentMethods            }        }    }} extension ViewController: PrimerHeadlessUniversalCheckoutDelegate {     // 👇 [Required] This function will return the checkout data once the payment is finished    func primerHeadlessUniversalCheckoutDidCompleteCheckoutWithData(_ data: PrimerCheckoutData) {        // ...    }}

Step 2: Render available payment methods UI

You should now have the available payment methods for the client session.

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.

do {    let paymentMethodAsset = try PrimerHeadlessUniversalCheckout.AssetsManager.getPaymentMethodAsset(for: paymentMethod.paymentMethodType)} catch {    // Handle error}

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 UIImage objects for different scenarios

  • [Optional] colored
    a UIImage to be used anywhere
  • [Optional] dark
    a UIImage to be used in dark mode
  • [Optional] light
    a UIImage to be used in light mode

The PrimerPaymentMethodBackgroundColor holds UIColor objects for different scenarios

  • [Optional] colored
    a UIColor to be used anywhere
  • [Optional] dark
    a UIColor to be used in dark mode
  • [Optional] light
    a UIColor to be used in light mode

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

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

Step 3: 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, 3 payment methods managers are supported, each of them responsible for a different payment method category.

  • PrimerHeadlessUniversalCheckout.NativeUIManager
    The native UI manager is responsible for payment methods that need to present their own UI (e.g. Apple Pay).
  • PrimerHeadlessUniversalCheckout.RawDataManager
    The raw data manager is responsible for payment methods that need to receive data from the user (e.g. card form).
  • PrimerHeadlessUniversalCheckout.CardComponentsManager
    The card components manager is responsible for payment methods that leverage our built-in card form components (e.g. card form).

Bear in mind that a payment method could be implemented by more than one payment method manager. 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 primerHeadlessUniversalCheckoutDidLoadAvailablePaymentMethods is triggered and payment methods for the provided clientToken are returned.

If that’s not the case Headless Checkout throws a uninitialized-sdk-session error.

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), Headless Checkot throws an unsupported-payment-method-type error.

Native UI manager

This manager can be used for the payment methods that contain .nativeUI in the paymentMethodManagerCategories array.

The native UI manager is typically used for the payment methods that to present their own UI, such as Apple Pay.

import UIKitimport PrimerSDK class ViewController: UIViewController {     // ...     @IBAction func paymentMethodButtonTapped(_ sender: UIButton) {        do {            // 👇 Create the payment method manager            let nativeUIPaymentMethodManager = try PrimerHeadlessUniversalCheckout.NativeUIManager(paymentMethodType: paymentMethod.paymentMethodType)             // 👇 Show the payment method            try nativeUIPaymentMethodManager.showPaymentMethod(intent: .checkout)         } catch {            // Handle error        }    }}

Make sure that the intent passed in the showPaymentMethod function (.checkout for making a payment, .vault for vaulting the payment method) is supported by the payment method. Check the supportedPrimerSessionIntents array of the payment method.

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 primerHeadlessUniversalCheckoutDidCompleteCheckoutWithData configured in Step 1.

Raw data manager

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

The raw data manager can be used for payment methods that allow you to capture customer data yourself. This gives you full control over the UI while still taking advantage of all the features that make Primer great.

At any point, you can validate your data by calling setRawData() and by listening to primerRawDataManager(_: dataIsValid: errors:) - the SDK will return errors specific to each required input.

Once the data is valid, send it to Headless Checkout for further processing by calling submit().

Configure the PrimerHeadlessUniversalCheckout.RawDataManager and conform to its delegate

Assuming that the payment method contains .rawData in the paymentMethodManagerCategories, create your raw data manager, and set its delegate.

import UIKitimport PrimerSDK class ViewController: UIViewController {     // ...     var rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager!     override func viewDidLoad() {        super.viewDidLoad()         // ...         do {            // 👇 Initialize the raw data manager            self.rawDataManager = try PrimerHeadlessUniversalCheckout.RawDataManager(paymentMethodType: paymentMethod.paymentMethodType, delete: self)         } catch {            // Handle error        }    }}

Build a form

Build your own form to let the users enter their data. You can use the requiredInputElementTypes of the PrimerHeadlessUniversalCheckout.RawDataManager object to get informed about what input fields you have to render.

The requiredInputElementTypes is an array that can contain any of the following:

  • .cardNumber
  • .expiryDate
  • .cvv
  • .cardholderName
  • .phoneNumber
  • .retailer
func renderForm() {    let inputElementTypes = primerRawDataManager.requiredInputElementTypes     for inputElementType in inputElementTypes {        switch inputElementType {            case .cardNumber:                // Create a card number input element and add it to your view                break             case .expiryDate:                // Create a expiry date input element and add it to your view                break             case .cvv:                // Create a CVV input element and add it to your view                break             case .cardholderName:                // Create a cardholder name input element and add it to your view                break             case .phoneNumber:                // Create a phone number input element and add it to your view                break             case .retailer:                // Create a retailer input element and add it to your view                break            }        }    }}

Build and validate your raw data

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

import UIKitimport PrimerSDK class ViewController: UIViewController {     // ...     func dataDidChange() {        // 👇 Create your raw data        let rawCardData = PrimerCardData(            cardNumber: cardnumberTextField.text,            expiryDate: expiryDateTextField.text, // 👈 ex. 03/2030            cvv: cvvTextField.text,            cardholderName: cardholderNameTextField.text,        )         // 👇 Set your raw data on the manager        self.rawDataManager.rawData = rawCardData    }}

When you set 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 validation changes.

extension ViewController: PrimerHeadlessUniversalCheckoutRawDataManagerDelegate {     // 👇 Implement the validation function to receive updates on the raw data validation changes    func primerRawDataManager(_ rawDataManager: PrimerHeadlessUniversalCheckout.RawDataManager, dataIsValid isValid: Bool, errors: [Error]?) {        // Enable your submit button when `isValid == true`    }}

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

Submit the form's data

Once the data has been validated, call the manager's submit() function.

import UIKitimport PrimerSDK class ViewController: UIViewController {         // ...         @IBAction func payButtonTapped(_ sender: UIButton) {            // 👇 Submit the data that have been set in prior steps            self.rawDataManager.submit()        }}

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

Card components UI manager

The card components UI manager 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.

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

Assuming that the payment method's type is PAYMENT_CARD, create your card components UI manager and your input elements.

import UIKitimport PrimerSDK class ViewController: UIViewController {     // ...     var cardComponentsManager: PrimerHeadlessUniversalCheckout.CardComponentsManager!     override func viewDidLoad() {        super.viewDidLoad()         // ...         do {            // 👇 Optionally, you can also set its delegate to get notified about validation changes            self.cardComponentsManager = try PrimerHeadlessUniversalCheckout.CardComponentsManager()             var tmpInputElements: [PrimerHeadlessUniversalCheckoutInputElement] = []            for inputElementType in self.cardComponentsManager.requiredInputElementTypes {                // 👇 For each inputElementType create a PrimerInputTextField                let textField = PrimerInputTextField(type: inputElementType, frame: .zero)                 // 👇 Optionally set the input element's delegate to receive events                textField.inputElementDelegate = self                 // 👇 Style the input element                textField.borderStyle = .line                textField.layer.borderColor = UIColor.black.cgColor                self.stackView.addArrangedSubview(textField)                textField.translatesAutoresizingMaskIntoConstraints = false                textField.heightAnchor.constraint(equalToConstant: 50).isActive = true                 if inputElementType == .cardNumber {                    self.cardNumberTextField = textField                    self.cardNumberTextField?.placeholder = "Card number"                } else if inputElementType == .expiryDate {                    self.expiryTextField = textField                    self.expiryTextField?.placeholder = "Expiry"                } else if inputElementType == .cvv {                    self.cvvTextField = textField                    self.cvvTextField?.placeholder = "CVV"                } else if inputElementType == .cardholderName {                    self.cardHolderNameTextField = textField                    self.cardHolderNameTextField?.placeholder = "Cardholder"                }                 tmpInputElements.append(textField)            }             // 👇 Set the input elements on the SDK            self.cardComponentsManager.inputElements = tmpInputElements         } catch {            // Handle error        }    }}

Conform to the PrimerHeadlessUniversalCheckoutCardComponentsManagerDelegate

Make sure to conform to the PrimerHeadlessUniversalCheckoutCardComponentsManagerDelegate and listen to the form validation events.

This way you can enable/disable your submit button depending on whether or not the form is valid.

extension ViewController: PrimerHeadlessUniversalCheckoutCardComponentsManagerDelegate {     func cardFormUIManager(_ cardFormUIManager: PrimerHeadlessUniversalCheckout.CardComponentsManager, isCardFormValid: Bool) {        // Apply changes on your UI based on whether the card form's data are valid.    }}

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, set the text fields’ inputElementDelegate and conform to the PrimerInputElementDelegate and listen to the input element field's update.

extension ViewController: PrimerInputElementDelegate {     func inputElementShouldFocus(_ sender: PrimerInputElement) -> Bool {     }     func inputElementDidFocus(_ sender: PrimerInputElement) {     }     func inputElementShouldBlur(_ sender: PrimerInputElement) -> Bool {     }     func inputElementDidBlur(_ sender: PrimerInputElement) {     }     func inputElementValueDidChange(_ sender: PrimerInputElement) {     }     func inputElementValueIsValid(_ sender: PrimerInputElement, isValid: Bool) {     }     func inputElementDidDetectType(_ sender: PrimerInputElement, type: Any?) {     }}

Configure the Pay button

Finally, configure a pay action:

class ViewController {     // ...     @IBAction func payButtonTapped(_ sender: UIButton) {        self.cardComponentsManager.submit()    }}

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

Step 4: Handle errors

Some functions can throw and they will fail immediately. All other errors will end up in the SDK’s optional delegate function func primerHeadlessUniversalCheckoutDidFail(withError:checkoutData:), where you can handle them accordingly.

extension ViewController: PrimerHeadlessUniversalCheckoutDelegate {     func primerHeadlessUniversalCheckoutDidFail(withError err: Error, checkoutData: PrimerCheckoutData?) {        // Handle the 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 API reference for a full list of the delegate 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:
  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