Skip to content

Quickstart

Follow our walkthrough to get Checkout Drop-in running in minutes.

Pre-requisites

Before you start, make sure you have:

  • Xcode 15.0 or higher installed on your computer
  • iOS 14.0 or higher as your deployment target
  • Swift 5.9 or higher
  • Your API credentials from the Unity Portal

Add the SDK dependency

To get started, add the iOS SDK to your project using Swift Package Manager.

In Xcode:

  1. Go to File > Add Package Dependencies.
  2. Enter the package URL: https://github.com/PXP-IO/ios-components-sdk.
  3. Choose Up to Next Major Version from the latest release on ios-components-sdk, then click Add Package.

Alternatively, add it to your Package.swift file. Prefer the Xcode flow above, or check ios-components-sdk releases for the current tag and use that as the floor:

dependencies: [
    .package(
        url: "https://github.com/PXP-IO/ios-components-sdk.git",
        .upToNextMajor(from: "{latestReleaseVersion}") // use current latest release as the floor
    )
]

Create a session on your backend

Drop-in needs a session from the PXP API. This must happen on your backend using HMAC authentication.

nodejs
python

Store your credentials securely

Set up your API credentials as environment variables. Never hardcode them in your application.

Create the HMAC signature function

This function generates a secure authentication hash by combining the timestamp, request ID, request path, and request body (no separators), then hashing with your token value using HMAC SHA256. Put the Token ID in the Authorization header (PXP-UST1 $TOKEN_ID:$timestamp:$hmac), not in the HMAC message body. Use Unix time in seconds for the timestamp.

Build the session request body

Create a request with your merchant details and transaction information. The request body must be minified (no whitespace) for the HMAC signature. Set serviceType to "CheckoutDropIn" for Drop-in integrations. For returning shoppers, include optional customerProfileId (omit it for guest checkout). See Implementation — Customer Profile.

Send the session creation request

POST to https://api-services.pxp.io/api/v1/sessions with your authentication headers and request body.

Return the session data to your app

The API returns the complete session object with sessionId, hmacKey, encryptionKey, allowedFundingTypes, and optional restrictions.

// Node.js backend: Create a session for Checkout Drop-in
const crypto = require('crypto');
const fetch = require('node-fetch');
const express = require('express');

const app = express();

// {% step id="credentials-setup" %}
// Store your credentials securely
const CLIENT_ID = process.env.PXP_CLIENT_ID;
const TOKEN_ID = process.env.PXP_TOKEN_ID;
const TOKEN_VALUE = process.env.PXP_TOKEN_VALUE;
// {% /step %}

// {% step id="create-signature-function" %}
// Create the HMAC signature function
// Message = timestamp + requestId + requestPath + requestBody (no separators).
// Put Token ID in the Authorization header only — not in the HMAC message body.
function createHmacSignature(timestamp, requestId, requestPath, requestBody) {
  const message = `${timestamp}${requestId}${requestPath}${requestBody}`;
  const hmac = crypto.createHmac('sha256', TOKEN_VALUE);
  hmac.update(message);
  const signature = hmac.digest('hex').toUpperCase();
  return `PXP-UST1 ${TOKEN_ID}:${timestamp}:${signature}`;
}
// {% /step %}

async function createSession() {
  // Unix seconds — matches web sample Session auth and Android create-session
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const requestId = crypto.randomUUID();
  const requestPath = 'api/v1/sessions';

  // {% step id="build-request-body" %}
  // Build the session request body (minified JSON for the HMAC signature)
  const requestBody = JSON.stringify({
    merchant: 'MERCHANT-1',
    site: 'SITE-1',
    sessionTimeout: 120,
    merchantTransactionId: crypto.randomUUID(),
    transactionMethod: {
      intent: {
        card: 'Authorisation',
        paypal: 'Purchase',
        aeropay: 'Authorisation'
      }
    },
    amounts: {
      currencyCode: 'USD',
      transactionValue: 49.99
    },
    allowTransaction: true,
    serviceType: 'CheckoutDropIn',
    // Include when the shopper is known. Omit for guest checkout.
    customerProfileId: 'your-customer-profile-id'
  });
  // {% /step %}

  // {% step id="send-session-request" %}
  // Send the session creation request
  const authHeader = createHmacSignature(timestamp, requestId, requestPath, requestBody);

  const response = await fetch('https://api-services.pxp.io/api/v1/sessions', {
    method: 'POST',
    headers: {
      'X-Client-Id': CLIENT_ID,
      'X-Request-Id': requestId,
      'Authorization': authHeader,
      'Content-Type': 'application/json'
    },
    body: requestBody
  });
  // {% /step %}

  // {% step id="handle-response" %}
  // Return the session data to your app
  const sessionData = await response.json();
  return sessionData;
  // {% /step %}
}

// Express.js endpoint example
app.get('/api/create-session', async (req, res) => {
  try {
    const sessionData = await createSession();
    res.json(sessionData);
  } catch (error) {
    console.error('Session creation failed:', error);
    res.status(500).json({ error: 'Failed to create session' });
  }
});

Initialise Drop-in in your app

The following example shows a complete minimal SwiftUI integration using CheckoutDropInConfig, DropInTransactionData, CheckoutDropIn(config:), await create(), and buildContent().

Import the required dependencies

Import SwiftUI and PXPCheckoutSDK from the iOS SDK.

Set up your SwiftUI view

Create a SwiftUI view that will host the Checkout Drop-in interface using the @StateObject property wrapper for the view model.

Fetch the session data from your backend

Call your backend endpoint to get the session data you created in the previous steps. Decode the response directly into a SessionData object using JSONDecoder().decode(SessionData.self, from:). The SDK's SessionData struct includes:

public struct SessionData: Codable {
    public let sessionId: String
    public let hmacKey: String
    public let encryptionKey: String
    public let sessionExpiry: String?
    public let allowedFundingTypes: AllowedFundingType?
    public let restrictions: Restrictions?
}

allowedFundingTypes controls which payment methods Drop-in can render (cards, PayPal, Apple Pay, and Aeropay when enabled in your session). restrictions carries card-level filtering rules. Pass the entire decoded SessionData object into CheckoutDropInConfig(session:). Don't map only selected fields.

Configure the transaction data

Specify the transaction details using DropInTransactionData. The amount, currency, entry type, merchant transaction ID, and merchant transaction date fields are required. Within intent, properties are optional — set entries only for methods you expect Drop-in to render (for example card when cards are enabled, aeropay when Aeropay is in the session):

DropInTransactionData(
    amount: Decimal(string: "99.99") ?? 0,
    currency: "USD",
    entryType: .ecom,
    intent: DropInTransactionIntentData(
        card: .authorisation,
        paypal: .purchase,
        aeropay: .authorisation
    ),
    merchantTransactionId: "test-txn-123",
    merchantTransactionDate: { Date() }
)
  • amount: the transaction amount as a Decimal
  • currency: a three-letter currency code (for example "USD", "GBP"). Aeropay requires "USD".
  • entryType: the entry type (use .ecom for Drop-in wallet flows such as PayPal and for Aeropay; use .moto for mail/telephone order card flows)
  • intent: the payment intent for each method you enable:
    • card: CardIntentType (for example .authorisation, .purchase, .verification, .estimatedAuthorisation, .payout)
    • paypal: DropInPayPalIntentType — .authorisation or .purchase (payout not available in Drop-in)
    • aeropay: DropInAeropayIntentType — .authorisation, .purchase, or .estimatedAuthorisation (payout not available in Drop-in)
  • merchantTransactionId: your unique transaction identifier
  • merchantTransactionDate: closure returning the transaction date (typically { Date() })

Initialise Checkout Drop-in

Configure Drop-in with your environment, session data, transaction details, merchant shopper ID, and owner ID (merchant group ID). The CheckoutDropIn(config:) initialiser can throw (for example empty sessionId or hmacKey):

let dropIn = try CheckoutDropIn(config: dropInConfig)

Handle configuration failures with catch let error as BaseSdkException. Runtime failures during create() (render, eligibility) surface via onError, not as throws from await create().

Provide shopper information (optional)

Implement the onGetShopper callback when you need card-on-file or shopper-scoped flows. merchantShopperId on CheckoutDropInConfig is always required. This callback returns the matching TransactionShopper when Drop-in needs it (including at the start of create()). This is separate from optional customerProfileId on the session request. See Implementation — Customer Profile.

Card-on-file UI is created only when onGetShopper returns TransactionShopper(id:) with a non-empty id. Card-on-file also requires showCOF to resolve to true on DropInCardConfig (the default when omitted). Use methodConfig.card to show or hide card-on-file and new card entry. See Cards.

Callback signature:

onGetShopper: (() async -> TransactionShopper?)?

onGetShopper is asynchronous because shopper data may come from merchant storage or backend:

onGetShopper: {
    TransactionShopper(id: "shopper-123")
}

Handle successful payments

Implement the onSuccess callback to handle successful payments. Always verify payments on your backend before fulfilling orders. Frontend callbacks can be manipulated.

Callback signature:

onSuccess: ((DropInSubmitResult) -> Void)?

onSuccess is synchronous and receives DropInSubmitResult with transaction identifiers and the payment method:

onSuccess: { result in
    let systemTransactionId = result.systemTransactionId
    let merchantTransactionId = result.merchantTransactionId // String?
    let paymentMethod = result.paymentMethod
    // paymentData is typically nil for card, PayPal, Apple Pay, and Aeropay

    // Verify the payment on your backend before fulfilling the order.
}

If your app needs to update UI from a non-main context, wrap that update in Task { @MainActor in ... }.

Handle payment errors

Implement the onError callback to handle payment failures and display appropriate error messages.

Callback signature:

onError: ((DropInPaymentMethod?, BaseSdkException) -> Void)?

onError is synchronous. paymentMethod can be nil for initialisation or configuration errors before a payment method is selected. CheckoutViewModel is @MainActor, so assign errorMessage directly:

onError: { paymentMethod, error in
    print("Payment failed: \(error.errorMessage)")
    self.errorMessage = error.errorMessage
}

If you need to update UI from a non-main context, wrap that update in Task { @MainActor in ... }.

For Apple Pay integrations that need dynamic shipping, payment method, or coupon handling, use the optional callbacks in DropInApplePayConfig. See the implementation guide and events guide for full examples.

PayPal doesn't invoke onSubmit.

Create the Drop-in component

Call the create() method from an async task to initialise the Drop-in component. If onError runs during create(), leave dropIn unset so the "Unable to load checkout" block isn't shown above an empty buildContent().

At the start of loadDropIn():

errorMessage = nil
dropIn = nil

After await instance.create():

await instance.create()
if errorMessage == nil {
    dropIn = instance
}

Handle configuration throws from CheckoutDropIn(config:) as follows:

} catch let error as BaseSdkException {
    errorMessage = error.errorMessage
} catch {
    errorMessage = error.localizedDescription
}

Render the Drop-in content

Call the buildContent() method to display the payment interface in your SwiftUI view.

// {% step id="import-dependencies" %}
import SwiftUI
import PXPCheckoutSDK
// {% /step %}

// {% step id="setup-view" %}
struct CheckoutView: View {
    @StateObject private var viewModel = CheckoutViewModel()
    
    var body: some View {
        Group {
            if let dropIn = viewModel.dropIn {
                // {% step id="render-content" %}
                dropIn.buildContent()
                // {% /step %}
            } else if viewModel.errorMessage == nil {
                ProgressView("Loading checkout...")
            }

            if let error = viewModel.errorMessage {
                VStack(spacing: 16) {
                    Text("Unable to load checkout")
                        .font(.headline)
                    Text(error)
                        .font(.body)
                        .foregroundColor(.secondary)
                    Button("Retry") {
                        Task {
                            await viewModel.loadDropIn()
                        }
                    }
                }
                .padding()
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}
// {% /step %}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    
    func loadDropIn() async {
        errorMessage = nil
        dropIn = nil
        do {
            // {% step id="fetch-session" %}
            let url = URL(string: "https://your-backend.com/api/create-session")!
            let (data, _) = try await URLSession.shared.data(from: url)
            
            // Decode the complete SessionData response — pass all fields to Drop-in
            let session = try JSONDecoder().decode(SessionData.self, from: data)
            // {% /step %}
            
            // {% step id="configure-transaction" %}
            let transactionData = DropInTransactionData(
                amount: Decimal(string: "49.99") ?? 0,
                currency: "USD",
                entryType: .ecom,
                intent: DropInTransactionIntentData(
                    card: .authorisation,
                    paypal: .purchase,
                    aeropay: .authorisation
                ),
                merchantTransactionId: UUID().uuidString,
                merchantTransactionDate: { Date() }
            )
            // {% /step %}
            
            // {% step id="initialize-dropin" %}
            let config = CheckoutDropInConfig(
                environment: .test,
                session: session,
                transactionData: transactionData,
                merchantShopperId: "shopper-123",
                ownerId: "your-merchant-group-id",
                // {% step id="setup-shopper" %}
                onGetShopper: {
                    // Card-on-file requires a non-empty id
                    TransactionShopper(id: "shopper-123")
                },
                // {% /step %}
                // {% step id="success-callback" %}
                onSuccess: { result in
                    let systemTransactionId = result.systemTransactionId
                    let merchantTransactionId = result.merchantTransactionId // String?
                    // paymentData is typically nil for card, PayPal, Apple Pay, and Aeropay
                    print("Payment successful: \(systemTransactionId)")
                    Task {
                        await self.verifyPaymentOnBackend(result)
                    }
                },
                // {% /step %}
                // {% step id="error-callback" %}
                onError: { paymentMethod, error in
                    print("Payment failed: \(error.errorMessage)")
                    self.errorMessage = error.errorMessage
                }
                // {% /step %}
            )
            
            let instance = try CheckoutDropIn(config: config)
            // {% /step %}
            
            // {% step id="create-dropin" %}
            await instance.create()
            if errorMessage == nil {
                dropIn = instance
            }
            // {% /step %}
            
        } catch let error as BaseSdkException {
            errorMessage = error.errorMessage
        } catch {
            errorMessage = error.localizedDescription
        }
    }
    
    private func verifyPaymentOnBackend(_ result: DropInSubmitResult) async {
        // Send transaction details to your backend for verification
        do {
            let url = URL(string: "https://your-backend.com/api/verify-payment")!
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = [
                "systemTransactionId": result.systemTransactionId,
                "merchantTransactionId": result.merchantTransactionId ?? ""
            ]
            request.httpBody = try JSONEncoder().encode(body)
            
            let (data, _) = try await URLSession.shared.data(for: request)
            let verification = try JSONDecoder().decode(VerificationResponse.self, from: data)
            
            if verification.success {
                print("Payment verified! Order ID: \(verification.orderId ?? "")")
            }
        } catch {
            print("Verification error: \(error.localizedDescription)")
        }
    }
}

struct VerificationResponse: Codable {
    let success: Bool
    let orderId: String?
    let error: String?
}
nodejs
python

Verify payments

When a payment succeeds, the onSuccess callback fires with transaction details. However, you must always verify the payment on your backend before fulfilling orders.

Configure webhooks in the Unity Portal to receive real-time payment notifications on your backend.

Create the webhook endpoint

Set up an endpoint at /webhooks/pxp to receive payment notifications from Unity.

Verify webhook signature

Verify the webhook HMAC / signature before processing events. Reject unauthenticated requests.

Process webhook events

Loop through the events array and filter for Transaction events.

Check payment state

Verify the transaction state is Authorised or Captured before processing.

Prevent duplicate processing

Check if you've already processed this transaction using systemTransactionId.

Verify transaction details

Match the merchantTransactionId, amount, and currency against your order records.

Fulfil the order

If verification passes, fulfil the order and mark the transaction as processed.

Respond to webhook

Always return { state: 'Success' } to acknowledge receipt, even if processing failed.

You can also verify payments using the Transactions API to query transaction status directly. See the Implementation guide for details.

// Node.js backend: Webhook handler for payment verification
const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.json());

// {% step id="webhook-endpoint" %}
app.post('/webhooks/pxp', async (req, res) => {
  try {
    const events = req.body;

    // {% step id="verify-signature" %}
    // Verify webhook authenticity using HMAC before processing
    const signature = req.headers['x-webhook-signature'];
    const expectedSignature = createWebhookSignature(JSON.stringify(events));

    if (signature !== expectedSignature) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Unauthorized' });
    }
    // {% /step %}

    // {% step id="process-events" %}
    for (const event of events) {
      if (event.eventCategory === 'Transaction') {
        const transaction = event.eventData;

        console.log('Processing transaction:', transaction.systemTransactionId);

        // {% step id="check-state" %}
        if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
          console.log(`Skipping transaction in state: ${transaction.state}`);
          continue;
        }
        // {% /step %}

        // {% step id="prevent-duplicates" %}
        const existingTransaction = await db.transactions.findOne({
          systemTransactionId: transaction.systemTransactionId
        });

        if (existingTransaction) {
          console.log('Transaction already processed, skipping');
          continue;
        }
        // {% /step %}

        // {% step id="verify-details" %}
        const order = await db.orders.findOne({
          merchantTransactionId: transaction.merchantTransactionId
        });

        if (!order) {
          console.error('Order not found:', transaction.merchantTransactionId);
          continue;
        }

        const transactionAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
        if (Math.abs(transactionAmount - order.amount) > 0.01) {
          console.error('Amount mismatch:', {
            expected: order.amount,
            actual: transactionAmount
          });
          continue;
        }

        const transactionCurrency = transaction.amounts?.currencyCode || transaction.currency;
        if (transactionCurrency !== order.currency) {
          console.error('Currency mismatch');
          continue;
        }
        // {% /step %}

        // {% step id="fulfill-order" %}
        await db.transactions.create({
          systemTransactionId: transaction.systemTransactionId,
          merchantTransactionId: transaction.merchantTransactionId,
          amount: transactionAmount,
          currency: transactionCurrency,
          state: transaction.state,
          paymentMethod: transaction.fundingData?.fundingType || 'Unknown',
          processedAt: new Date()
        });

        await fulfillOrder(order.id, transaction.systemTransactionId);

        console.log('Order fulfilled:', order.id);
        // {% /step %}
      }
    }
    // {% /step %}

    // {% step id="respond" %}
    res.json({ state: 'Success' });
    // {% /step %}

  } catch (error) {
    console.error('Webhook processing error:', error);
    // Always return success to prevent retries
    res.json({ state: 'Success' });
  }
});
// {% /step %}

function createWebhookSignature(body) {
  const secret = process.env.WEBHOOK_SECRET;
  return crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
}

async function fulfillOrder(orderId, systemTransactionId) {
  // Your order fulfilment logic here
  // - Update order status
  // - Send confirmation email
  // - Trigger shipping
  // - Update inventory
  console.log(`Fulfilling order ${orderId} for transaction ${systemTransactionId}`);
}

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook handler listening on port ${PORT}`);
});

That's it. You now have a working Checkout Drop-in integration.

What's next?

Now that you have Drop-in running, here are the recommended next steps:

  • Customise the look and feel in the Unity Portal (Checkout Drop-In site settings).
  • Configure card display to show or hide card-on-file and new card entry.
  • Enable Aeropay for pay-by-bank: enable the service in the Unity Portal, create a session with Aeropay funding (externalMerchantId, configurationId), set DropInTransactionIntentData(aeropay:), and use currency: "USD" and entryType: .ecom.
  • Set up backend verification to verify payments before fulfilling orders.
  • Add optional callbacks to enhance the user experience with validation and loading states.