Skip to content

Implementation

Complete guide to integrating the Paze component into your Android application.

Overview

The Paze component provides a fast, secure payment experience using the Paze digital wallet. The component follows a simple three-step lifecycle:

  1. Initialise: Configure the SDK with your session and transaction data.
  2. Create and display: Build the Paze button with your configuration and render it in Compose.
  3. Handle callbacks: Respond to payment success, errors, and checkout redirects.

The component automatically handles Paze presentment, Custom Tab checkout, token decryption, and transaction processing.

Backend verification is mandatory. Always verify payments on your backend before fulfilling orders. App callbacks can be manipulated by malicious users.

Before you start

Complete Paze onboarding in the Unity Portal before integrating the component. If you have not installed the SDK yet, start with Install the Android SDK. You will need:

  • Paze enabled at merchant group and site level, with your Paze client ID saved in Paze Account Settings
  • PXP API credentials (client ID and authentication token) for session creation on your backend
  • Android API 24 or later with Jetpack Compose
  • A Paze merchant account linked to the Unity Portal

Set the SDK environment to Environment.TEST for UAT or Environment.LIVE for production.

Paze requires USD currency and a card intent in your transaction data. Set transactionData.entryType to EntryType.Ecom for Paze authorisation requests. We also recommend an onGetShopper callback on SDK initialisation. These are covered in the steps below.

If portal or session setup fails, see Portal and session setup.

Platform requirements

RequirementMinimum / notes
Android API24+
UI frameworkJetpack Compose
Transaction entryTypeEntryType.Ecom (recommended for Paze authorisation; not validated at presentment on the standalone button)
BrowserChrome or another Custom Tabs-compatible browser for checkout

The button uses static presentment after SDK validation. Optional emailAddress and phoneNumber are validated when provided. See How it works.

Step 1: Install the Android SDK

Install the PXP Android Components SDK from Maven Central and configure Gradle. See Install the Android SDK for repositories, Compose setup, dependency version, permissions, and verification steps.

The Paze component is included in io.pxp:android-components-sdk. You don't need a separate Paze dependency.

Import the SDK in your Kotlin files:

import com.pxp.PxpCheckout
import com.pxp.checkout.components.paze.PazeButtonComponent
import com.pxp.checkout.components.paze.PazeButtonComponentConfig
import com.pxp.checkout.models.AllowedFundingTypes
import com.pxp.checkout.models.PazeWalletConfig
import com.pxp.checkout.models.SessionConfig
import com.pxp.checkout.models.WalletsConfig
import com.pxp.checkout.types.ComponentType

The Paze component is part of the main SDK package. You don't need a separate Paze dependency.

Step 2: Handle checkout redirects

Paze checkout opens in a Chrome Custom Tab and returns to your app via a deep link. The SDK merges PazeSdkRedirectActivity for pxpcheckout://callback and pxpcheckout://paze. You don't need your own intent filter for these default schemes.

When checkout completes, the SDK forwards the callback to your host Activity. Implement onNewIntent and deliver the result to your Paze component:

class CheckoutActivity : ComponentActivity() {

    private lateinit var pazeButton: PazeButtonComponent

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        intent.deliverPazeCheckoutResult(pazeButton)
    }
}
import com.pxp.checkout.components.paze.payment.deliverPazeCheckoutResult

The SDK registers your host Activity automatically via PazeRedirectRegistry when checkout launches from an Activity context. Ensure your checkout screen uses an Activity context (not only Application context) when creating PxpCheckout. The component also checks for pending redirect results when the host Activity resumes.

If onNewIntent doesn't call deliverPazeCheckoutResult, checkout may complete in the Custom Tab but your app won't process the payment.

For a custom callback scheme, register it through PazeRedirectRegistry and add a matching intent filter in your host app. See the SDK's PazeRedirectActivity for custom scheme integration.

Step 3: Create a session on your backend

The Paze component requires a session from the PXP Sessions API. This must be done on your backend using HMAC authentication to keep your credentials secure.

Understanding HMAC authentication

Our platform uses HMAC (Hash-based Message Authentication Code) with SHA256 for authentication to ensure secure communication and data integrity. This method involves creating a signature by hashing your request data with a secret key, which must then be included in the HTTP headers of your API request.

Create an HMAC signature

To create the HMAC signature, you need to prepare a string that includes four parts concatenated together:

  • Timestamp: The current time in Unix seconds (UTC) (e.g., 1754701373)
  • Request ID: A unique GUID for this request (e.g., ce244054-b372-42c2-9102-f0d976db69f6)
  • Request path: The API endpoint path: api/v1/sessions
  • Request body: The complete JSON request body as a minified string

Example request body to minify:

{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 120,
  "merchantTransactionId": "txn-123",
  "transactionMethod": {
    "intent": {
      "card": "Authorisation"
    }
  },
  "amounts": {
    "currencyCode": "USD",
    "transactionValue": 99.99
  },
  "allowTransaction": true
}

When creating the HMAC signature, the request body must be minified (no whitespace or formatting). The formatted JSON above is for readability only. Paze requires currencyCode to be USD.

Session request parameters

The session request accepts the following parameters:

Parameter Description
merchant
string (≤ 20 characters)
required
Your unique merchant identifier, as assigned by PXP. You can find it in the Unity Portal, by going to Merchant setup > Merchants and checking the Merchant ID column.
site
string (≤ 20 characters)
required
Your unique site identifier, as assigned by PXP. You can find it in the Unity Portal, by going to Merchant setup > Sites and checking the Site ID column.
merchantTransactionId
string (≤ 50 characters)
required
A unique identifier of your choice that represents this transaction.
sessionTimeout
number
required
The duration of the session, in minutes.
transactionMethod
object
required
Details about the transaction method, including the intent for each payment type.
transactionMethod.intent
object
required
The transaction intent for each payment method.
transactionMethod.intent.card
string
required
The intent for Paze transactions. Use Authorisation or Purchase for Paze wallet checkout.
amounts
object
required
Details about the transaction amount.
amounts.currencyCode
string (3 characters)
required
The currency code associated with the transaction, in ISO 4217 format. Paze supports USD only.
amounts.transactionValue
number
required
The transaction amount.
allowTransaction
boolean
Whether or not to proceed with the transaction.

Prepare and send your request

String to hash: {timestamp}{requestId}{requestPath}{requestBody}

Use your token value as the secret key to create an HMAC SHA256 hash of this string. The result will be a hex-encoded signature.

Your Authorization header follows this format: PXP-UST1 {tokenId}:{timestamp}:{signature}.

curl -i -X POST \
  'https://api-services.dev.pxp.io/api/v1/sessions' \
  -H 'Authorization: PXP-UST1 9aac6071-38d0-4545-9d2f-15b936af6d7f:1754701373:1DE2DFC390D7CD746A972140F26846AFA81CF85F5A0BAABA95DBC95301795EA6' \
  -H 'X-Request-Id: ce244054-b372-42c2-9102-f0d976db69f6' \
  -H 'X-Client-Id: f47ac10b-58cc-4372-a567-0e02b2c3d479' \
  -H 'Content-Type: application/json' \
  -d '{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 120,
  "merchantTransactionId": "txn-123",
  "transactionMethod": {
    "intent": {
      "card": "Authorisation"
    }
  },
  "amounts": {
    "currencyCode": "USD",
    "transactionValue": 99.99
  },
  "allowTransaction": true
}'

Session response

If your request is successful, you'll receive a 200 response containing the session data. When Paze is enabled for your site, the response includes Paze configuration:

{
  "sessionId": "c5f0799b-0839-43ce-abc5-5b462a98f250",
  "hmacKey": "904bc42395d4af634e2fd48ee8c2c7f52955a1da97a3aa3d82957ff12980a7bb",
  "encryptionKey": "20d175a669ad3f8c195c9c283fc86155",
  "sessionExpiry": "2025-05-19T13:39:20.3843454Z",
  "allowedFundingTypes": {
    "cardSchemes": ["Visa", "Mastercard", "AmericanExpress"],
    "cards": [],
    "wallets": {
      "paze": {
        "clientId": "your-paze-client-id",
        "profileId": "optional-profile-id",
        "merchantCategoryCode": "5812"
      }
    }
  }
}

Return the session response to your app together with the merchantTransactionId you sent in the session request. Your app must pass the same value to transactionData.merchantTransactionId when initialising the SDK.

Include merchantCategoryCode in allowedFundingTypes.wallets.paze in your session response. The SDK checks it when calling Paze complete after checkout; if missing, it throws SDK1202A.

When mapping the session response to SessionConfig, include data and locale as required by the SDK type.

Never embed session credentials, API tokens, or HMAC keys in your Android app binary. Fetch session data from your backend at runtime.

Fetch session data from your app

Call your backend session endpoint from a coroutine and deserialise the response into the types your SDK initialisation expects:

suspend fun fetchSessionFromBackend(): SessionResponse = withContext(Dispatchers.IO) {
    val connection = URL("https://your-backend/api/sessions")
        .openConnection() as HttpURLConnection
    connection.requestMethod = "POST"
    connection.connect()
    // Deserialise connection.inputStream into SessionResponse
}

Your backend endpoint wraps the PXP Sessions API. The request format and HMAC signing described above apply on the server, not in the Android app.

Step 4: Initialise the SDK

Fetch session data from your backend, then build PxpCheckout with your configuration:

val sessionData = SessionConfig(
    sessionId = session.sessionId,
    hmacKey = session.hmacKey,
    data = session.data,
    encryptionKey = session.encryptionKey,
    locale = "en-US",
    allowedFundingTypes = AllowedFundingTypes(
        wallets = WalletsConfig(
            paze = PazeWalletConfig(
                clientId = session.pazeClientId,
                profileId = session.pazeProfileId,
                merchantCategoryCode = session.merchantCategoryCode,
            ),
        ),
    ),
)

val transactionData = TransactionData(
    amount = 99.99,
    currency = "USD",
    merchant = "MERCHANT-1",
    intent = TransactionIntentData(card = CardIntentType.Authorisation),
    merchantTransactionId = session.merchantTransactionId,
    merchantTransactionDate = {
        LocalDateTime.now(ZoneOffset.UTC)
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
    },
    entryType = EntryType.Ecom,
)

val sdkConfig = PxpSdkConfig(
    environment = Environment.TEST,
    session = sessionData,
    transactionData = transactionData,
    clientName = "Your Merchant",
    siteName = "Your Store",
    ownerType = "MerchantGroup",
    ownerId = "MERCHANT-1",
    onGetShopper = {
        Shopper(
            id = "shopper-123",
            firstName = "John",
            lastName = "Doe",
            email = "customer@example.com",
        )
    },
    analyticsEvent = { event -> Log.d("PazeAnalytics", event.eventName) },
)

val checkout = PxpCheckout.builder()
    .withConfig(sdkConfig)
    .withContext(context)
    .build()

Use the same merchantTransactionId in your session request and in transactionData.merchantTransactionId. If these values differ, reconciliation, callbacks, and transaction lookups will not align.

Step 5: Create and configure the Paze button

Create the Paze button through the SDK factory:

val config = PazeButtonComponentConfig().apply {
    emailAddress = "customer@example.com"
    phoneNumber = "15551234567"
    paymentDescription = "Order #12345"
    billingPreference = PazeBillingPreference.ALL
    performAVS = true
    style = PazeButtonStyleConfig(
        color = PazeButtonColor.AUTO,
        shape = PazeButtonShape.DEFAULT,
        label = PazeButtonLabel.CHECKOUT_WITH,
    )

    onPresentmentResolved = { customTabAvailable ->
        Log.d("Paze", "Custom Tab available: $customTabAvailable")
    }
    onPazeButtonClicked = {
        clearCheckoutErrors()
    }
    onCheckoutComplete = { result ->
        // Return false to reject checkout after the Custom Tab returns
        true
    }
    onCheckoutIncomplete = { result ->
        Log.d("Paze", "Checkout incomplete: ${result.reason}")
    }
    onComplete = { result ->
        Log.d("Paze", "Payload ID: ${result.completeDecodedResponse?.payloadId}")
    }
    onPreDecryption = { true }
    onPostDecryption = { decryptedPayload ->
        val maskedToken = decryptedPayload.fundingData.networkToken.take(6) + "..."
        Log.d("Paze", "Decrypted token prefix: $maskedToken")
    }
    onPreAuthorisation = { PazePreAuthorisationResult.Proceed }
    onPostAuthorisation = { result ->
        verifyPaymentOnBackend(systemTransactionId = result.systemTransactionId)
    }
    onSubmitError = { result ->
        val failed = result as? FailedSubmitResult
        Log.e("Paze", "Submit error: ${failed?.errorReason}")
    }
    onError = { error ->
        Log.e("Paze", "Error [${error.errorCode}]: ${error.message}")
    }
}

val pazeButton = checkout.createComponent<PazeButtonComponent>(
    ComponentType.PAZE_BUTTON,
    config,
)

Missing clientId is detected at presentment and throws SDK1202.

For the full list of component properties and callbacks, see Configuration and Events.

Step 6: Display the component in your app

Render the Paze button in your Compose hierarchy:

@Composable
fun CheckoutScreen(pazeButton: PazeButtonComponent) {
    Column(modifier = Modifier.padding(16.dp)) {
        pazeButton.Content(modifier = Modifier.fillMaxWidth())
    }
}

Presentment runs automatically when you render Content() in Compose with a valid Context. The button is shown only when presentment validation succeeds; onPresentmentResolved indicates Custom Tab availability when the button is visible.

During create-session, complete, and authorisation, the SDK shows a loading indicator on the button and ignores taps while loading is active. After the Custom Tab opens, the button is interactive again until the redirect returns.

Step 7: Handle callbacks

Most Paze callbacks are optional. Configure them on PazeButtonComponentConfig to customise checkout approval, decryption, and authorisation handling.

We recommend implementing onGetShopper on PxpSdkConfig (not on the button component). The SDK calls it when building the transaction request after decryption.

Use onPazeButtonClicked for non-blocking side effects before the Custom Tab opens (for example analytics or clearing error messages). Disable the button in your UI to block checkout before the tab opens, or return false from onCheckoutComplete to block the flow after checkout returns.

If you implement onCheckoutComplete, return true to approve checkout and proceed to complete, decryption, and authorisation (default when omitted). Return false to cancel.

For a successful payment, callbacks fire in this order:

  1. onInitonPresentmentResolved (after presentment validation, when Content() is displayed)
  2. onPazeButtonClicked (on tap, after checkout validation succeeds)
  3. onCheckoutComplete → return true to continue (default when omitted)
  4. onComplete (complete API response)
  5. onPreDecryption → return true to continue (default when omitted)
  6. onPostDecryption (skipped when onPreDecryption returns false)
  7. onPreAuthorisationProceed, Cancel, or TransactionInitData(...) (default Proceed when omitted)
  8. onPostAuthorisation with MerchantSubmitResult (verify on your backend), or onSubmitError on hard failure

onCheckoutIncomplete handles shopper cancellation and merchant rejection from onCheckoutComplete. onError handles presentment, checkout, complete, and decryption errors.

For all component callbacks and payload types, see Events.

Never trust app callbacks for order fulfilment. Always verify payments on your backend using webhooks or the Get transaction details API before fulfilling orders.

Step 8: Backend verification (critical)

App callbacks can be manipulated by malicious users. You must verify all payments on your backend before fulfilling orders.

Use the same webhook and API verification patterns described in the card implementation guide.

Understanding the Paze payment flow

On Android, checkout opens in a Chrome Custom Tab. The SDK calls the PXP create-session API, opens the returned checkout URL, and handles the pxpcheckout://callback or pxpcheckout://paze redirect before calling the PXP complete API, decryption, and authorisation.

For a full sequence diagram and stage descriptions, see How it works.

Backend decryption

onPreDecryption controls whether the SDK decrypts the secured payload or your backend does:

  • SDK-managed (recommended): Return true or omit the callback. The SDK calls the PXP decrypt API, invokes onPostDecryption, then continues through onPreAuthorisation → transaction submission → onPostAuthorisation.
  • Merchant-managed: Return false. The SDK skips decrypt and authorisation. Read result.completeDecodedResponse?.securedPayload in onComplete and POST it to your backend, which calls the PXP decrypt endpoint and submits the scheme token transaction.
config.onPreDecryption = { true }
config.onPostDecryption = { decryptedPayload ->
    val masked = decryptedPayload.fundingData.networkToken.take(6) + "..."
    Log.d("Paze", "Decrypted token prefix: $masked")
}

Backend decrypt and transaction APIs

For merchant-managed decryption, configure the app as shown above, then call these endpoints from your backend:

From your backend, call the PXP decrypt-token endpoint using HMAC authentication.

When onPreDecryption returns true or is omitted, the Android SDK decrypts internally via POST /api/v1/paze/decrypt-token using session authentication. Use the wallets endpoint below only for manual backend decryption with HMAC (PXP-UST1).

EnvironmentEndpoint
TestPOST https://api-services.dev.pxp.io/api/v1/wallets/Paze/decrypt-token
LivePOST https://api-services.pxp.io/api/v1/wallets/Paze/decrypt-token

Request body:

{
  "site": "your-site-id",
  "token": "eyJhdWQHBmaW..."
}

The token value is the securedPayload from onComplete.

Submit the scheme token transaction

Use the decrypted fundingData to submit an authorisation through the PXP Create transaction API (POST /api/v1/transactions) from your backend.

Decrypt path Transaction payload
SDK-managedThe Android SDK submits schemeTokenNumber, expiry, and walletType: "Paze" via session-authenticated processTransaction. Cryptogram handling is managed by the gateway, not included explicitly in the SDK client request.
Manual backendMap networkToken, expiry, walletType: "Paze", and any cryptogram or ECI fields from the decrypt response into your HMAC transaction request.

After submission, verify the transaction on your backend before fulfilling the order. See Backend verification.

Complete example

class PazeCheckoutActivity : ComponentActivity() {

    private var checkout: PxpCheckout? = null
    private var pazeButton: PazeButtonComponent? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var isLoading by remember { mutableStateOf(true) }
            var errorMessage by remember { mutableStateOf<String?>(null) }

            LaunchedEffect(Unit) {
                try {
                    val session = fetchSessionFromBackend()
                    val pxpCheckout = createPxpCheckout(session)
                    val component = pxpCheckout.createComponent<PazeButtonComponent>(
                        ComponentType.PAZE_BUTTON,
                        buildPazeConfig(),
                    )
                    checkout = pxpCheckout
                    pazeButton = component
                    isLoading = false
                } catch (e: Exception) {
                    errorMessage = e.message
                    isLoading = false
                }
            }

            when {
                isLoading -> CircularProgressIndicator()
                errorMessage != null -> Text(errorMessage!!, color = Color.Red)
                pazeButton != null -> pazeButton!!.Content(Modifier.fillMaxWidth())
            }
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        pazeButton?.let { intent.deliverPazeCheckoutResult(it) }
    }

    private fun buildPazeConfig() = PazeButtonComponentConfig().apply {
        emailAddress = "customer@example.com"
        onCheckoutComplete = { _ -> true }
        onPostAuthorisation = { result ->
            verifyPaymentOnBackend(result.systemTransactionId)
        }
    }
}

What's next?

  • Configuration: Explore advanced Paze button options and styling.
  • Events: Reference all callbacks and payload types.
  • Testing: Test your integration in the UAT environment.
  • Troubleshooting: Diagnose common integration issues.