Accept card payments with 3D Secure authentication, automatic validation, and saved cards for returning customers.
Card payments are automatically included in the drop-in when enabled in your session. The drop-in handles all card field rendering, validation, 3D Secure authentication, and payment processing automatically.
Card payments in Drop-in give you:
- Card fields appear automatically in the drop-in when cards are enabled in your session configuration.
- The drop-in handles all card setup, so you don't need card-specific code.
- Card payments use the same
onSuccessandonErrorcallbacks as other payment methods for a unified integration. - Automatic field validation runs for card number, expiry date, and CVC.
- 3D Secure authentication is handled automatically with native iOS UI.
- Returning customers can use saved cards when card-on-file is enabled.
- PCI DSS Level 1 compliance applies, with no card data stored on your server.
When a customer pays with a card:
- The customer taps "Card" in the payment options list. The card form appears.
- The customer enters their card number, expiry date, and CVC.
- The drop-in validates the card details in real time.
- The customer taps "Pay" and 3D Secure authentication begins (if required).
- The customer completes authentication in the native iOS UI.
- The payment is processed through Unity.
- Your
onSuccesscallback fires.
Use methodConfig.card to control which card sections appear in Drop-in, and methodConfig.global for card networks and consent. For Apple Pay funding and issuer-country settings that live on DropInGlobalConfig, see the notes below: they don't filter manual new-card entry. For manual card funding-source filtering (including .prepaid) and owner-type filtering, use CheckoutDropInConfig.restrictions with Restrictions.Card, or session-level card restrictions. Issuer-country filtering is available for Apple Pay via methodConfig.global.allowedIssuerCountryCodes only.
The following properties are available through DropInCardConfig:
| Property | Description |
|---|---|
showCOFBool? | Controls whether the card-on-file (saved cards) section and its submit button are created. Defaults to true when omitted. When true, card-on-file is created only if the session allows cards and onGetShopper returns a shopper with a non-empty id. |
showNewCardBool? | Controls whether the new card form and related billing address, store-card consent, and new-card submit button are created. Defaults to true when omitted. |
Both flags are optional. Omitted values resolve to true, so both sections are shown unless you opt out. The Card payment method panel is visible when either resolved flag is true (showCOF || showNewCard). When both are false, Drop-in omits the entire Card panel and doesn't create card components.
These flags control creation and rendering of card UI inside Drop-in. They don't change session funding eligibility. Keep cards enabled in the session when you expect the Card payment method to appear.
The following properties in DropInGlobalConfig also apply to card payments:
| Property | Description |
|---|---|
acceptedCardNetworks[DropInCardNetworks]? | Which card brands to accept through card payments. Falls back to session configuration if not specified. Possible values:
|
onGetConsent(DropInPaymentMethod) -> Bool | For cards, onGetConsent(.card) provides the consent value used during new card tokenisation. Drop-in uses this value during new card submission before tokenisation. Saved card (COF) submit doesn't invoke this callback. The SDK renders the card consent checkbox only when showNewCard is true, a shopper ID is available, and the Unity site configuration requires asking for stored-card consent (storeCardConsent == Ask). See Events — onGetConsent. |
In Drop-in, methodConfig.global.allowedCardFundingSource and allowedIssuerCountryCodes apply to Apple Pay only. allowedCardFundingSource maps to Apple Pay merchant capabilities and defaults to [.credit, .debit] when omitted. allowedIssuerCountryCodes maps to Apple Pay supportedCountries. Neither filters manual new-card entry. For manual card funding-source filtering (including .prepaid) and owner-type filtering, use CheckoutDropInConfig.restrictions with Restrictions.Card, or session-level card restrictions. Issuer-country filtering is available for Apple Pay via methodConfig.global.allowedIssuerCountryCodes only.
This example shows a full card configuration with display options and global settings that apply to cards:
methodConfig: DropInMethodConfig(
// Global settings that apply to card payments
global: DropInGlobalConfig(
// Restrict to specific card networks
acceptedCardNetworks: [
.visa,
.mastercard,
.amex
],
// Provide consent value for new card tokenisation (not COF submit)
onGetConsent: { paymentMethod in
return paymentMethod == .card
}
),
// Show both card-on-file and new card entry
card: DropInCardConfig(
showCOF: true,
showNewCard: true
)
)Omit card, or leave both properties unset, to show card-on-file and new card entry by default:
methodConfig: DropInMethodConfig(
// card omitted — both COF and new card are shown
)Show saved cards only for returning shoppers:
methodConfig: DropInMethodConfig(
card: DropInCardConfig(
showCOF: true,
showNewCard: false
)
),
onGetShopper: {
TransactionShopper(id: "shopper-123") // shopper.id required for COF
}Show new card entry only, for example when the shopper is anonymous:
methodConfig: DropInMethodConfig(
card: DropInCardConfig(
showCOF: false,
showNewCard: true
)
)Hide the entire Card panel, for example in a wallet-only checkout:
methodConfig: DropInMethodConfig(
card: DropInCardConfig(
showCOF: false,
showNewCard: false
)
)Card payments require the following to function correctly:
- iOS compatibility: iOS 14.0 or higher.
- HTTPS: Your backend endpoints must be served over HTTPS.
- Unity Portal configuration: Cards must be enabled and configured in the Unity Portal.
- Entry type: Cards support
.ecomand.motoentry types. - 3D Secure: Your merchant account must be configured for 3D Secure authentication.
Card-on-file payments require the shopper to have previously authorised your merchant account. Authorisations must be captured within the time window specified by your payment scheme (typically 7-30 days).
Card payments work through the standard implementation, with no card-specific code needed:
import SwiftUI
import PXPCheckoutSDK
struct CheckoutView: View {
@StateObject private var viewModel = CheckoutViewModel()
var body: some View {
Group {
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
} else if let dropIn = viewModel.dropIn {
dropIn.buildContent()
} else {
ProgressView("Loading checkout...")
}
}
.task {
await viewModel.loadDropIn()
}
}
}
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
private var isCreatingDropIn = false
private var createFailed = false
func loadDropIn() async {
// Fetch session from backend
guard let sessionData = await fetchSessionFromBackend() else {
errorMessage = "Failed to create session"
return
}
isCreatingDropIn = true
createFailed = false
dropIn = nil
// Initialise Drop-in
let config = CheckoutDropInConfig(
environment: .test,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1",
onGetShopper: {
// Provide shopper ID for vaulting
TransactionShopper(id: "shopper-123")
},
onSuccess: { result in
print("Card payment successful!")
print("System transaction ID: \(result.systemTransactionId)")
print("Payment method: \(result.paymentMethod.rawValue)")
// CRITICAL: Verify on backend
Task {
await verifyPaymentOnBackend(result)
}
},
onError: { paymentMethod, error in
if isCreatingDropIn {
createFailed = true
}
print("Card payment failed: \(error.errorMessage)")
Task { @MainActor in
self.errorMessage = "Payment failed: \(error.errorMessage)"
}
}
)
do {
let instance = try CheckoutDropIn(config: config)
await instance.create()
isCreatingDropIn = false
if !createFailed {
dropIn = instance
}
} catch let error as BaseSdkException {
isCreatingDropIn = false
errorMessage = error.errorMessage
} catch {
isCreatingDropIn = false
errorMessage = error.localizedDescription
}
}
private func fetchSessionFromBackend() async -> SessionData? {
// Implementation to fetch session from your backend
// ...
}
private func verifyPaymentOnBackend(_ result: DropInSubmitResult) async {
// Implementation to verify payment
// ...
}
}Enable cards in your session request with a card intent. The Sessions API returns allowedFundingTypes in the response. Map that response into SessionData on the client. Don't put allowedFundingTypes in the create-session request body.
// BACKEND: Create a session with cards enabled
const sessionRequest = {
merchant: "MERCHANT-1",
site: "SITE-1",
sessionTimeout: 120,
merchantTransactionId: crypto.randomUUID(),
transactionMethod: {
intent: {
card: "Authorisation" // or "Purchase"
}
},
amounts: {
currencyCode: "GBP",
transactionValue: 99.99
},
allowTransaction: true,
serviceType: "CheckoutDropIn"
};The session response must include allowedFundingTypes.cards (it may be an empty array). cardSchemes and cards are also used to resolve accepted networks when the card panel is shown. Map the full allowedFundingTypes object into SessionData on the client. See Implementation for the full session request and response mapping.
Drop-in supports these common e-commerce card flows through DropInTransactionIntentData.card. Configure the intent with CardIntentType:
CardIntentType also includes .verification, .estimatedAuthorisation, and .payout. Use those only when your Unity Portal setup and backend support the corresponding card flow. See the session intent table in Implementation.
When a card payment succeeds, your onSuccess callback receives the same standard result as other payment methods:
onSuccess: { result in
print("Payment details:")
print("- System transaction ID: \(result.systemTransactionId)")
print("- Merchant transaction ID: \(result.merchantTransactionId ?? "N/A")")
print("- Payment method: \(result.paymentMethod.rawValue)") // "Card"
// Note: Amount, currency, card details must be retrieved from backend
// 3D Secure authentication data is handled internally
}Handle card-specific errors:
onError: { paymentMethod, error in
print("Error code: \(error.errorCode)")
print("Error message: \(error.errorMessage)")
// Handle specific error codes
let userMessage: String
switch error.errorCode {
case "SDK1114":
userMessage = "Authentication failed. Please try again."
case "SDK1115", "SDK1116":
userMessage = "Card payment failed. Please try another card."
default:
// Fall back to message-based detection
if error.errorMessage.localizedCaseInsensitiveContains("declined") {
userMessage = "Card declined. Please try a different card."
} else if error.errorMessage.localizedCaseInsensitiveContains("insufficient") {
userMessage = "Insufficient funds. Please use a different card."
} else if error.errorMessage.localizedCaseInsensitiveContains("expired") {
userMessage = "Card expired. Please use a different card."
} else if error.errorMessage.localizedCaseInsensitiveContains("invalid") {
userMessage = "Invalid card details. Please check and try again."
} else if error.errorMessage.localizedCaseInsensitiveContains("3DS") ||
error.errorMessage.localizedCaseInsensitiveContains("authentication") {
userMessage = "Authentication failed. Please try again."
} else {
userMessage = "Payment failed: \(error.errorMessage)"
}
}
Task { @MainActor in
showError(userMessage)
}
}The following table describes common card error scenarios:
| Scenario | How to detect | Recommended action |
|---|---|---|
| Update allow transaction failed | error.errorCode == "SDK1113" | Retry the payment or contact support. |
| Authentication failed | error.errorCode == "SDK1114" or message contains "authentication" | Suggest trying again or contacting the bank. |
| Authorisation failed | error.errorCode == "SDK1115" | Suggest trying a different card. |
| Card payment failed | error.errorCode == "SDK1116" | Suggest trying a different card. |
| Card declined | error.errorMessage contains "declined" | Suggest trying a different card. |
| Insufficient funds | error.errorMessage contains "insufficient" | Suggest using a different card or payment method. |
| Card expired | error.errorMessage contains "expired" | Request valid card details. |
| Invalid card | error.errorMessage contains "invalid" | Ask customer to check card details. |
| Network error | error.errorMessage contains "network" or "timeout" | Retry the payment after a brief delay. |
Card errors include both error codes (SDK1114, SDK1115, SDK1116) and descriptive messages. Use error.errorCode for programmatic handling and error.errorMessage for additional context. For production apps, implement robust error handling with retry logic and user-friendly messaging.
Always verify card payments on your backend to ensure payment success before fulfilling orders:
onSuccess: { result in
// Send to backend for verification
Task {
do {
let response = try await apiClient.post("/api/verify-payment", body: [
"systemTransactionId": result.systemTransactionId,
"merchantTransactionId": result.merchantTransactionId ?? ""
])
if response.success {
// Navigate to success screen
await MainActor.run {
navigateToSuccess(orderId: response.orderId)
}
} else {
await MainActor.run {
showError("Payment verification failed")
}
}
} catch {
print("Verification error: \(error.localizedDescription)")
await MainActor.run {
showError("Failed to verify payment")
}
}
}
}Use the following backend code to verify card transactions via the PXP API:
// BACKEND: Verify card payment
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
try {
// Query the PXP API to get transaction details
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader, requestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
}
}
).then(r => r.json());
// Verify transaction state
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not successful' });
}
// Verify merchant transaction ID matches
if (transaction.merchantTransactionId !== merchantTransactionId) {
return res.json({ success: false, error: 'Transaction ID mismatch' });
}
// Verify amount matches expected amount from your order records
const order = await getOrderByMerchantTransactionId(merchantTransactionId);
const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
if (Math.abs(txnAmount - order.amount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// Verify funding type is card
const fundingType = transaction.fundingData?.fundingType ||
transaction.fundingType ||
'Unknown';
if (fundingType !== 'Card') {
return res.json({ success: false, error: 'Invalid funding type' });
}
// Fulfill order
const orderId = await fulfillOrder(transaction);
return res.json({ success: true, orderId });
} catch (error) {
console.error('Verification error:', error);
return res.json({ success: false, error: 'Verification failed' });
}
});Card vaulting allows returning customers to pay with a saved card. When enabled, customers who have previously saved a card can use it for faster checkout.
Vaulting follows this sequence:
- The customer pays with a card and agrees to save it.
- Unity vaults the card and returns a vault ID.
- On return visit, your backend creates a session with optional
customerProfileId, andonGetShopperprovides the shopper ID. - Drop-in displays the saved card for payment.
- The customer completes the payment with the saved card (CVC or expiry date may be required depending on your site configuration).
Card-on-file requires all of the following:
- The session allows cards.
showCOFresolves totrue(the default when omitted).onGetShopperreturns a shopper with a non-emptyid.merchantShopperIdonCheckoutDropInConfigis set at initialisation (required separately from theidreturned byonGetShopper).
Optional customerProfileId on the session request links the Unity session to a Customer Profile. It doesn't replace merchantShopperId or onGetShopper for card-on-file. See Implementation — Customer Profile.
Implement onGetShopper and keep card-on-file enabled:
import SwiftUI
import PXPCheckoutSDK
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var dropIn: CheckoutDropIn?
@Published var errorMessage: String?
private var isCreatingDropIn = false
private var createFailed = false
func loadDropIn() async {
// Fetch session from backend
guard let sessionData = await fetchSessionFromBackend() else {
errorMessage = "Failed to create session"
return
}
isCreatingDropIn = true
createFailed = false
dropIn = nil
// Initialise Drop-in
let config = CheckoutDropInConfig(
environment: .test,
session: sessionData,
transactionData: DropInTransactionData(
amount: Decimal(string: "99.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .purchase
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() }
),
merchantShopperId: "shopper-123",
ownerId: "MERCHANT-1",
methodConfig: DropInMethodConfig(
card: DropInCardConfig(
showCOF: true,
showNewCard: true
)
),
// REQUIRED: Provide shopper ID to enable card vaulting
onGetShopper: {
let user = getCurrentUser()
return TransactionShopper(id: user.shopperId) // e.g., TransactionShopper(id: "shopper-123")
},
onSuccess: { result in
Task {
await verifyPaymentOnBackend(result)
await MainActor.run {
navigateToSuccess()
}
}
},
onError: { paymentMethod, error in
if isCreatingDropIn {
createFailed = true
}
print("Card payment failed: \(error.errorMessage)")
Task { @MainActor in
self.errorMessage = "Payment failed: \(error.errorMessage)"
}
}
)
do {
let instance = try CheckoutDropIn(config: config)
await instance.create()
isCreatingDropIn = false
if !createFailed {
dropIn = instance
}
} catch let error as BaseSdkException {
isCreatingDropIn = false
errorMessage = error.errorMessage
} catch {
isCreatingDropIn = false
errorMessage = error.localizedDescription
}
}
}When showCOF is enabled and onGetShopper returns a shopper ID, the SDK automatically:
- Fetches saved cards from the PXP API.
- Displays saved cards for faster checkout.
- Handles vault setup during the first payment.
To hide saved cards while still allowing new card entry, set showCOF to false.
Card vaulting is enabled by setting merchantShopperId, implementing onGetShopper, keeping showCOF enabled, and optionally controlling new card consent with methodConfig.global.onGetConsent (onGetConsent(.card) does not run on COF submit):
CheckoutDropInConfig(
// ... other config
merchantShopperId: "shopper-123", // Required at initialisation
ownerId: "MERCHANT-1",
onGetShopper: {
let user = getCurrentUser()
return TransactionShopper(id: user.shopperId) // Required for vaulting (non-empty id)
},
methodConfig: DropInMethodConfig(
global: DropInGlobalConfig(
onGetConsent: { paymentMethod in
// Consent for new card tokenisation only
return paymentMethod == .card
}
),
card: DropInCardConfig(
showCOF: true,
showNewCard: true
)
)
)3D Secure authentication is handled automatically by the drop-in. When a card requires 3D Secure, the drop-in:
- Detects that 3D Secure is required from the payment response.
- Launches the native iOS 3D Secure UI.
- Guides the customer through authentication (OTP, biometric, etc.).
- Processes the authenticated payment.
- Fires your
onSuccesscallback on completion.
No additional code is required for 3D Secure authentication. The drop-in manages the entire flow automatically.
The 3D Secure UI is rendered using native iOS components and adapts to the authentication method required by the card issuer (OTP, biometric, challenge questions, etc.). The UI is fully PCI DSS Level 1 compliant.
For subscriptions and recurring charges, use the .authorisation flow with card vaulting enabled and the recurring transaction field:
transactionData: DropInTransactionData(
amount: Decimal(string: "9.99") ?? 0,
currency: "GBP",
entryType: .ecom,
intent: DropInTransactionIntentData(
card: .authorisation // Use Authorisation for recurring
),
merchantTransactionId: UUID().uuidString,
merchantTransactionDate: { Date() },
recurring: RecurringType(
frequencyInDays: 30,
frequencyExpiration: "2026-12-31"
)
)After the first payment, use the vaulted card token to process subsequent recurring payments via the PXP API without customer interaction.