# Implementation

Complete guide to integrating the Aeropay component into your iOS 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 SwiftUI view.
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](/guides/checkout/components/ios/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 iOS app that meets the SDK requirements (iOS 14 or later, and SwiftUI).


Aeropay transactions must also meet these requirements:

| Setting | Required value |
|  --- | --- |
| Currency | `USD` |
| Entry type | `.ecom` |
| Intent | `.authorisation`, `.purchase`, `.estimatedAuthorisation`, or `.payout` |


## Step 1: Install the iOS SDK

Add the PXP Checkout SDK through Swift Package Manager. The public product is `PXPCheckoutSDK`, and the package requires iOS 14 or later.

In Xcode, add the package dependency for your PXP Checkout distribution, then link the `PXPCheckoutSDK` product to your app target.

Import the SDK in your Swift files:

```swift
import PXPCheckoutSDK
```

Register the SDK callback URL scheme so your app can complete Aerosync bank linking. The SDK builds the callback as `{callbackScheme}://aerosync/callback`, where `callbackScheme` comes from the SDK configuration (`PXP_CALLBACK_SCHEME`) and defaults to `pxpcheckout`. Make sure your app can handle that URL, for example `pxpcheckout://aerosync/callback`.

Register the scheme in your app's `Info.plist`. The value must match `PXP_CALLBACK_SCHEME` for your SDK distribution (default `pxpcheckout`):

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>pxpcheckout</string>
        </array>
    </dict>
</array>
```

Without `CFBundleURLTypes`, Aerosync bank linking can't return to the app. For package setup details, see [Install](/guides/checkout/components/ios/install).

## 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 iOS app.

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

```json
{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "sessionTimeout": 120,
  "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:

| Property | Description |
|  --- | --- |
| `merchantTransactionId`string | Your unique identifier for the session transaction. We recommend reusing the value when initialising the SDK to simplify reconciliation. |
| `amounts.currencyCode`string | Transaction currency. Set this to `USD`. |
| `amounts.transactionValue`number | The payment or payout amount. |
| `transactionMethod.intent.aeropay`string | The Aeropay transaction intent. Use `Authorisation`, `Purchase`, `EstimatedAuthorisation`, or `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:

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

The SDK 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 iOS app.

## Step 3: Initialise the SDK

Request the session from your backend, then initialise `PxpCheckout`:

Decode the backend session response into `SessionData`, then pass that session into `CheckoutConfig`. Aeropay funding is already on the decoded session at `allowedFundingTypes.payByBanks.aeropay`.

```swift
import PXPCheckoutSDK

let session = try JSONDecoder().decode(SessionData.self, from: responseData)
```

Use a merchant-defined wrapper if you also pass your own `merchantTransactionId` (e.g., `SessionResult` with `session: SessionData` and `merchantTransactionId: String`):

```swift
func createPxpCheckout(sessionResult: SessionResult) throws -> PxpCheckout {
    let transactionData = TransactionData(
        amount: Decimal(25.00),
        currency: "USD",
        entryType: .ecom,
        intent: TransactionIntentData(aeropay: .authorisation),
        merchantTransactionId: sessionResult.merchantTransactionId,
        merchantTransactionDate: { Date() }
    )

    let checkoutConfig = CheckoutConfig(
        environment: .test,
        session: sessionResult.session,
        transactionData: transactionData,
        merchantShopperId: "shopper-123",
        ownerId: "MERCHANT_GROUP_1",
        onGetShopper: { TransactionShopper(
                id: "shopper-123",
                firstName: "John",
                lastName: "Doe",
                email: "john.doe@example.com",
                phoneNumber: "+14155550123"
            )
        }
    )

    return try PxpCheckout.initialize(config: checkoutConfig)
}
```

Set `environment` to `.test` for UAT or `.live` for production. The SDK maps those values to Aerosync as `sandbox` and `production` respectively.

Keep the session request and SDK transaction data consistent so that you can trace and reconcile transactions. The iOS 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 `pxpCheckout.create(.aeropayButton, componentConfig:)`:

```swift
let config = AeropayButtonComponentConfig(label: "Pay by bank")
config.onClick = {
    clearPaymentErrors()
}
config.onCustomValidation = {
    await validateCheckoutReady()
}
config.onUserVerificationSuccess = { user in
    saveAeropayUserId(user.id)
}
config.onCancel = {
    resetCheckoutState()
}
config.onPreAuthorisation = {
    await validateOrderOnBackend()
}
config.onPostAuthorisation = { result in
    verifyPaymentOnBackend(result)
}
config.onSubmitError = { error in
    if let failed = error as? FailedSubmitResult {
        showPaymentError("Unable to complete the transaction.")
    }
}
config.onError = { error in
    showPaymentError("Unable to continue with Aeropay.")
}

let aeropayButton = try pxpCheckout.create(.aeropayButton, componentConfig: config)
```

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. Return `false` (or omit `onPreAuthorisation`) to abort submission without calling `onSubmitError`, `onCancel`, or `onError` — the bank-selection screen stays open and no transaction is sent.

`onCustomValidation` runs after the optional `onClick` handler on `AeropayButtonComponentConfig` (inherited from `ButtonComponentConfig`) and before the Aeropay popup opens. Return `false` from `onCustomValidation` to block the flow. `onClick` is for synchronous side effects only and can't cancel the flow.

`onCancel` runs when the shopper dismisses the popup, including the close control and swipe dismiss. Successful payment dismissal is programmatic and doesn't invoke `onCancel`.

For all component properties, see [Configuration](/guides/checkout/components/ios/aeropay/configuration). For callback payloads and examples, see [Events](/guides/checkout/components/ios/aeropay/events).

## Step 5: Render the button

Call `buildContent()` in your SwiftUI view:

```swift
import SwiftUI
import PXPCheckoutSDK

struct AeropayCheckoutButton: View {
    let aeropayButton: BaseComponent?

    var body: some View {
        aeropayButton?.buildContent()
            .frame(maxWidth: .infinity)
    }
}
```

Create the component in a `Task` from `.onAppear` or an equivalent lifecycle hook, then store it in `@State` so the view 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:

```swift
config.onPostAuthorisation = { submitResult in
    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, you can find it by `merchantTransactionId` or `systemTransactionId` in *Reporting* in the Unity Portal and select an available action:

* **Capture:** Capture funds for an authorised transaction.
* **Increment:** Increase the authorised amount.
* **Void:** Cancel an authorisation before it's captured.
* **Refund:** Return funds for a captured or purchased transaction.


Available actions depend on the transaction intent and current state. 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`:

```swift
config.onUserVerificationSuccess = { user in
    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:

```swift
let config = AeropayButtonComponentConfig()
config.skipConsumerDataCollection = true
config.onUserVerificationSuccess = { user in
    saveAeropayUserId(user.id)
}
config.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, `nil`, or empty.

## Returning-shopper flow

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

```swift
let config = AeropayButtonComponentConfig()
config.userId = aeropayProfile.userId
config.onPreAuthorisation = { true }
config.onPostAuthorisation = { result in
    verifyPaymentOnBackend(result)
}
config.onError = { error in
    switch error.errorCode {
    case "SDK1307", "SDK1308":
        offerNewShopperFlow()
    default:
        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 the Aerosync bank-selection screen.

If the user isn't active, `onError` receives `SDK1307`. If retrieving the user fails, `onError` receives `SDK1308`. Remove the 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 `.payout` in the SDK and `Payout` in the session request:

```swift
let transactionData = TransactionData(
    amount: Decimal(25.00),
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(aeropay: .payout),
    merchantTransactionId: sessionResult.merchantTransactionId,
    merchantTransactionDate: { Date() }
)
```

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

```swift
let config = AeropayButtonComponentConfig()
config.userId = aeropayProfile.userId
config.bankSelectionConfig = BankSelectionConfig(
    allowLinkBankOnPayout: true
)
config.onPreAuthorisation = { true }
config.onPostAuthorisation = { result in
    verifyPayoutOnBackend(result)
}
```

`config.label` doesn't override the payout button text. For `.payout`, the SDK uses the localised payout label (`en-US`: `Withdraw by Bank`).

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, cast to the button component and call `setDisabled(_:)` so the button state updates:

```swift
let config = AeropayButtonComponentConfig()
config.disabled = true

let component = try pxpCheckout.create(.aeropayButton, componentConfig: config)

if let aeropayButton = component as? AeropayButtonComponent {
    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.

`hide()` and `show()` are available on `AeropayButtonComponent`, not on the `BaseComponent` protocol returned by `create`. Cast before you use them when Aeropay availability depends on your application state:

```swift
if let aeropayButton = component as? AeropayButtonComponent {
    aeropayButton.hide()
    aeropayButton.show()
}
```

## Complete SwiftUI example

The following example creates a new-shopper `.authorisation` flow:

```swift
import SwiftUI
import PXPCheckoutSDK

struct AeropayPaymentView: View {
    @State private var aeropayComponent: BaseComponent?
    @State private var isLoading = true

    var body: some View {
        Group {
            if isLoading {
                ProgressView()
            } else {
                aeropayComponent?.buildContent()
                    .frame(maxWidth: .infinity)
            }
        }
        .padding()
        .onAppear {
            createAeropayComponent()
        }
    }

    private func createAeropayComponent() {
        Task {
            do {
                let pxpCheckout = try createPxpCheckout(sessionResult: await fetchSessionFromBackend())
                let config = AeropayButtonComponentConfig(label: "Pay by bank")
                config.onCustomValidation = { true }
                config.onUserVerificationSuccess = { user in
                    saveAeropayUserId(user.id)
                }
                config.onCancel = {
                    resetCheckoutState()
                }
                config.onPreAuthorisation = {
                    await validateOrderOnBackend()
                }
                config.onPostAuthorisation = { result in
                    verifyPaymentOnBackend(result)
                }
                config.onSubmitError = { error in
                    if error is FailedSubmitResult {
                        showPaymentError("Unable to complete the transaction.")
                    }
                }
                config.onError = { _ in
                    showPaymentError("Unable to continue with Aeropay.")
                }

                let component = try pxpCheckout.create(.aeropayButton, componentConfig: config)
                await MainActor.run {
                    aeropayComponent = component
                    isLoading = false
                }
            } catch {
                await MainActor.run {
                    isLoading = false
                    showPaymentError("Pay by Bank is not available for this checkout.")
                }
            }
        }
    }
}
```