# Accept a card payment (/sdk/ios/add-payment-methods/card)

Accept Advanced Credit and Debit Card (ACDC) payments on iOS, including saving payment methods, with PayPal Mobile SDK v3.0.0.



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.

> **Info:** Complete [Install and set up](/sdk/ios/install-and-setup).

## Overview [#overview]

This diagram shows the full round trip from card entry to a captured payment, including the optional 3D Secure step-up.

```mermaid
sequenceDiagram
    participant App as Your App
    participant SDK as PayPal SDK
    participant PayPal as PayPal / 3DS challenge
    participant Server as Your Server

    Note over App: Buyer enters card details in your own UI
    App->>Server: Create order (Orders v2)
    Server-->>App: orderID
    App->>SDK: cardClient.approveOrder(CardRequest(orderID, card, sca))
    alt 3DS step-up required
        SDK->>PayPal: Present the 3DS challenge in ASWebAuthenticationSession
        PayPal-->>SDK: Challenge result (resolved in-process)
    end
    SDK->>App: .success(CardResult) / .failure (cancel through CardError.isThreeDSecureCanceled)
    App->>Server: Capture order (Orders v2)
```

## Before you begin [#before-you-begin]

Complete Install and set up. For Card specifically:

* Add the `CardPayments` product.
* Construct the client from the `CoreConfig` you built in setup:

```swift
let cardClient = CardClient(config: config)
```

* No return-URL registration is needed. The 3DS challenge resolves in-process through `ASWebAuthenticationSession`.

## Server: create an order [#server-create-an-order]

Call the Orders v2 API server-to-server and return only the order ID to your app.

```shell
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" } } ] }'
```

## Integrate card payments [#integrate-card-payments]

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.

### 1. Collect the card and create the order [#1-collect-the-card-and-create-the-order]

The SDK provides no card-entry fields, so build your own UI, then create the order on your server.

```swift
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()
```

### 2. Approve the order [#2-approve-the-order]

If a 3DS challenge is required, the SDK presents it automatically. The completion does not fire until any challenge is fully resolved.

```swift
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)
```

### 3. Capture the order [#3-capture-the-order]

Once the order is approved, capture it on your server to complete the payment.

```swift
func captureOrder(_ orderID: String) {
    // POST /v2/checkout/orders/{orderID}/capture on your server
    myServer.captureOrder(orderID)
}
```

## Save with purchase [#save-with-purchase]

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:

```shell
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 without purchase [#save-without-purchase]

Save a card with no purchase now. Follows the identical pattern against a setup token.

```swift
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)
```

## Handle results [#handle-results]

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`.                            |

> **Info:** 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.

## Test and go live [#test-and-go-live]

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

### Go live [#go-live]

* Validate card input in your own UI: Luhn check, brand detection, formatting. The SDK does not.
* Switch `.sandbox` to `.live` and use your live client ID and merchant ID.
* Confirm success, 3DS-canceled (`CardError.isThreeDSecureCanceled`), and other failures are handled.
* Contact your PayPal account team to enable ACDC for production traffic.

## See also [#see-also]

* [Troubleshooting](/sdk/ios/troubleshooting): diagnose common build and integration failures by symptom.
* [Orders v2 API](/api/orders/v2/): create, authorize, capture, and refund orders from your server.
