Integrate a frictionless checkout experience.
By implementing non-3DS transactions into your payment flow, you benefit from:
- Streamlined checkout: No additional authentication required, providing faster payment completion.
- Lower transaction friction: Reduced cart abandonment with seamless payment flow.
- Faster processing: Immediate payment completion without authentication delays.
- Better mobile experience: Optimised for mobile checkouts where 3DS challenges can be problematic.
Non-3DS transactions are ideal for low-risk scenarios, trusted customers, or when implementing other fraud prevention measures. However, they may have different liability arrangements and potentially higher processing fees.
The non-3DS flow is made up of six key steps.
The customer taps the submit button or the payment is triggered programmatically. The submitAsync() method in CardSubmitComponent is invoked and validation occurs inside it. If validation passes, the method continues with the payment processing. If it fails, the method exits early and doesn't proceed with the transaction.
If it's a new card, the SDK sends the card details to the tokenisation service. If it's a saved card, the SDK retrieves the existing token.
The SDK evaluates whether 3DS authentication is required. In this flow, 3DS isn't required based on factors such as:
- The transaction intent being
.payout. - Your configuration not requiring 3DS authentication (i.e.,
onPreInitiateAuthenticationcallback not provided). - The transaction falling below risk thresholds.
- Regulatory exemptions being applicable.
Since 3DS isn't required, the flow skips all authentication steps and proceeds directly to authorisation.
This is the final processing step where the SDK sends the authorisation request directly to the payment gateway without any 3DS authentication data. The transaction data is processed using standard card payment protocols.
The authorisation step has two associated callbacks:
onPreAuthorisation: Provides transaction data for final review. This is your last chance to modify the transaction before authorisation. Note that in non-3DS flows, no authentication results are included.onPostAuthorisation: Receives the final transaction result from the payment gateway. The transaction is either approved or declined with standard payment processing details.
Depending on your configuration, the transaction may be automatically captured or require manual capture later.
You receive the final authorisation response from the payment gateway. The transaction is either approved or declined and final transaction details are available. Since this is a non-3DS flow, no authentication confirmation data is included in the response.
Non-3DS transactions use simplified callbacks without authentication complexity:
onPreAuthorisation:((PreAuthorizationData?) async -> TransactionInitiationData?)?- Purpose: Last chance to modify transaction data before authorisation.
- Parameter:
PreAuthorizationData?(optional) containing token identifiers (no 3DS data). - Return:
TransactionInitiationDatawith additional metadata, exemption data, or custom parameters. - Return nil: To cancel the transaction.
- Note: This callback is
asyncas it returns data for the SDK to process.
onPostAuthorisation:((BaseSubmitResult) -> Void)?- Purpose: Receives the final transaction result from the payment gateway.
- Parameter:
BaseSubmitResult(one ofAuthorisedSubmitResult,CapturedSubmitResult,RefusedSubmitResult,FailedSubmitResult). - Contains: Transaction status, provider response, transaction reference, processing details.
- Usage: Handle payment success/failure, navigate to appropriate screens, update order status.
- Note: This callback is NOT
asyncas it only receives results.
onPreTokenisation:(() -> Bool)?- Purpose: Called before card tokenisation begins.
- Return:
trueto proceed with tokenisation,falseto abort. - Usage: Perform final validation, show loading states.
onPostTokenisation:((CardTokenizationResult) -> Void)?- Purpose: Receives tokenisation results.
- Parameter:
CardTokenizationResult(either success with token ID or failure with error). - Usage: Handle tokenisation success/failure, store token references.
onSubmitError:((BaseSdkException) -> Void)?- Purpose: Handles submission errors.
- Parameter:
BaseSdkExceptioncontaining error details and codes. - Usage: Display error messages, implement retry logic, track failures.
To use non-3DS payments in your application:
- Ensure your merchant account supports non-3DS transactions.
- Configure your risk settings appropriately in the Unity Portal.
- Consider implementing additional fraud prevention measures.
To start, set up your CheckoutConfig with the required transaction details.
import PXPCheckoutSDK
let sessionData = SessionData(
sessionId: "your-session-id",
hmacKey: "your-hmac-key",
encryptionKey: "your-encryption-key"
)
let transactionData = TransactionData(
amount: Decimal(string: "99.99")!,
currency: "USD",
entryType: .ecom,
intent: TransactionIntentData(card: .authorisation),
merchantTransactionId: "order-\(UUID().uuidString)",
merchantTransactionDate: { Date() }
)
let checkoutConfig = CheckoutConfig(
environment: .test,
session: sessionData,
transactionData: transactionData,
merchantShopperId: "shopper-123",
ownerType: "MerchantGroup",
ownerId: "your-owner-id",
onGetShopper: { async in
TransactionShopper(
id: "shopper-123",
email: "customer@example.com",
firstName: "John",
lastName: "Doe"
)
}
)
let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)Create your card components and submit configuration without 3DS callbacks.
let cardNumber = try pxpCheckout.create(
.cardNumber,
componentConfig: CardNumberComponentConfig(
label: "Card number",
validationOnChange: true
)
) as! CardNumberComponent
let cardExpiry = try pxpCheckout.create(
.cardExpiryDate,
componentConfig: CardExpiryDateComponentConfig(
label: "Expiry date",
validationOnChange: true
)
) as! CardExpiryDateComponent
let cardCvc = try pxpCheckout.create(
.cardCvc,
componentConfig: CardCvcComponentConfig(
label: "Security code",
validationOnChange: true
)
) as! CardCvcComponent
var submitConfig = CardSubmitComponentConfig()
submitConfig.submitText = "Pay £99.99"
submitConfig.cardNumberComponent = cardNumber
submitConfig.cardExpiryDateComponent = cardExpiry
submitConfig.cardCvcComponent = cardCvc
// Note: No 3DS callbacks = non-3DS transaction
// onPreAuthorisation is async as it returns data
submitConfig.onPreAuthorisation = { preAuth async in
print("Pre-authorisation data: \(String(describing: preAuth))")
return TransactionInitiationData()
}
submitConfig.onPostAuthorisation = { result in
switch result {
case let authorised as AuthorisedSubmitResult:
print("Payment successful: \(authorised.provider.message)")
// Navigate to success screen
case let captured as CapturedSubmitResult:
print("Payment captured: \(captured.provider.message)")
// Navigate to success screen
case let refused as RefusedSubmitResult:
print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
// Show error message
case let failed as FailedSubmitResult:
print("Payment failed: \(failed.errorReason ?? "Unknown error")")
// Show error message
default:
print("Unknown result type")
}
}
submitConfig.onSubmitError = { error in
print("Payment error: \(error.errorMessage)")
// Show error message
}
let cardSubmit = try pxpCheckout.create(.cardSubmit, componentConfig: submitConfig) as! CardSubmitComponentNever set only one of onPreInitiateAuthentication and onPreAuthentication. The SDK throws an exception if they are mismatched. For a pure non-3DS path, leave all 3DS callbacks unset on CardSubmitComponentConfig. If you need optional 3DS (for example by amount), configure both hooks and return nil from onPreInitiateAuthentication when you want to skip—see 3DS.
For low-value transactions where 3DS exemptions apply:
class LowValuePaymentManager {
private let transactionAmount: Decimal
init(transactionAmount: Decimal) {
self.transactionAmount = transactionAmount
}
func createNon3DSConfig() -> CardSubmitComponentConfig {
var config = CardSubmitComponentConfig()
config.submitText = "Pay \(formatCurrency(transactionAmount))"
config.onPreAuthorisation = { _ async in
// Add risk screening data for fraud detection
return TransactionInitiationData(
psd2Data: PSD2Data(scaExemption: .lowValue),
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
excludeDeviceData: false,
deviceSessionId: generateDeviceSessionId(),
userIp: "192.168.1.100",
account: RiskScreeningAccount(
id: "user_12345678",
creationDateTime: "2024-01-15T10:30:00.000Z"
),
items: [
RiskScreeningItem(
price: transactionAmount,
quantity: 1,
category: "General"
)
],
fulfillments: [
RiskScreeningFulfillment(
type: .shipped,
recipientPerson: RiskScreeningRecipientPerson(
phoneNumber: "+1234567890"
)
)
]
)
)
}
config.onPostAuthorisation = { result in
switch result {
case is AuthorisedSubmitResult, is CapturedSubmitResult:
print("Low-value payment successful")
// Track successful low-value payment
self.trackPaymentSuccess(amount: self.transactionAmount, type: "low_value_non_3ds")
case let refused as RefusedSubmitResult:
print("Low-value payment declined: \(refused.stateData?.message ?? "Unknown")")
default:
print("Low-value payment failed")
}
}
return config
}
private func formatCurrency(_ amount: Decimal) -> String {
return "£\(amount)"
}
private func generateDeviceSessionId() -> String {
return UUID().uuidString.replacingOccurrences(of: "-", with: "")
}
private func trackPaymentSuccess(amount: Decimal, type: String) {
// Analytics tracking for successful payments
print("Payment successful: amount=\(amount), type=\(type)")
}
}For returning customers with established trust:
class TrustedCustomerPaymentManager {
private let customerId: String
init(customerId: String) {
self.customerId = customerId
}
func createTrustedCustomerConfig() -> CardSubmitComponentConfig {
var config = CardSubmitComponentConfig()
config.submitText = "Pay securely"
config.onPreAuthorisation = { _ async in
// Add risk screening data with trusted customer indicators
return TransactionInitiationData(
psd2Data: PSD2Data(scaExemption: .trustedBeneficiary),
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
excludeDeviceData: false,
deviceSessionId: generateDeviceSessionId(),
userIp: "192.168.1.100",
account: RiskScreeningAccount(
id: self.customerId,
creationDateTime: "2024-01-15T10:30:00.000Z"
),
items: [
RiskScreeningItem(
price: 89.99,
quantity: 1,
category: "Electronics"
)
],
fulfillments: [
RiskScreeningFulfillment(
type: .shipped,
shipping: RiskScreeningShipping(
shippingMethod: .standard
),
recipientPerson: RiskScreeningRecipientPerson(
phoneNumber: "+1234567890",
email: "customer@example.com"
)
)
]
)
)
}
config.onPostAuthorisation = { result in
switch result {
case is AuthorisedSubmitResult, is CapturedSubmitResult:
print("Trusted customer payment successful")
self.updateCustomerTrustScore(customerId: self.customerId, event: "successful_payment")
case is RefusedSubmitResult:
print("Trusted customer payment declined")
self.updateCustomerTrustScore(customerId: self.customerId, event: "declined_payment")
default:
print("Unknown payment result")
}
}
return config
}
private func updateCustomerTrustScore(customerId: String, event: String) {
print("Updating trust score for \(customerId): \(event)")
}
}Implement comprehensive error handling for non-3DS transactions:
class Non3DSErrorHandler {
func handlePaymentError(_ error: BaseSdkException) {
switch error.errorCode {
case "SDK0304":
showError("Your card was declined. Please try a different payment method.")
case "SDK0500":
showError("Network error. Please check your connection and try again.")
case "SDK0305":
showError("Invalid card details. Please check and try again.")
default:
showError("Payment failed. Please try again or use a different payment method.")
}
}
private func showError(_ message: String) {
// Implementation depends on your UI framework
print("Error: \(message)")
}
}The following example shows a complete non-3DS implementation using SwiftUI:
import SwiftUI
import PXPCheckoutSDK
struct Non3DSPaymentView: View {
@StateObject private var viewModel = Non3DSPaymentViewModel()
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Express checkout")
.font(.title)
.padding(.bottom, 8)
Text("Fast and secure payment without additional verification")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.bottom, 16)
// Show error message if any
if let error = viewModel.errorMessage {
HStack {
Image(systemName: "exclamationmark.triangle")
Text(error)
}
.padding()
.background(Color.red.opacity(0.1))
.foregroundColor(.red)
.cornerRadius(8)
}
// Render payment component
if let newCard = viewModel.newCardComponent {
newCard.buildContent()
}
// Loading indicator
if viewModel.isLoading {
HStack {
Spacer()
VStack(spacing: 8) {
ProgressView()
Text("Processing payment...")
.font(.caption)
}
Spacer()
}
.padding()
}
}
.padding()
}
.onAppear {
viewModel.setupComponents()
}
}
}
class Non3DSPaymentViewModel: ObservableObject {
@Published var isLoading = false
@Published var errorMessage: String?
var newCardComponent: NewCardComponent?
private var pxpCheckout: PxpCheckout?
private let errorHandler = Non3DSErrorHandler()
func setupComponents() {
do {
// Setup checkout
let sessionData = SessionData(
sessionId: "session-\(UUID().uuidString)",
hmacKey: "your-hmac-key",
encryptionKey: "your-encryption-key"
)
let transactionData = TransactionData(
amount: 29.99,
currency: "GBP",
entryType: .ecom,
intent: TransactionIntentData(card: .authorisation),
merchantTransactionId: "order-\(UUID().uuidString)",
merchantTransactionDate: { Date() }
)
let checkoutConfig = CheckoutConfig(
environment: .test,
session: sessionData,
transactionData: transactionData,
merchantShopperId: "shopper-456",
ownerType: "MerchantGroup",
ownerId: "your-owner-id",
onGetShopper: { async in
TransactionShopper(
id: "shopper-456",
email: "customer@example.com",
firstName: "Jane",
lastName: "Smith"
)
}
)
pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
// Setup submit config without 3DS
var submitConfig = CardSubmitComponentConfig()
// No 3DS callbacks - transaction will be non-3DS
// onPreAuthorisation is async as it returns data
submitConfig.onPreAuthorisation = { _ async in
return TransactionInitiationData(
psd2Data: PSD2Data(scaExemption: .lowValue)
)
}
submitConfig.onPostAuthorisation = { [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case let authorised as AuthorisedSubmitResult:
print("Payment successful: \(authorised.provider.message)")
self?.navigateToSuccessScreen()
case let captured as CapturedSubmitResult:
print("Payment captured: \(captured.provider.message)")
self?.navigateToSuccessScreen()
case let refused as RefusedSubmitResult:
print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
self?.handlePaymentDecline(refused)
case let failed as FailedSubmitResult:
print("Payment failed: \(failed.errorReason ?? "Unknown error")")
self?.showError("Payment failed. Please try again.")
default:
print("Unknown result type")
self?.showError("Payment completed with unknown status.")
}
}
}
submitConfig.onSubmitError = { [weak self] error in
print("Submit error: \(error.errorMessage)")
DispatchQueue.main.async {
self?.isLoading = false
self?.errorHandler.handlePaymentError(error)
}
}
submitConfig.onStartSubmit = { [weak self] in
DispatchQueue.main.async {
self?.isLoading = true
}
}
// Create new card component
let newCardConfig = NewCardComponentConfig(submit: submitConfig)
newCardComponent = try pxpCheckout?.create(.newCard, componentConfig: newCardConfig) as? NewCardComponent
} catch {
showError("Failed to initialise payment: \(error.localizedDescription)")
}
}
private func handlePaymentDecline(_ result: RefusedSubmitResult) {
let declineReason = result.stateData?.code ?? "Unknown"
let message: String
switch declineReason {
case "05":
message = "Payment declined by your bank"
case "14":
message = "Invalid card number"
case "54":
message = "Card has expired"
case "61":
message = "Amount limit exceeded"
default:
message = "Payment was declined"
}
showError(message)
}
private func navigateToSuccessScreen() {
// Navigate to success screen
print("Payment successful!")
}
private func showError(_ message: String) {
errorMessage = message
}
}This section describes the data received by the different callbacks as part of the non-3DS flow.
The onPreAuthorisation callback receives pre-authorisation data. For non-3DS transactions, the 3DS-related data will be nil.
The pre-authorisation data contains token identifiers when available.
PreAuthorizationData(
gatewayTokenId: "gw_token_abc123def456789",
schemeTokenId: nil
)| Parameter | Description |
|---|---|
gatewayTokenId | The token identifier from the gateway. |
schemeTokenId | The token identifier from the card scheme (if available). |
Here's an example of what to do with this data:
submitConfig.onPreAuthorisation = { preAuth async in
print("Non-3DS authorisation for token: \(String(describing: preAuth?.gatewayTokenId))")
// Access optional token data if available
if let tokenId = preAuth?.gatewayTokenId {
print("Gateway token: \(tokenId)")
// Merchant can use gatewayTokenId to retrieve token details from backend
// and make authorisation decision
let transactionDecision = await getAuthorisationDecision(tokenId)
if !transactionDecision {
// Not proceeding with authorisation
print("Not proceeding with authorisation")
return nil
}
}
// Add fraud prevention data
return TransactionInitiationData(
psd2Data: PSD2Data(scaExemption: .lowValue),
riskScreeningData: RiskScreeningData(
performRiskScreening: true,
excludeDeviceData: false,
deviceSessionId: generateDeviceSessionId(),
userIp: "192.168.1.100",
account: RiskScreeningAccount(
id: "user_12345678",
creationDateTime: "2024-01-15T10:30:00.000Z"
),
items: [
RiskScreeningItem(
price: 89.99,
quantity: 1,
category: "Electronics"
)
],
fulfillments: [
RiskScreeningFulfillment(
type: .shipped,
shipping: RiskScreeningShipping(
shippingMethod: .standard
),
recipientPerson: RiskScreeningRecipientPerson(
phoneNumber: "+1234567890",
email: "customer@example.com"
)
)
]
)
)
}
private func generateDeviceSessionId() -> String {
return UUID().uuidString.replacingOccurrences(of: "-", with: "")
}Use the gatewayTokenId with the Get masked card data related to gateway token API to retrieve full token details including card scheme, funding source (credit/debit), masked PAN, and expiry date.
Important: 3DS external data should no longer be provided via the callback. For non-3DS flows, no 3DS data is applicable.
The onPostAuthorisation callback receives the final transaction result (SubmitResult).
If the transaction was successful, you'll receive either an AuthorisedSubmitResult or a CapturedSubmitResult:
AuthorisedSubmitResult(
state: "Authorised",
provider: ProviderResponse(
code: "00",
message: "Approved",
cardVerificationCodeResult: "M",
addressVerificationServiceResult: "Y"
),
fundingData: FundingData(
cardVerificationCodeResult: "Matched",
addressVerificationServiceResult: "Y"
)
)| Parameter | Description |
|---|---|
state | The final state of the transaction. Possible values:
|
provider | Details about the provider's response including code and message. |
provider.code | The raw result code returned by the provider that processed the transaction. |
provider.message | The raw message associated with the result code from the provider that processed the transaction. |
provider.cardVerificationCodeResult | The Card Verification Code (CVC) result returned by the provider. |
provider.addressVerificationServiceResult | The Address Verification Service (AVS) result returned by the provider. |
fundingData | Details about the payment method. |
fundingData.cardVerificationCodeResult | The Card Verification Code (CVC) result in human-readable format. |
fundingData.addressVerificationServiceResult | The Address Verification Service (AVS) result in human-readable format. |
Here's an example of what to do with this data:
submitConfig.onPostAuthorisation = { result in
print("Non-3DS payment result: \(result)")
switch result {
case let authorised as AuthorisedSubmitResult:
print("Payment successful!")
print("Provider response: \(authorised.provider.message)")
print("Transaction ID: \(authorised.fundingData.transactionId)")
// Check verification results
let fundingData = authorised.fundingData
if fundingData.cardVerificationCodeResult == "Matched" {
print("CVC verification passed")
}
if fundingData.addressVerificationServiceResult == "Y" {
print("Address verification passed")
}
// Store transaction details
storeTransactionRecord(TransactionRecord(
amount: Decimal(string: "99.99")!,
currency: "USD",
cardType: "card", // FundingDataResult doesn't have a scheme property
processingType: "non-3ds",
timestamp: Date().toISOString()
))
// Navigate to success screen
navigateToSuccessScreen()
case let captured as CapturedSubmitResult:
print("Payment captured successfully!")
print("Provider response: \(captured.provider.message)")
navigateToSuccessScreen()
case let refused as RefusedSubmitResult:
print("Payment declined: \(refused.stateData?.message ?? "Unknown")")
handlePaymentFailure(refused)
case let failed as FailedSubmitResult:
print("Payment failed: \(failed.errorReason ?? "Unknown error")")
showError("Payment failed: \(failed.errorReason ?? "Unknown error")")
default:
print("Unknown result type")
showError("Payment completed with unknown status")
}
}
struct TransactionRecord {
let amount: Decimal
let currency: String
let cardType: String
let processingType: String
let timestamp: String
}
private func navigateToSuccessScreen() {
// Navigate to success screen
print("Navigating to success screen")
}If the bank or issuer declines the transaction, you'll receive a RefusedSubmitResult:
RefusedSubmitResult(
state: "Refused",
stateData: StateData(
code: "05",
message: "Do not honour"
),
provider: ProviderResponse(
code: "05",
message: "Do not honour",
merchantAdvice: MerchantAdvice(
code: "01",
message: "Try another payment method"
),
cardVerificationCodeResult: "M",
addressVerificationServiceResult: "Y"
),
fundingData: FundingData(
cardVerificationCodeResult: "Matched",
addressVerificationServiceResult: "Y"
)
)| Parameter | Description |
|---|---|
state | The final state of the transaction. Value: Refused. |
stateData | Details about the refusal including code and message. |
stateData.code | The state code. |
stateData.message | The state message. |
provider | Provider response with merchant advice (if available). |
provider.merchantAdvice | Guidance for handling the decline. |
Here's an example of how to handle failures:
private func handlePaymentFailure(_ result: RefusedSubmitResult) {
print("Payment declined: \(result.stateData?.message ?? "Unknown")")
// Check for merchant advice
if let advice = result.provider.merchantAdvice {
switch advice.code {
case "01":
// Try another payment method
showError("Payment declined. Please try a different card.")
case "02":
// Retry with different amount
showError("Transaction amount issue. Please contact support.")
case "03":
// Contact issuer
showError("Please contact your bank to authorise this payment.")
default:
showError("Payment declined: \(advice.message)")
}
} else {
// Generic decline message based on state code
let declineReason: String
switch result.stateData?.code {
case "05":
declineReason = "Payment declined by your bank"
case "14":
declineReason = "Invalid card number"
case "54":
declineReason = "Card has expired"
case "61":
declineReason = "Amount limit exceeded"
default:
declineReason = "Payment was declined"
}
showError(declineReason)
}
}
private func showError(_ message: String) {
print("Error: \(message)")
// Show error to user
}If an error occurs during the payment processing, you'll receive error details through the onSubmitError callback.
submitConfig.onSubmitError = { error in
print("Submit error: \(error.errorMessage)")
switch error.errorCode {
// Validation Errors
case "VALIDATION_FAILED":
showError("Please check your payment details and try again.")
case "INVALID_CARD_NUMBER":
showError("Invalid card number. Please check and try again.")
case "INVALID_EXPIRY_DATE":
showError("Invalid expiry date. Please check and try again.")
case "INVALID_CVC":
showError("Invalid security code. Please check and try again.")
// Processing Errors
case "TOKENIZATION_FAILED":
showError("Unable to process card details. Please try again.")
case "SDK0500":
showError("Network connection issue. Please check your internet and try again.")
case "GATEWAY_TIMEOUT":
showError("Payment system is busy. Please try again in a moment.")
case "SERVICE_UNAVAILABLE":
showError("Payment service temporarily unavailable. Please try again later.")
// Card Errors
case "SDK0304":
showError("Card tokenisation failed. Please check your card details.")
case "SDK0305":
showError("Your card has expired. Please use a different card.")
default:
showError("Payment failed. Please try again or use a different payment method.")
}
}For more information about error handling, see Troubleshooting.