On this page
No Headings
Last updated: September 14, 2026
This guide shows you how to accept an Advanced Credit and Debit Card (ACDC) payment in your iOS app with PayPal Mobile SDK v3.0.0, including saving a card with or without a purchase.
The SDK ships with no card-entry UI. You build your own fields for card number, expiry, CVV, and optionally cardholder name and billing address. You are responsible for input validation, including the Luhn check, brand detection, and formatting.
Card checkout uses its own client, CardClient, and passes everything in a single CardRequest. It does not use createPayPalSession().
On iOS, the 3D Secure (3DS) step-up, when required, is handled entirely inside approveOrder() / vault(): the SDK presents the challenge in an ASWebAuthenticationSession and resolves your completion handler once it is done. There is no separate "present the challenge" call and no return intent to forward.
Before you start
Complete Install and set up.
This diagram shows the full round trip from card entry to a captured payment, including the optional 3D Secure step-up.
Complete Install and set up. For Card specifically:
CardPayments product.CoreConfig you built in setup:let cardClient = CardClient(config: config)ASWebAuthenticationSession.Call the Orders v2 API server-to-server and return only the order ID to your app.
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{ "intent": "CAPTURE", "purchase_units": [ { "amount": { "currency_code": "USD", "value": "49.99" } } ] }'These steps take you from a blank card form to a captured payment: collect and validate the card, hand it to the SDK to approve, which also resolves any 3D Secure challenge, then capture the order. When the last step completes, the buyer sees their payment confirmed.
The SDK provides no card-entry fields, so build your own UI, then create the order on your server.
let card = Card(
number: "4111111111111111",
expirationMonth: "01",
expirationYear: "2028",
securityCode: "123",
cardholderName: "Jane Smith", // optional
billingAddress: Address(countryCode: "US") // optional
)
let orderID = try await myServer.createOrder()If a 3DS challenge is required, the SDK presents it automatically. The completion does not fire until any challenge is fully resolved.
let request = CardRequest(
orderID: orderID,
card: card,
sca: .scaWhenRequired // or .scaAlways to force a challenge every time
)
cardClient.approveOrder(request: request) { result in
switch result {
case .success(let cardResult):
captureOrder(cardResult.orderID) // cardResult.didAttemptThreeDSecureAuthentication also available
case .failure(let error):
if CardError.isThreeDSecureCanceled(error) {
showCheckoutScreen()
} else {
showError(error.localizedDescription)
}
}
}
// An async/await variant is also available:
// let result = try await cardClient.approveOrder(request: request)Once the order is approved, capture it on your server to complete the payment.
func captureOrder(_ orderID: String) {
// POST /v2/checkout/orders/{orderID}/capture on your server
myServer.captureOrder(orderID)
}Save the card while completing a purchase, in one approval, using the same approveOrder() sequence as above. The only difference is that your server creates the order with payment_source.card.attributes.vault set:
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"intent": "CAPTURE",
"purchase_units": [ { "amount": { "currency_code": "USD", "value": "49.99" } } ],
"payment_source": { "card": { "attributes": { "vault": { "store_in_vault": "ON_SUCCESS" }, "customer": { "id": "<existing-or-new-customer-id>" } } } }
}'On approval the SDK result still only carries the order ID, status, and didAttemptThreeDSecureAuthentication. It does not return a payment token. Retrieve the saved card's token server-side after capture.
Save a card with no purchase now. Follows the identical pattern against a setup token.
let vaultRequest = CardVaultRequest(card: card, setupTokenID: setupTokenID)
cardClient.vault(vaultRequest) { result in
switch result {
case .success(let vaultResult):
savePaymentToken(vaultResult.setupTokenID) // vaultResult.didAttemptThreeDSecureAuthentication also available
case .failure(let error):
showError(error.localizedDescription)
}
}
// An async/await variant is also available:
// let result = try await cardClient.vault(vaultRequest)Card delivers a single Result. Any 3DS challenge resolves inline before it fires. Cancellation of the challenge is represented as a failure, not a separate case.
| Outcome | How it is delivered | What you do |
|---|---|---|
| Success | .success with CardResult / CardVaultResult, and the order or setup token ID, status, and didAttemptThreeDSecureAuthentication | Capture the order or store the setup token. |
| 3DS canceled | .failure where CardError.isThreeDSecureCanceled(error) is true | Return the buyer to your checkout screen. No charge was made. |
| Other failure | .failure, for example CardError.threeDSecureURLError, when the challenge URL fails iOS's PayPal 3DS validation | Show error.localizedDescription. |
Platform note
iOS validates that a 3DS challenge URL is a genuine PayPal 3DS page before opening it; Android opens whatever URL comes back on the payer-action link.
| Scenario | Expected result |
|---|---|
| No challenge required | approveOrder() / vault() resolves with success and didAttemptThreeDSecureAuthentication = false. |
| 3DS challenge required | The buyer completes the challenge inline. The completion fires with didAttemptThreeDSecureAuthentication = true. |
| Buyer cancels the 3DS challenge | CardError.isThreeDSecureCanceled(error) is true. No charge is made. |
sca: .scaAlways | Every approval attempts a challenge. Useful for testing the challenge path on demand. |
| Save with purchase | Order captures. The saved card's token is retrievable server-side, not from the SDK result. |
.sandbox to .live and use your live client ID and merchant ID.CardError.isThreeDSecureCanceled), and other failures are handled.