Skip to content

Implementation

Complete guide to integrating the Paze component into your iOS 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 your SwiftUI view.
  3. 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.

Before you start

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.

Platform requirements

RequirementMinimum / notes
iOS14.0+
Xcode15.0+ (Swift 5.9)
Swift5.9+
Architecturesarm64 (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.

Step 1: Install the iOS SDK

Add PXPCheckoutSDK to your Xcode project using Swift Package Manager:

  1. In Xcode, go to File > Add Package Dependencies.
  2. Enter the package URL: https://github.com/PXP-IO/ios-components-sdk
  3. Select your app target and add the PXPCheckoutSDK product.
  4. Import the SDK in your Swift files:
import PXPCheckoutSDK

The 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.

Step 2: Register your URL callback scheme

Paze checkout opens in ASWebAuthenticationSession and returns to your app via a custom URL scheme:

{callbackScheme}://paze

The 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.

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:

ParameterDescription
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.

Other session values include:
  • Authorisation
  • Purchase
  • Verification
  • EstimatedAuthorisation
  • Payout
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. 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.
allowTransaction
boolean
Whether or not to proceed with the transaction.

Prepare and send your request

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
}'

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.

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.

Step 4: Initialise the SDK

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.

Step 5: Create and configure the Paze button

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).

Configuration options

ParameterDescription
emailAddress
String?
Optional shopper email. Validated at presentment and again at checkout (RFC 5322, max 128 characters). Passed to Paze during session creation when provided.
phoneNumber
String?
Optional US phone: 11 digits starting with 1, without + (e.g. 15551234567). Validated when provided at presentment and checkout.
paymentDescription
String?
Statement descriptor forwarded to Paze. Maximum 25 characters. Validated during presentment initialisation.
billingPreference
PazeBillingPreference?
Billing address collection in the Paze checkout. When nil, defaults to .all internally. Set to .all when using performAVS.

Possible values:
  • .all
  • .zipCountry
  • .none
confirmLaunch
Bool?
When true, Paze may prompt before launching checkout.
sessionId
String?
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.
enhancedTransactionData
PazeEnhancedTransactionDataConfig?
Optional checkout metadata forwarded to Paze. See Configuration.
style.color
enum
Button colour: .auto, .pazeBlue, .white, .whiteWithOutline, .midnightBlack.
style.shape
enum
Button shape: .rounded, .rectangle, .pill.
style.label
enum
Button label: .checkoutWith, .checkout, .pazeCheckout, .donateWith.
style.disableMaxHeight
Bool?
When true, removes the default 48pt height cap.
performAVS
Bool?
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.

Step 6: Display the component in your app

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 (onInitonPresentmentResolved(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, and onError fires.

When the view disappears, internal state (session coordinator, loading flags) is cleaned up automatically. No manual unmount call is required.

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 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:

  1. onInitonPresentmentResolved(true) (on view attach)
  2. onPazeButtonClicked (on tap)
  3. onCheckoutComplete → return true to continue (default when omitted)
  4. onComplete (complete API response — decryptedPayload is nil)
  5. onPreDecryption → return true to continue (default when omitted)
  6. onPostDecryption (decrypted token from Unity — skipped when onPreDecryption returns false)
  7. onPreAuthorisation → return .proceed, .cancel, or .transactionInitData(...) (default .proceed when omitted)
  8. onPostAuthorisation when the SDK receives a MerchantSubmitResult, or onSubmitError on 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.

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 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.

Backend decryption

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.

1. Configure the app

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.

2. Decrypt the secured payload

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.

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

Headers:

  • Content-Type: application/json
  • Authorization: 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.

3. 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.

Cryptogram and ECI handling

Decrypt pathWhere cryptogram / ECI go
SDK-managedThe 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.schemeTokenCryptogramfundingData.card.schemeTokenCryptogram, and fundingData.ecithreeDSecureData.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.

Complete example

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