Skip to content

Implementation

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

Overview

The Aeropay component lets shoppers make payments or receive payouts through a linked US bank account. The integration follows a three-stage lifecycle:

  1. Initialise: Configure the SDK with an Aeropay-enabled session and transaction data.
  2. Create and render: Create the Aeropay button and show it in your Jetpack Compose UI.
  3. Handle the flow: Respond to custom validation, user verification, transaction results, cancellation, and errors.

The SDK handles consumer data collection, OTP verification, bank selection, Aerosync bank linking, and pay-by-bank transaction submission.

Backend verification is mandatory. Always verify transactions on your backend before fulfilling orders or confirming payouts. Client callbacks can be manipulated by malicious users.

Before you start

Complete Aeropay onboarding in the Unity Portal before integrating the component. You'll need:

  • Aeropay enabled at merchant group and site level.
  • Your Aeropay merchant ID, API key, API secret, and configuration ID added to the Aeropay service.
  • PXP API credentials for creating sessions on your backend.
  • An Android app that meets the SDK requirements (minSdk 24, Jetpack Compose, and Kotlin 2.x).

Aeropay transactions must also meet these requirements:

SettingRequired value
CurrencyUSD
Entry typeEcom
Intent
  • Authorisation
  • Purchase
  • EstimatedAuthorisation
  • Payout

Step 1: Install the Android SDK

See Install the Android SDK for full Gradle, Compose plugin, and minSdk setup. Add the PXP Android Components SDK from Maven Central, then continue with the Aeropay session and component steps on this page.

Step 2: Create a session on your backend

Create sessions on your backend using PXP-UST1 HMAC authentication. Never expose your PXP token value or other API credentials in the Android app.

Send a POST request to /api/v1/sessions with the Aeropay intent under transactionMethod.intent.aeropay:

{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 1200,
  "merchantTransactionId": "0f3501f0-2577-4dee-8be2-bbb6908588e3",
  "amounts": {
    "currencyCode": "USD",
    "transactionValue": 25.00
  },
  "transactionMethod": {
    "intent": {
      "aeropay": "Authorisation"
    }
  },
  "allowTransaction": true
}

Configure the request with these Aeropay-specific values:

PropertyDescription
merchantTransactionId
string
required
Your unique identifier for the session transaction. We recommend reusing the value when initialising the SDK to simplify reconciliation.
amounts.currencyCode
string
required
Transaction currency. Set this to USD.
amounts.transactionValue
number
required
The payment or payout amount.
transactionMethod.intent.aeropay
string
required
The Aeropay transaction intent. Possible values:
  • Authorisation
  • Purchase
  • EstimatedAuthorisation
  • Payout
allowTransaction
boolean
Whether the session can be used to submit a transaction.

Session response

If Aeropay is configured for the site, the session response includes its funding configuration:

{
  "sessionId": "c5f0799b-0839-43ce-abc5-5b462a98f250",
  "hmacKey": "904bc42395d4af634e2fd48ee8c2c7f52955a1da97a3aa3d82957ff12980a7bb",
  "encryptionKey": "20d175a669ad3f8c195c9c283fc86155",
  "data": "eyJzZXNzaW9uSWQiOiJjNWYwNzk5Yi0wODM5LTQzY2UtYWJjNS01YjQ2MmE5OGYyNTAifQ==",
  "sessionExpiry": "2026-07-21T12:00:00.000Z",
  "allowedFundingTypes": {
    "payByBanks": {
      "aeropay": {
        "externalMerchantId": "aeropay-merchant-id",
        "configurationId": "aerosync-configuration-id"
      }
    }
  }
}

Pass data through to SessionConfig.data when you initialise the SDK in Step 3. The SDK also requires both allowedFundingTypes.payByBanks.aeropay.externalMerchantId and allowedFundingTypes.payByBanks.aeropay.configurationId.

PXP adds the Aeropay funding configuration to the session automatically from your Unity Portal setup. Don't add Aeropay credentials to the response yourself.

Return the session and merchantTransactionId to your Android app.

Step 3: Initialise the SDK

Request the session from your backend, then build PxpCheckout:

import android.content.Context
import com.pxp.PxpCheckout
import com.pxp.checkout.models.AeropayFundingConfig
import com.pxp.checkout.models.AeropayIntentType
import com.pxp.checkout.models.AllowedFundingTypes
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.Environment
import com.pxp.checkout.models.PayByBanksConfig
import com.pxp.checkout.models.PxpSdkConfig
import com.pxp.checkout.models.SessionConfig
import com.pxp.checkout.models.TransactionData
import com.pxp.checkout.models.TransactionIntentData
import com.pxp.checkout.services.models.transaction.Shopper
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

fun createPxpCheckout(
    context: Context,
    sessionResult: SessionResult,
): PxpCheckout {
    val sessionConfig = SessionConfig(
        sessionId = sessionResult.session.sessionId,
        hmacKey = sessionResult.session.hmacKey,
        encryptionKey = sessionResult.session.encryptionKey,
        data = sessionResult.session.data,
        locale = "en-US",
        allowedFundingTypes = AllowedFundingTypes(
            payByBanks = PayByBanksConfig(
                aeropay = AeropayFundingConfig(
                    externalMerchantId = sessionResult.session.aeropayExternalMerchantId,
                    configurationId = sessionResult.session.aeropayConfigurationId,
                ),
            ),
        ),
    )

    val transactionData = TransactionData(
        amount = 25.00,
        currency = "USD",
        entryType = EntryType.Ecom,
        intent = TransactionIntentData(aeropay = AeropayIntentType.Authorisation),
        merchant = "your-merchant-id",
        merchantTransactionId = sessionResult.merchantTransactionId,
        merchantTransactionDate = {
            LocalDateTime.now(ZoneOffset.UTC)
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
        },
    )

    val sdkConfig = PxpSdkConfig(
        environment = Environment.TEST,
        session = sessionConfig,
        transactionData = transactionData,
        clientId = "your-client-id",
        ownerType = "MerchantGroup",
        ownerId = "MERCHANT_GROUP_1",
        onGetShopper = {
            Shopper(
                id = "shopper-123",
                firstName = "John",
                lastName = "Doe",
                email = "john.doe@example.com",
                phoneNumber = "+14155550123",
            )
        },
    )

    return PxpCheckout.builder()
        .withConfig(sdkConfig)
        .withContext(context)
        .build()
}

Set environment to Environment.TEST for UAT or Environment.LIVE for production. Map those values to Aerosync as SANDBOX and PROD respectively.

Keep the session request and SDK transaction data consistent so that you can trace and reconcile transactions. The Android SDK doesn't compare the merchantTransactionId, amount, currency, or intent with the values used to create the session. Verify the completed transaction against records stored on your backend.

Shopper data

For new shoppers, the SDK uses onGetShopper to prefill the consumer data screen. It supports these fields:

  • firstName
  • lastName
  • email
  • phoneNumber

If you provide a field, it must be valid. First and last names accept a maximum of 100 Unicode letters and spaces. Email addresses accept a maximum of 128 characters and must match the SDK's email format. Phone numbers must contain +1 followed by ten digits, such as +14155550123.

If a supplied name, email address, or phone number is non-empty but invalid, the SDK invokes onError with SDK1300 before opening the popup.

Return only the data required for the flow. Don't include personal data in merchantTransactionId, shopper.id, or free-text order descriptions.

Step 4: Create the Aeropay button

Create the component with checkout.createComponent():

import com.pxp.checkout.components.aeropaybutton.AeropayButtonComponent
import com.pxp.checkout.components.aeropaybutton.types.AeropayButtonComponentConfig
import com.pxp.checkout.exceptions.BaseSdkException
import com.pxp.checkout.models.FailedSubmitResult
import com.pxp.checkout.types.ComponentType

val config = AeropayButtonComponentConfig(label = "Pay by bank").apply {
    onCustomValidation = {
        validateCheckoutReady() // must return Boolean
    }
    onUserVerificationSuccess = { user ->
        saveAeropayUserId(user.id)
    }
    onCancel = {
        resetCheckoutState()
    }
    onPreAuthorisation = {
        validateOrderOnBackend() // must return Boolean
    }
    onPostAuthorisation = { result ->
        verifyPaymentOnBackend(result)
    }
    onSubmitError = { error ->
        if (error is FailedSubmitResult) {
            showPaymentError("Unable to complete the transaction.")
        }
    }
    onError = { error ->
        showPaymentError("Unable to continue with Aeropay.")
    }
}

val aeropayButton: AeropayButtonComponent? =
    try {
        checkout.createComponent(ComponentType.AERO_PAY_BUTTON, config)
    } catch (error: BaseSdkException) {
        // e.g. SDK0113–SDK0116 for funding, entry type, intent, or currency
        showAlternativePaymentMethods()
        null
    }

Invalid Aeropay session or transaction configuration throws from createComponent() as a BaseSdkException (for example SDK0113, SDK0114, SDK0115, or SDK0116). Catch it at creation time; the button isn't created when these checks fail.

Both onCustomValidation and onPreAuthorisation are suspend () -> Boolean. Return true to continue.

Implement onPreAuthorisation and return true when the transaction can proceed. If this callback is omitted or doesn't return true, the SDK doesn't submit the transaction.

onCustomValidation runs after the optional ButtonComponentConfig.onClick callback (if set) and before the Aeropay popup opens. Return false to keep the popup closed.

onCancel runs when the shopper dismisses the popup, including the close control and system back or scrim dismiss. Successful payment dismissal is programmatic and doesn't invoke onCancel.

For all component properties, see Configuration. For callback payloads and examples, see Events.

Step 5: Render the button

Call Content() in your Compose hierarchy:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier

@Composable
fun AeropayCheckoutButton(aeropayButton: AeropayButtonComponent?) {
    aeropayButton?.Content(modifier = Modifier.fillMaxWidth())
}

Create the component in a LaunchedEffect or equivalent lifecycle scope, then store it in Compose state so recomposition can render it.

Step 6: Verify the transaction on your backend

After transaction submission, onPostAuthorisation receives the merchant and PXP transaction identifiers. Send the result to your backend and retrieve the transaction from PXP before updating the order or payout:

config.onPostAuthorisation = { submitResult ->
    verifyPaymentOnBackend(
        merchantTransactionId = submitResult.merchantTransactionId,
        systemTransactionId = submitResult.systemTransactionId,
    )
}

Your backend verification must:

  1. Authenticate directly with PXP.
  2. Retrieve the transaction using its transaction identifier.
  3. Confirm that the merchant, amount, currency, intent, and final state match your records.
  4. Update the order or payout only after every check passes.

Don't trust values sent only from the app. Use them to identify the transaction, then compare the authoritative PXP response with values stored on your backend.

Manage post-transaction operations

After a successful Aeropay transaction, use the merchant and system transaction identifiers from onPostAuthorisation to look up the payment and manage follow-up operations on your backend or in the Unity Portal. Available actions depend on the transaction intent and current state. Typical post-authorisation actions include capture, increment, void, and refund. A Purchase captures funds at submission, so it doesn't need a separate capture.

New-shopper flow

Use the new-shopper flow when you don't have a verified Aeropay user ID. Don't set userId on the component.

The shopper completes these stages:

  1. Reviews or enters their first name, last name, email address, and phone number.
  2. Enters the OTP sent to their phone.
  3. Selects a linked bank account or links a new account through Aerosync.
  4. Confirms the transaction.

Save the verified Aeropay user ID in onUserVerificationSuccess:

config.onUserVerificationSuccess = { user ->
    saveAeropayUserId(
        shopperId = "shopper-123",
        aeropayUserId = user.id,
    )
}

Skip consumer data collection

If your application already has all four consumer fields, set skipConsumerDataCollection to true to begin with OTP verification:

val config = AeropayButtonComponentConfig().apply {
    skipConsumerDataCollection = true
    onUserVerificationSuccess = { user ->
        saveAeropayUserId(user.id)
    }
    onPreAuthorisation = { true }
}

The SDK skips the screen only when onGetShopper returns valid, non-empty firstName, lastName, email, and phoneNumber values, and consumerDataCollectionConfig.editableFields is omitted, null, or empty.

Returning-shopper flow

For a returning shopper, get their stored Aeropay user ID from your backend and pass it to the component:

val config = AeropayButtonComponentConfig().apply {
    userId = aeropayProfile.userId
    onPreAuthorisation = { true }
    onPostAuthorisation = { result ->
        verifyPaymentOnBackend(result)
    }
    onError = { error ->
        when (error.errorCode) {
            "SDK1307", "SDK1308" -> offerNewShopperFlow()
            "SDK0500", "SDK0000" -> showPaymentError("Unable to look up Aeropay user.")
            else -> showPaymentError("Unable to continue with Aeropay.")
        }
    }
}

The SDK validates that the Aeropay user exists and is active. It then skips consumer data collection and OTP verification, and opens bank selection.

If the user isn't active, onError receives SDK1307. If the get-user API returns a recognised failure response, onError receives SDK1308. Network or transport failures on the same call use SDK0500 (or SDK0000), not SDK1308. See Troubleshooting — Returning-shopper issues. Remove an invalid stored ID and let the shopper restart the new-shopper flow.

Associate each Aeropay user ID with the authenticated customer on your backend. Retrieve it from your backend rather than device storage alone, and don't accept a userId supplied or changed by untrusted client input.

The SDK and provider handle bank linking. Don't collect routing numbers or full account numbers in your checkout. If you store bank metadata for a business need, restrict access and retain only the required values.

Payout flow

Use AeropayIntentType.Payout in the SDK and Payout in the session request:

val transactionData = TransactionData(
    amount = 25.00,
    currency = "USD",
    entryType = EntryType.Ecom,
    intent = TransactionIntentData(aeropay = AeropayIntentType.Payout),
    merchant = "your-merchant-id",
    merchantTransactionId = sessionResult.merchantTransactionId,
    merchantTransactionDate = { /* ISO 8601 UTC timestamp */ },
)

By default, shoppers can only select an existing linked bank account during a payout. To allow bank linking, set allowLinkBankOnPayout to true:

import com.pxp.checkout.components.aeropaybutton.types.BankSelectionConfig

val config = AeropayButtonComponentConfig().apply {
    userId = aeropayProfile.userId
    bankSelectionConfig = BankSelectionConfig(
        allowLinkBankOnPayout = true,
    )
    onPreAuthorisation = { true }
    onPostAuthorisation = { result ->
        verifyPayoutOnBackend(result)
    }
}

Before enabling payouts, confirm that your PXP and Aeropay accounts support them. Validate customer eligibility, amount limits, account status, and duplicate requests on your backend.

Control the component

Set disabled on the config before you create the component if the button should start disabled. After creation, use setDisabled() so the button state updates:

val config = AeropayButtonComponentConfig().apply {
    disabled = true
}

val aeropayButton = checkout.createComponent(
    ComponentType.AERO_PAY_BUTTON,
    config,
)

aeropayButton.setDisabled(false)
aeropayButton.setDisabled(true)

Changing config.disabled after creation doesn't update the rendered button on its own. The button is also non-interactive while loading. The component manages the loading overlay during API calls and popup operations.

Complete Compose example

The following example creates a new-shopper Authorisation flow:

import android.content.Context
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pxp.checkout.components.aeropaybutton.AeropayButtonComponent
import com.pxp.checkout.components.aeropaybutton.types.AeropayButtonComponentConfig
import com.pxp.checkout.exceptions.BaseSdkException
import com.pxp.checkout.models.FailedSubmitResult
import com.pxp.checkout.types.ComponentType

@Composable
fun AeropayPaymentScreen(context: Context) {
    var aeropayComponent by remember { mutableStateOf<AeropayButtonComponent?>(null) }
    var isLoading by remember { mutableStateOf(true) }

    LaunchedEffect(Unit) {
        try {
            val checkout = createPxpCheckout(context, fetchSessionFromBackend())
            val config = AeropayButtonComponentConfig(label = "Pay by bank").apply {
                onCustomValidation = { true }
                onUserVerificationSuccess = { user ->
                    saveAeropayUserId(user.id)
                }
                onCancel = {
                    resetCheckoutState()
                }
                onPreAuthorisation = {
                    validateOrderOnBackend() // must return Boolean
                }
                onPostAuthorisation = { result ->
                    verifyPaymentOnBackend(result)
                }
                onSubmitError = { error ->
                    if (error is FailedSubmitResult) {
                        showPaymentError("Unable to complete the transaction.")
                    }
                }
                onError = { error ->
                    showPaymentError("Unable to continue with Aeropay.")
                }
            }
            aeropayComponent = checkout.createComponent(
                ComponentType.AERO_PAY_BUTTON,
                config,
            )
        } catch (error: BaseSdkException) {
            // Log error.errorCode in restricted diagnostics (e.g. SDK0113–SDK0116).
            showPaymentError("Pay by Bank is not available for this checkout.")
        } finally {
            isLoading = false
        }
    }

    if (isLoading) {
        CircularProgressIndicator(modifier = Modifier.padding(16.dp))
    } else {
        aeropayComponent?.Content(modifier = Modifier.fillMaxWidth())
    }
}