Complete guide to integrating the Paze component into your Android application.
The Paze component provides a fast, secure payment experience using the Paze digital wallet. The component follows a simple three-step lifecycle:
- Initialise: Configure the SDK with your session and transaction data.
- Create and display: Build the Paze button with your configuration and render it in Compose.
- 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.
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.
| Requirement | Minimum / notes |
|---|---|
| Android API | 24+ |
| UI framework | Jetpack Compose |
Transaction entryType | EntryType.Ecom (recommended for Paze authorisation; not validated at presentment on the standalone button) |
| Browser | Chrome 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.
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.ComponentTypeThe Paze component is part of the main SDK package. You don't need a separate Paze dependency.
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.deliverPazeCheckoutResultThe 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.
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.
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.
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.
The session request accepts the following parameters:
| Parameter | Description |
|---|---|
merchantstring (≤ 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. |
sitestring (≤ 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. |
merchantTransactionIdstring (≤ 50 characters) required | A unique identifier of your choice that represents this transaction. |
sessionTimeoutnumber required | The duration of the session, in minutes. |
transactionMethodobject required | Details about the transaction method, including the intent for each payment type. |
transactionMethod.intentobject required | The transaction intent for each payment method. |
transactionMethod.intent.cardstring required | The intent for Paze transactions. Use Authorisation or Purchase for Paze wallet checkout. |
amountsobject required | Details about the transaction amount. |
amounts.currencyCodestring (3 characters) required | The currency code associated with the transaction, in ISO 4217 format. Paze supports USD only. |
amounts.transactionValuenumber required | The transaction amount. |
allowTransactionboolean | Whether or not to proceed with the transaction. |
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
}'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.
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.
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.
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.
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.
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:
onInit→onPresentmentResolved(after presentment validation, whenContent()is displayed)onPazeButtonClicked(on tap, after checkout validation succeeds)onCheckoutComplete→ returntrueto continue (default when omitted)onComplete(complete API response)onPreDecryption→ returntrueto continue (default when omitted)onPostDecryption(skipped whenonPreDecryptionreturnsfalse)onPreAuthorisation→Proceed,Cancel, orTransactionInitData(...)(defaultProceedwhen omitted)onPostAuthorisationwithMerchantSubmitResult(verify on your backend), oronSubmitErroron 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.
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.
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.
onPreDecryption controls whether the SDK decrypts the secured payload or your backend does:
- SDK-managed (recommended): Return
trueor omit the callback. The SDK calls the PXP decrypt API, invokesonPostDecryption, then continues throughonPreAuthorisation→ transaction submission →onPostAuthorisation. - Merchant-managed: Return
false. The SDK skips decrypt and authorisation. Readresult.completeDecodedResponse?.securedPayloadinonCompleteand 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")
}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).
| Environment | Endpoint |
|---|---|
| Test | POST https://api-services.dev.pxp.io/api/v1/wallets/Paze/decrypt-token |
| Live | POST 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.
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-managed | The 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 backend | Map 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.
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)
}
}
}- 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.