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 14 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. Select the latest version and click Add Package.

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, then hashing with your token value using HMAC SHA256.

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.

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 1: 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 2: Create the HMAC signature function
function createHmacSignature(tokenId, timestamp, requestId, requestPath, requestBody) {
  const tokenValue = TOKEN_VALUE;
  
  // Concatenate the values with colons
  const message = `${tokenId}:${timestamp}:${requestId}:${requestPath}:${requestBody}`;
  
  // Create HMAC SHA256 hash
  const hmac = crypto.createHmac('sha256', tokenValue);
  hmac.update(message);
  const signature = hmac.digest('hex');
  
  return `PXP-WCA1 ${tokenId}:${timestamp}:${signature}`;
}

// Step 3: Build the session request body
async function createSession() {
  const timestamp = Date.now().toString();
  const requestId = crypto.randomUUID();
  const requestPath = 'api/v1/sessions';
  
  const requestBody = JSON.stringify({
    merchant: 'MERCHANT-1',
    site: 'SITE-1',
    sessionTimeout: 120,
    merchantTransactionId: crypto.randomUUID(),
    transactionMethod: {
      intent: {
        card: 'Authorisation',
        paypal: 'Purchase'
      }
    },
    amounts: {
      currencyCode: 'USD',
      transactionValue: 49.99
    },
    allowTransaction: true,
    serviceType: 'CheckoutDropIn'
    // Optional — when Paze is enabled in the Unity Portal, the session response
    // includes allowedFundingTypes.wallets.paze. You may also request it explicitly:
    // allowedFundingTypes: {
    //   wallets: {
    //     paze: {
    //       clientId: 'your-paze-client-id',
    //       merchantCategoryCode: '5812'
    //     }
    //   }
    // }
  });

  // Step 4: Send the session creation request
  const authHeader = createHmacSignature(TOKEN_ID, 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 5: Return the session data to your app
  const sessionData = await response.json();
  return sessionData;
}

// 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, and Apple Pay 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. All of these fields are required:

DropInTransactionData(
    amount: Decimal(string: "99.99") ?? 0,
    currency: "USD",
    entryType: .ecom,
    intent: DropInTransactionIntentData(
        card: .authorisation,
        paypal: .authorisation
    ),
    merchantTransactionId: "test-txn-123",
    merchantTransactionDate: { Date() }
)
  • amount: the transaction amount as a Decimal
  • currency: a three-letter currency code (e.g., "USD", "GBP").
  • entryType: the entry type — use .ecom for PayPal
  • intent: the payment intent for each method (.authorisation or .purchase).
  • 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.

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

Callback signature:

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

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

onGetShopper: { async in
    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
    let paymentMethod = result.paymentMethod
    // paymentData is nil for card, PayPal, and Apple Pay;

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

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

If the surrounding view model is already marked @MainActor, a simpler form works:

onError: { paymentMethod, error in
    self.errorMessage = error.errorMessage
}

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.

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 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()
            } else {
                ProgressView("Loading checkout...")
            }
        }
        .task {
            await viewModel.loadDropIn()
        }
    }
}
// {% /step %}

@MainActor
final class CheckoutViewModel: ObservableObject {
    @Published var dropIn: CheckoutDropIn?
    @Published var errorMessage: String?
    
    func loadDropIn() async {
        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 (including wallets.paze when enabled)
            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: .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: { async in
                    TransactionShopper(id: "shopper-123")
                },
                // {% /step %}
                // {% step id="success-callback" %}
                onSuccess: { result in
                    print("Payment successful: \(result.systemTransactionId)")
                    // paymentData is nil for card, PayPal, and Apple Pay; Paze may return .paze(...)
                    Task {
                        await self.verifyPaymentOnBackend(result)
                    }
                },
                // {% /step %}
                // {% step id="error-callback" %}
                onError: { paymentMethod, error in
                    print("Payment failed: \(error.errorMessage)")
                    Task { @MainActor in
                        self.errorMessage = error.errorMessage
                    }
                }
                // {% /step %}
            )
            
            let instance = try CheckoutDropIn(config: config)
            // {% /step %}
            
            // {% step id="create-dropin" %}
            await instance.create()
            dropIn = instance
            // {% /step %}
            
        } 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 authenticity using HMAC signature validation to prevent fraudulent 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 1: Create the webhook endpoint
app.post('/webhooks/pxp', async (req, res) => {
  try {
    const events = req.body;
    
    // Step 2: Verify webhook signature
    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 3: Process webhook events
    for (const event of events) {
      if (event.eventCategory === 'Transaction') {
        const transaction = event.eventData;
        
        console.log('Processing transaction:', transaction.systemTransactionId);
        
        // Step 4: Check payment state
        if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
          console.log(`Skipping transaction in state: ${transaction.state}`);
          continue;
        }
        
        // Step 5: Prevent duplicate processing
        const existingTransaction = await db.transactions.findOne({
          systemTransactionId: transaction.systemTransactionId
        });
        
        if (existingTransaction) {
          console.log('Transaction already processed, skipping');
          continue;
        }
        
        // Step 6: Verify transaction details
        const order = await db.orders.findOne({
          merchantTransactionId: transaction.merchantTransactionId
        });
        
        if (!order) {
          console.error('Order not found:', transaction.merchantTransactionId);
          continue;
        }
        
        // Verify amount matches
        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;
        }
        
        // Verify currency matches
        const transactionCurrency = transaction.amounts?.currencyCode || transaction.currency;
        if (transactionCurrency !== order.currency) {
          console.error('Currency mismatch');
          continue;
        }
        
        // Step 7: Fulfil the order
        // Mark transaction as processed
        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()
        });
        
        // Fulfil the order
        await fulfillOrder(order.id, transaction.systemTransactionId);
        
        console.log('Order fulfilled:', order.id);
      }
    }
    
    // Step 8: Respond to webhook
    res.json({ state: 'Success' });
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Always return 200 to prevent retries
    res.json({ state: 'Success' });
  }
});

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}`);
}

// Start server
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: