Complete guide to integrating the Paze component into your iOS 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 your SwiftUI view.
- Handle callbacks: Respond to payment success, errors, and other events.
The component automatically handles Paze presentment, in-app web 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. 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
- iOS 14.0 or later as your deployment target
- A Paze merchant account linked to the Unity Portal
Set the SDK environment to .test for UAT or .live for production.
Paze requires USD currency, a card intent in your transaction data, and merchantCategoryCode in the session for payment completion on iOS. We also recommend an onGetShopper callback on CheckoutConfig. These are covered in the steps below.
If portal or session setup fails, see Portal and session setup.
| Requirement | Minimum / notes |
|---|---|
| iOS | 14.0+ |
| Xcode | 15.0+ (Swift 5.9) |
| Swift | 5.9+ |
| Architectures | arm64 (device), arm64 and x86_64 (simulator) |
Transaction entryType | .ecom — required on TransactionData for Paze authorisation (not validated at presentment) |
The iOS Paze component uses static presentment — the button is shown after SDK validation without calling Paze's canCheckout() eligibility check. Dynamic presentment isn't available on iOS at this time.
Add PXPCheckoutSDK to your Xcode project using Swift Package Manager:
- In Xcode, go to File > Add Package Dependencies.
- Enter the package URL:
https://github.com/PXP-IO/ios-components-sdk - Select your app target and add the PXPCheckoutSDK product.
- Import the SDK in your Swift files:
import PXPCheckoutSDKThe Paze component is included in the main PXPCheckoutSDK package. You don't need a separate Paze dependency. SPM resolves transitive dependencies (including Kount) through the package manifest.
Paze checkout opens in ASWebAuthenticationSession and returns to your app via a custom URL scheme:
{callbackScheme}://pazeThe callback scheme is loaded from the SDK's bundled sdk-config.json (PXP_CALLBACK_SCHEME). The default is pxpcheckout, so the default return URL is pxpcheckout://paze.
Register the scheme in your app's Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>pxpcheckout</string>
</array>
</dict>
</array>Use a URL scheme that is unique to your app. If another app registers the same scheme, checkout callbacks may be intercepted.
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.Other session values include:
|
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. The numbers after the decimal will be zero padded if they are less than the expected currencyCode exponent. For example, USD 1 = USD 1.00. |
allowTransactionboolean | Whether or not to proceed with the transaction. |
The HMAC signature requires combining your timestamp, request ID, request path, and request body. Here's the format:
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.
You can now put together your Authorization header. It follows this format: PXP-UST1 {tokenId}:{timestamp}:{signature}.
Lastly, send your request to the Sessions API. You'll need to add a request ID of your choice and include your client ID, which you can find in the Unity Portal.
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.
iOS requires merchantCategoryCode in the session for the Paze complete step. If it's missing, the SDK throws SDK1202A when building the complete request.
Card intent on transactionData.intent.card (.authorisation or .purchase) controls the Unity authorisation after decryption. The Paze complete API always receives transactionType: "PURCHASE" from the SDK — you don't configure this separately. See Transaction intents.
Never embed session credentials, API tokens, or HMAC keys in your iOS app binary. Fetch session data from your backend at runtime.
Fetch session data from your backend, then initialise PxpCheckout with your configuration:
import PXPCheckoutSDK
// Session data from your backend
let sessionData = SessionData(
sessionId: session.sessionId,
hmacKey: session.hmacKey,
encryptionKey: session.encryptionKey,
allowedFundingTypes: AllowedFundingType(
wallets: Wallets(
paze: Paze(
clientId: session.pazeClientId,
profileId: session.pazeProfileId,
merchantCategoryCode: session.merchantCategoryCode
)
)
)
)
let merchantTransactionId = session.merchantTransactionId
let transactionData = TransactionData(
amount: 99.99,
currency: "USD",
entryType: .ecom,
intent: TransactionIntentData(card: .authorisation),
merchantTransactionId: merchantTransactionId,
merchantTransactionDate: { Date() }
)
let checkoutConfig = CheckoutConfig(
environment: .test,
session: sessionData,
transactionData: transactionData,
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-GROUP-1",
clientName: "Your Merchant",
siteName: "Your Store",
onGetShopper: { async in
TransactionShopper(
id: "shopper-123",
firstName: "John",
lastName: "Doe",
email: "customer@example.com"
)
},
analyticsEvent: { event in
print("Analytics: \(event.eventName)")
}
)
let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)Use the same merchantTransactionId in your session request and in transactionData.merchantTransactionId. If these values differ, reconciliation, callbacks, and transaction lookups won't align.
Create the Paze button through the SDK factory. Direct instantiation isn't supported.
let config = PazeButtonComponentConfig()
config.emailAddress = "customer@example.com"
config.phoneNumber = "15551234567"
config.paymentDescription = "Order #12345"
config.billingPreference = .all
config.performAVS = true
config.style = PazeButtonStyleConfig(
color: .auto,
shape: .rounded,
label: .checkoutWith
)
config.onPresentmentResolved = { isVisible in
print("Paze button visible:", isVisible)
}
config.onCheckoutComplete = { _ async in
// Return true to approve checkout and continue to complete/decrypt/authorise
return true
}
config.onPostAuthorisation = { result async in
// CRITICAL: Verify transaction outcome on backend before fulfilling order
await verifyPaymentOnBackend(systemTransactionId: result.systemTransactionId)
}
config.onSubmitError = { submitError async in
if let failed = submitError as? FailedSubmitResult {
print("Authorisation failed:", failed.errorReason ?? "")
}
}
config.onError = { error in
print("Paze error [\(error.errorCode)]: \(error.errorMessage)")
}
let pazeButton = try pxpCheckout.create(.pazeButton, componentConfig: config)The factory validates that session.allowedFundingTypes.wallets.paze.clientId is present. Without it, component creation throws UnsupportedFundingTypePazeSdkException (SDK0118).
| Parameter | Description |
|---|---|
emailAddressString? | Optional shopper email. Validated at presentment and again at checkout (RFC 5322, max 128 characters). Passed to Paze during session creation when provided. |
phoneNumberString? | Optional US phone: 11 digits starting with 1, without + (e.g. 15551234567). Validated when provided at presentment and checkout. |
paymentDescriptionString? | Statement descriptor forwarded to Paze. Maximum 25 characters. Validated during presentment initialisation. |
billingPreferencePazeBillingPreference? | Billing address collection in the Paze checkout. When nil, defaults to .all internally. Set to .all when using performAVS.Possible values:
|
confirmLaunchBool? | When true, Paze may prompt before launching checkout. |
sessionIdString? | Optional session ID echoed back by Paze. Falls back to CheckoutConfig.session.sessionId when not set. |
acceptedShippingCountries[String]? | ISO 3166-1 alpha-2 country codes (e.g. "US", "CA"). Each code must be exactly two uppercase letters. Validated at checkout time. |
acceptedPaymentCardNetworks[PazeAcceptedPaymentCardNetwork]? | Accepted card networks: .visa, .mastercard, .discover. When all three are selected or nil, no filter is sent. |
cobrand[PazeCobrandConfig]? | Optional cobrand entries for partner branding in the Paze wallet. Each entry requires a non-empty cobrandName. |
enhancedTransactionDataPazeEnhancedTransactionDataConfig? | Optional checkout metadata forwarded to Paze. See Configuration. |
style.colorenum | Button colour: .auto, .pazeBlue, .white, .whiteWithOutline, .midnightBlack. |
style.shapeenum | Button shape: .rounded, .rectangle, .pill. |
style.labelenum | Button label: .checkoutWith, .checkout, .pazeCheckout, .donateWith. |
style.disableMaxHeightBool? | When true, removes the default 48pt height cap. |
performAVSBool? | When true, includes billing address from decrypted payload in AVS fields when the SDK submits the transaction. Defaults to false. Applies only when the SDK manages decryption and authorisation. |
For the full list of component callbacks, see Events. For additional configuration examples, see Configuration.
Embed the Paze button in your SwiftUI view hierarchy using buildContent(). The component initialises presentment automatically when the view appears.
struct CheckoutView: View {
@State private var pazeButton: BaseComponent?
var body: some View {
VStack {
if let pazeButton {
pazeButton.buildContent()
.frame(maxWidth: .infinity)
} else {
ProgressView("Loading Paze...")
}
}
.onAppear {
Task { await loadPazeButton() }
}
}
private func loadPazeButton() async {
// Initialise pxpCheckout and create component (see steps 4–5)
}
}The component manages its own visibility and loading state:
- The button appears only after presentment validation succeeds (
onInit→onPresentmentResolved(true)). - A loading indicator overlays the button during checkout, complete, and authorisation.
- The button is non-interactive while loading.
- When presentment fails, the button is hidden,
onPresentmentResolved(false)is called, andonErrorfires.
When the view disappears, internal state (session coordinator, loading flags) is cleaned up automatically. No manual unmount call is required.
Most Paze callbacks are optional. Configure them on PazeButtonComponentConfig to customise checkout approval, decryption, and authorisation handling.
We recommend implementing onGetShopper on CheckoutConfig when calling PxpCheckout.initialize(config:) (not on the button component). The SDK calls it when building the transaction request after decryption. The Paze button doesn't call onGetShippingAddress during authorisation.
let checkoutConfig = CheckoutConfig(
// ... session and transactionData
onGetShopper: { async in
TransactionShopper(
id: "shopper-123",
firstName: "John",
lastName: "Doe",
email: "customer@example.com"
)
}
)If you implement onCheckoutComplete, return true to approve checkout and proceed to complete, decryption, and authorisation (default when omitted). Return false to cancel.
Use onPazeButtonClicked for pre-checkout logic (for example, validating cart state before the Paze web session opens). The Web component's onCustomValidation callback isn't available on iOS.
For a successful payment, callbacks fire in this order:
onInit→onPresentmentResolved(true)(on view attach)onPazeButtonClicked(on tap)onCheckoutComplete→ returntrueto continue (default when omitted)onComplete(complete API response —decryptedPayloadisnil)onPreDecryption→ returntrueto continue (default when omitted)onPostDecryption(decrypted token from Unity — skipped whenonPreDecryptionreturnsfalse)onPreAuthorisation→ return.proceed,.cancel, or.transactionInitData(...)(default.proceedwhen omitted)onPostAuthorisationwhen the SDK receives aMerchantSubmitResult, oronSubmitErroron submission 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 iOS, checkout opens in ASWebAuthenticationSession rather than an embedded Paze JavaScript SDK. The SDK calls the PXP create-session API, opens the returned checkout URL, and handles the pxpcheckout://paze callback (or your configured scheme) before proceeding to complete, decryption, and authorisation.
For a full sequence diagram and stage descriptions, see How it works.
When you return false from onPreDecryption, the SDK skips built-in decryption and authorisation. Your backend must decrypt the secured payload and submit the scheme token transaction.
Return false from onPreDecryption and forward the secured payload to your backend in onComplete:
config.onCheckoutComplete = { _ async in true }
config.onPreDecryption = { async in false }
config.onComplete = { result async in
guard let securedPayload = result.completeDecodedResponse?.securedPayload else { return }
// Call your backend — never call the PXP decrypt API directly from the app
await sendToBackendForDecryption(securedPayload: securedPayload)
}The onComplete callback receives a PazeCompleteResult with completeDecodedResponse.securedPayload. Pass this value to your backend.
From your backend, call the PXP decrypt-token endpoint using HMAC authentication. Use the same HMAC pattern as session creation.
When onPreDecryption returns true or is omitted, the iOS SDK decrypts internally using the session-authenticated decrypt API. Use the wallets endpoint below only for manual backend decryption.
| 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 |
Headers:
Content-Type: application/jsonAuthorization: PXP-UST1 <tokenId>:<timestamp>:<hmac>X-Request-Id: <uuid>X-Client-Id: <clientId>
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 | Where cryptogram / ECI go |
|---|---|
| SDK-managed | The gateway stores schemeTokenCryptogram and ECI in the session before the SDK submits the transaction. You don't pass them in the app transaction request. |
Manual backend (/api/v1/wallets/Paze/decrypt-token) | Map fields from the decrypt response into your transaction request: fundingData.schemeTokenCryptogram → fundingData.card.schemeTokenCryptogram, and fundingData.eci → threeDSecureData.electronicCommerceIndicator when required. |
After submission, verify the transaction on your backend before fulfilling the order. See Backend verification.
When using manual decryption, you are responsible for securely calling the decrypt API and submitting the transaction from your backend. The SDK doesn't proceed with authorisation.
Here's a complete SwiftUI example showing Paze integration:
import SwiftUI
import PXPCheckoutSDK
struct PazeCheckoutView: View {
@State private var pxpCheckout: PxpCheckout?
@State private var pazeButton: BaseComponent?
@State private var isLoading = true
@State private var errorMessage: String?
var body: some View {
VStack(spacing: 16) {
if isLoading {
ProgressView("Connecting...")
} else if let errorMessage {
Text(errorMessage).foregroundColor(.red)
} else if let pazeButton {
pazeButton.buildContent()
.frame(maxWidth: .infinity)
}
}
.padding()
.onAppear { Task { await setupPaze() } }
}
private func setupPaze() async {
do {
let session = try await fetchSessionFromBackend()
let checkout = try createPxpCheckout(session: session)
let config = buildPazeConfig()
let component = try checkout.create(.pazeButton, componentConfig: config)
await MainActor.run {
self.pxpCheckout = checkout
self.pazeButton = component
self.isLoading = false
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
private func createPxpCheckout(session: BackendSession) throws -> PxpCheckout {
let sessionData = SessionData(
sessionId: session.sessionId,
hmacKey: session.hmacKey,
encryptionKey: session.encryptionKey,
allowedFundingTypes: AllowedFundingType(
wallets: Wallets(
paze: Paze(
clientId: session.pazeClientId,
profileId: session.pazeProfileId,
merchantCategoryCode: session.merchantCategoryCode
)
)
)
)
let transactionData = TransactionData(
amount: session.amount,
currency: "USD",
entryType: .ecom,
intent: TransactionIntentData(card: .authorisation),
merchantTransactionId: session.merchantTransactionId,
merchantTransactionDate: { Date() }
)
let config = CheckoutConfig(
environment: .test,
session: sessionData,
transactionData: transactionData,
merchantShopperId: "shopper-123",
ownerId: session.ownerId,
clientName: "Your Merchant",
siteName: "Your Store",
onGetShopper: { async in
TransactionShopper(
id: "shopper-123",
firstName: "John",
lastName: "Doe",
email: "customer@example.com"
)
}
)
return try PxpCheckout.initialize(config: config)
}
private func buildPazeConfig() -> PazeButtonComponentConfig {
let config = PazeButtonComponentConfig()
config.emailAddress = "customer@example.com"
config.style = PazeButtonStyleConfig(color: .auto, shape: .rounded, label: .checkoutWith)
config.onCheckoutComplete = { _ async in true }
config.onPostAuthorisation = { result async in
await verifyPaymentOnBackend(systemTransactionId: result.systemTransactionId)
}
config.onSubmitError = { submitError async in
if let failed = submitError as? FailedSubmitResult {
print("Authorisation failed:", failed.errorReason ?? "")
}
}
config.onError = { error in
print("Paze error [\(error.errorCode)]: \(error.errorMessage)")
}
return config
}
private func fetchSessionFromBackend() async throws -> BackendSession {
// Replace with your backend API call
fatalError("Implement session fetch")
}
private func verifyPaymentOnBackend(systemTransactionId: String) async {
// Replace with your verification logic
}
}