Last updated: October 14, 2026
Card checkout uses CardClient and a single CardRequest, so you collect card details in your own UI and the SDK handles any 3D Secure step-up.
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 does not use createPayPalSession().
On iOS, approveOrder() and vault() handle the 3D Secure (3DS) step-up. When PayPal requires it, the SDK presents the challenge in an ASWebAuthenticationSession and resolves your completion handler after the challenge finishes. There is no separate "present the challenge" call and no return intent to forward.
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 cards:
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. 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)After the SDK approves the order, 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 carries only the order ID, status, and didAttemptThreeDSecureAuthentication. Retrieve the saved card's token server-side after capture.
Save a card with no purchase now. This 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. PayPal does not charge the buyer. |
| Other failure | .failure, for example CardError.threeDSecureURLError, when the challenge URL fails iOS's PayPal 3DS validation | Show error.localizedDescription. |
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. PayPal does not charge the buyer. |
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. |
.sandbox to .live and use your live client ID and merchant ID.CardError.isThreeDSecureCanceled), and other failures.