# Integrate the iOS onramp

Integrate fiat-to-crypto purchases into your iOS application with the Stripe Crypto Onramp SDK.

This guide explains how to build your integration with the iOS Crypto Onramp SDK. Use it when you need full control over the onramp flow, want to understand each API, or want to customize the flow for your app.

## Before you begin

- The Embedded Components onramp is only available in the US, excluding Hawaii.
- The Embedded Components API is in private preview. No API calls succeed until onboarding is complete, including in a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes). To request access:
  1. [Submit your application](https://docs.stripe.com/crypto/onramp.md#submit-your-application).
  1. [Sign up to join the waitlist](https://docs.stripe.com/crypto/onramp.md#sign-up).
  1. Work with your Stripe account executive or solutions architect to complete onboarding before you start your integration. This includes:
     - Confirm that your account is enrolled in the required feature gates for the Embedded Components onramp APIs and Link OAuth APIs.
     - Confirm that your app is registered as a trusted application. We require this before you can use the SDK, including for simulator testing.
     - Enable Link as a payment method in your [Dashboard](https://dashboard.stripe.com/settings/payment_methods).
     - Obtain your OAuth client ID and client secret. Stripe provisions these credentials, and you need them for the [authentication flow](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#authentication).
- After onboarding is complete, obtain your secret key and [publishable API key](https://docs.stripe.com/keys.md#obtain-api-keys) from the [API keys page](https://dashboard.stripe.com/apikeys).

## Configure the mobile SDK

### Add the onramp dependency (Client-side)

The [Stripe iOS SDK](https://github.com/stripe/stripe-ios) is open source and [fully documented](https://stripe.dev/stripe-ios/index.html). It supports apps that run iOS 13 or later. Add the `StripeCryptoOnramp` product to your app with your package manager.

#### Swift Package Manager

1. In Xcode, select **File** > **Add Package Dependencies…** and enter **https://github.com/stripe/stripe-ios-spm** as the repository URL.
1. Select the latest version number from the [releases page](https://github.com/stripe/stripe-ios/releases).
1. Add the `StripeCryptoOnramp` product to the [target of your app](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app).

#### CocoaPods

1. If you haven’t already, install the latest version of [CocoaPods](https://guides.cocoapods.org/using/getting-started.html).
1. If you don’t have an existing [Podfile](https://guides.cocoapods.org/syntax/podfile.html), run the following command to create one:

```shell
pod init
```

1. Add this line to your `Podfile`:

```ruby
pod 'StripeCryptoOnramp'
```

1. Run the following command:

```shell
pod install
```

1. Open your project in Xcode with the `.xcworkspace` file instead of the `.xcodeproj` file.
1. To update to the latest version of the SDK later, run:

```shell
pod update StripeCryptoOnramp
```

#### Carthage

1. If you haven’t already, install the latest version of [Carthage](https://github.com/Carthage/Carthage#installing-carthage).
1. Add this line to your `Cartfile`:

```text
github "stripe/stripe-ios"
```

1. Follow the [Carthage installation instructions](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos). Make sure that you embed [all required frameworks](https://github.com/stripe/stripe-ios/tree/master/StripeCryptoOnramp#manual-linking).
1. To update to the latest version of the SDK later, run:

```shell
carthage update stripe-ios --platform ios
```

#### Manual framework

1. Go to the [GitHub releases page](https://github.com/stripe/stripe-ios/releases/latest) and download and unzip `Stripe.xcframework.zip`.
1. Drag `StripeCryptoOnramp.xcframework` to the **Embedded Binaries** section of the **General** settings in your Xcode project. Make sure that you select **Copy items if needed**.
1. Repeat step 2 for all required frameworks listed [here](https://github.com/stripe/stripe-ios/blob/master/StripeCryptoOnramp/README.md#manual-linking).
1. To update to the latest version of the SDK later, repeat steps 1–3.

### Opt in to experimental APIs (Client-side)

The SDK is in private preview. You must opt in with the `@_spi(CryptoOnrampAlpha)` attribute. Mark the `StripeCryptoOnramp` import like this:

```swift
@_spi(CryptoOnrampAlpha) import StripeCryptoOnramp
```

### Configure the SDK (Client-side)

Before you call any onramp APIs, set your publishable key and create a `CryptoOnrampCoordinator` instance. You can also create a `LinkAppearance` instance to customize Stripe-provided UI elements such as one-time passcode entry, payment method selection, and identity verification.

Only one `CryptoOnrampCoordinator` instance can be active at a time because the SDK uses shared internal state.

```swift
STPAPIClient.shared.publishableKey = "pk_test_123"
```

```swift
let appearance = LinkAppearance(
    colors: .init(primary: .systemBlue, selectedBorder: .label),
    primaryButton: .init(cornerRadius: 16, height: 56),
    style: .alwaysDark
)
```

```swift
Task {
    do {
        self.coordinator = try await CryptoOnrampCoordinator.create(appearance: appearance)
        // Coordinator successfully configured.
    } catch {
        // Handle thrown errors.
    }
}
```

## Authenticate the customer

### Check for a Link account (Client-side)

The customer must have a [Link](https://link.com) account to use the onramp APIs. Use `hasLinkAccount(with:)` to determine whether the customer’s email is associated with an existing Link account.

- If the customer has an account, go to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-3-authorize).
- If the customer doesn’t have an account, [register a new Link customer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-2-register-a-new-link-customer-if-needed), then go to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-3-authorize).

```swift
do {
    if try await coordinator.hasLinkAccount(with: email) {
        // The customer has an account. Proceed to authorization.
    } else {
        // Register the customer first.
    }
} catch {
    // Handle thrown errors.
}
```

### Register a new Link customer if needed (Client-side) 

If the customer doesn’t have a Link account, use `registerLinkUser` to create one with information that you collect in your UI. After account creation succeeds, go to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-3-authorize).

```swift
do {
    try await coordinator.registerLinkUser(
        email: email,
        fullName: fullName,
        phone: phoneNumber,
        country: country
    )
    // The customer is registered. Proceed to authorization.
} catch {
    // Handle thrown errors.
}
```

### Authorize (Client-side) (Server-side) 

The primary authentication method uses two-factor authorization.

#### Create a LinkAuthIntent 

A `LinkAuthIntent` tracks the scopes of the OAuth requests and the status of customer consent. Your backend calls the Create a LinkAuthIntent API with your `OAUTH_CLIENT_ID` and the onramp OAuth scopes, receives the `authIntentId`, and sends it to the client.

To obtain your `OAUTH_CLIENT_ID`, contact your Stripe account executive or solutions architect. Stripe provisions this credential during onboarding.

Use the following OAuth scopes when you create a `LinkAuthIntent`:

| Scope (string)            | Description                                                                |
| ------------------------- | -------------------------------------------------------------------------- |
| `kyc.status:read`         | Read the customer’s KYC verification status.                               |
| `crypto:ramp`             | Add crypto wallets to deposit from the customer’s account on their behalf. |
| `auth.persist_login:read` | Allow use of a persisted token for seamless sign-in.                       |

#### Client-side

```swift
// createAuthIntent is a function you implement to call your backend.
let response = try await clientBackend.createAuthIntent(oauthScopes: scopes)
let authIntentId = response.authIntentId
```

#### Server-side

```shell
curl -X POST https://login.link.com/v1/link_auth_intent \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -d email=user@example.com \
  -d oauth_client_id=$OAUTH_CLIENT_ID \
  -d oauth_scopes=kyc.status:read,crypto:ramp,auth.persist_login:read
```

#### Collect customer consent

Call `authorize(linkAuthIntentId:from:)` on `CryptoOnrampCoordinator` with the `authIntentId` to complete consent. This presents the OTP dialog so the customer can authorize the request.

```swift
do {
    let authResult = try await coordinator.authorize(
        linkAuthIntentId: authIntentId,
        from: presentingViewController
    )
    switch authResult {
    case .denied, .canceled:
        // The customer denied or canceled the authentication flow.
    case let .consented(customerId):
        // The customer successfully authenticated.
        // Proceed to KYC, identity verification, or payment.
        // Store authIntent.token to enable Seamless Sign-In in future sessions.
    }
} catch {
    // Handle thrown errors.
}
```

#### Request access tokens

If the result is `.consented`, your back end calls the Retrieve Access Tokens API to request access tokens. Store the access token and use it in all subsequent onramp API requests, for example, in the `Stripe-OAuth-Token` header.

```shell
# Request
curl -X POST https://login.link.com/v1/link_auth_intent/{authIntentId}/tokens \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY"

# Response
{
  "access_token": "liwltoken_xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh": {
    "refresh_token": "liwlrefresh_xxx",
    "expires_in": 7776000
  }
}
```

### Use Seamless Sign-In for a returning customer (Client-side) (Server-side)

To reduce friction for a returning customer, you can skip the OTP dialog by storing the `LinkAuthIntent` token after a successful `authorize` call and exchanging it for a `linkAuthTokenClientSecret` (LATCS) through your back end in the next session. Pass the LATCS to `authenticateUserWithToken(_:)` to sign in without customer interaction.

If authentication fails, for example because the token expired, clear the stored token and fall back to the standard `hasLinkAccount` and `authorize` flow.

```swift
do {
    let result = try await clientBackend.createLinkAuthToken(storedLAIToken)
    let latcs = result.linkAuthTokenClientSecret
    try await coordinator.authenticateUserWithToken(latcs)
    // The customer successfully authenticated.
} catch {
    // Seamless Sign-In failed. Clear stored tokens and fall back to authorize.
}
```

### Log out (Client-side)

Call `logOut()` when the customer logs out of your app to clear all SDK state, including authorization, the selected payment method, and the crypto payment token. Also clear any locally stored tokens that you use for seamless sign-in.

```swift
do {
    try await coordinator.logOut()
    // The customer successfully logged out.
} catch {
    // Handle thrown errors.
}
```

### View an end-to-end authentication example

The following example shows how to use the authentication APIs together in a complete authentication lifecycle, including seamless sign-in, new and existing customer handling, OTP authorization, and logout.

```swift
@_spi(CryptoOnrampAlpha) import StripeCryptoOnramp
import UIKit

final class OnrampAuthExample {

    private let coordinator: CryptoOnrampCoordinator
    private let backend: MerchantBackend
    private let tokenStore: SeamlessTokenStore

    init(
        coordinator: CryptoOnrampCoordinator,
        backend: MerchantBackend,
        tokenStore: SeamlessTokenStore
    ) {
        self.coordinator = coordinator
        self.backend = backend
        self.tokenStore = tokenStore
    }

    // Call when the customer starts an onramp session.
    func authenticateUserForOnramp() async throws {
        // 1) Try Seamless Sign-In first. This is optional and skips OTP for a returning customer.
        if let storedLAIToken = tokenStore.load() {
            do {
                let latcs = try await backend.createLinkAuthTokenClientSecret(
                    fromStoredLAIToken: storedLAIToken
                )
                try await coordinator.authenticateUserWithToken(latcs)
                return
            } catch {
                // Tokens can expire. Clear and fall back to the standard flow.
                tokenStore.clear()
            }
        }

        // 2) Collect email in your own UI.
        let email = try await promptForEmailFromYourUI()

        // 3) Check for an existing Link account.
        let hasLinkAccount = try await coordinator.hasLinkAccount(with: email)

        if !hasLinkAccount {
            // 4) Collect details and register the customer.
            let reg = try await promptForRegistrationInfoFromYourUI(prefilledEmail: email)
            try await coordinator.registerLinkUser(
                email: reg.email,
                fullName: reg.fullName,
                phone: reg.phoneE164,
                country: reg.country
            )
        }

        // 5) Create a LinkAuthIntent on your backend.
        let authIntent = try await backend.createLinkAuthIntent(
            oauthScopes: Scopes.requiredScopes
        )

        // 6) Present the OTP and consent UI.
        let presentingVC = try presentingViewControllerForOTP()
        let authResult = try await coordinator.authorize(
            linkAuthIntentId: authIntent.authIntentId,
            from: presentingVC
        )

        switch authResult {
        case .consented:
            // Store the token to enable Seamless Sign-In in future sessions.
            tokenStore.save(authIntent.token)
        case .denied:
            throw AuthFlowError.authorizationDenied
        case .canceled:
            throw AuthFlowError.authorizationCanceled
        @unknown default:
            throw AuthFlowError.unknownError
        }
    }

    // Call when the customer logs out of your app.
    func logOutUserFromApp() async {
        do {
            try await coordinator.logOut()
        } catch {
            // Continue with your own logout flow.
        }
        tokenStore.clear()
    }

    // MARK: - Placeholder UI hooks (replace with your app's UI)

    private func promptForEmailFromYourUI() async throws -> String {
        return "user@example.com"
    }

    private func promptForRegistrationInfoFromYourUI(
        prefilledEmail: String
    ) async throws -> RegistrationInput {
        RegistrationInput(
            email: prefilledEmail,
            fullName: "Jane Doe",
            phoneE164: "+12125551234",
            country: "US"
        )
    }

    private func presentingViewControllerForOTP() throws -> UIViewController {
        guard let vc = UIApplication.shared.connectedScenes
            .compactMap({ \$0 as? UIWindowScene })
            .flatMap(\.windows)
            .first(where: \.isKeyWindow)?
            .rootViewController else {
            throw AuthFlowError.noPresentingViewController
        }
        return vc
    }
}

// MARK: - Supporting types

struct RegistrationInput {
    let email: String
    let fullName: String?
    let phoneE164: String
    let country: String
}

protocol MerchantBackend {
    func createLinkAuthIntent(oauthScopes: [Scopes]) async throws -> CreateLinkAuthIntentResponse
    func createLinkAuthTokenClientSecret(fromStoredLAIToken token: String) async throws -> String
}

struct CreateLinkAuthIntentResponse {
    let authIntentId: String
    let token: String
}

protocol SeamlessTokenStore {
    func load() -> String?
    func save(_ token: String)
    func clear()
}

enum AuthFlowError: Error {
    case authorizationDenied
    case authorizationCanceled
    case noPresentingViewController
    case unknownError
}

enum Scopes: String {
    static let requiredScopes: [Scopes] = [.cryptoRamp, .kycStatusRead, .authPersistLoginRead]

    case cryptoRamp = "crypto:ramp"
    case kycStatusRead = "kyc.status:read"
    case authPersistLoginRead = "auth.persist_login:read"
}
```

## Verify identity

For details about KYC tiers and identity requirements, see the [KYC integration guide](https://docs.stripe.com/crypto/onramp/kyc-integration-guide.md).

### Check whether KYC collection is needed (Server-side)

Your back end calls the Retrieve a CryptoCustomer API with the `customerId`. Inspect the `verifications` array in the response. If it includes an entry with type `kyc_verified` and status `not_started`, go to [Collect KYC](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-2-collect-kyc-if-needed).

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId} \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

### Collect KYC if needed (Client-side) 

If the customer needs KYC verification, call `attachKYCInfo(info:)` to collect and submit KYC data. Present your own UI to collect this information.

```swift
let kycInfo = KycInfo(
    firstName: firstName,
    lastName: lastName,
    idNumber: idNumber,
    address: address,
    dateOfBirth: dateOfBirth
)

do {
    try await coordinator.attachKYCInfo(info: kycInfo)
    // KYC attached. Proceed to identity verification if needed, or to payment.
} catch {
    // Handle thrown errors.
}
```

### Verify KYC if needed (Client-side)

When a customer already has KYC information on file, use `verifyKYCInfo(updatedAddress:from:)` to present a Stripe-provided screen where the customer can confirm their existing information. If the customer needs to update their address, call `verifyKYCInfo` again with the updated address.

```swift
do {
    let result = try await coordinator.verifyKYCInfo(
        updatedAddress: nil,
        from: presentingViewController
    )
    switch result {
    case .confirmed:
        // KYC verified. Proceed to identity verification or payment.
    case .updateAddress:
        // The customer wants to update their address.
        // Show your address form, then call verifyKYCInfo(updatedAddress:from:) again.
    case .canceled:
        // The customer dismissed the flow without confirming.
    }
} catch {
    // Handle thrown errors.
}
```

### Verify identity if needed (Client-side) 

Some customers must verify their identity before they can complete checkout. When required, call `verifyIdentity(from:)` to present a Stripe-hosted flow where the customer uploads an identity document and a selfie.

Verification is asynchronous. After the customer completes the flow, your back end can call the Retrieve a CryptoCustomer API and inspect the `verifications` array to check the result.

```swift
do {
    let result = try await coordinator.verifyIdentity(from: presentingViewController)
    switch result {
    case .completed:
        // The customer completed identity verification. Proceed to payment.
    case .canceled:
        // The customer canceled the identity verification flow.
    }
} catch {
    // Handle thrown errors.
}
```

## Collect payment

### Register a crypto wallet (Client-side) (Server-side)

You must register a wallet address before you can create a payment token. This validates that the address is valid for the given network. Your back end can call the List ConsumerWallets API to determine whether the customer already has wallets on file.

If the list is empty or the customer wants to add another address, have the client call `registerWalletAddress(walletAddress:network:)` with the customer’s chosen address and network. You can reuse a previously registered wallet in future sessions.

#### List ConsumerWallets

```shell
curl "https://api.stripe.com/v1/crypto/customers/{customerId}/crypto_consumer_wallets" \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

#### Register a wallet

```swift
do {
    try await coordinator.registerWalletAddress(
        walletAddress: "bc1qztnc…",
        network: .bitcoin
    )
    // Wallet registered. Proceed to collect a payment method.
} catch {
    // Handle thrown errors.
}
```

### Collect a payment method (Client-side) (Server-side)

You must collect a payment method before a transaction can occur. Your back end can call the List PaymentTokens API to determine which payment methods the customer already has. If the list is empty or the customer wants to use a different method, have the client call `collectPaymentMethod(type:from:)` on `CryptoOnrampCoordinator`.

We support cards, bank accounts, and Apple Pay. For card and bank account, `collectPaymentMethod` presents the Stripe wallet UI, which lists existing stored payment methods, lets the customer add new ones, and lets the customer select one. After a successful selection, it returns a `PaymentMethodDisplayData` instance with `paymentMethodType`, `icon`, `label`, and `sublabel` properties that you can use in your UI.

#### List PaymentTokens

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId}/payment_tokens \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

#### Collect a payment method

```swift
do {
    let type = PaymentMethodType.card // or PaymentMethodType.bankAccount
    if let displayData = try await coordinator.collectPaymentMethod(
        type: type,
        from: presentingViewController
    ) {
        // Payment method selected. Optionally use displayData in your UI.
    } else {
        // The customer canceled payment method selection.
    }
} catch {
    // Handle thrown errors.
}
```

#### Apple Pay

To offer Apple Pay, check whether the device supports it with [`StripeAPI.deviceSupportsApplePay()`](https://stripe.dev/stripe-ios/stripe/documentation/stripe/stripeapi/devicesupportsapplepay\(\)) before you show the button. For example, in a SwiftUI view:

```swift
if StripeAPI.deviceSupportsApplePay() {
    PayWithApplePayButton(.plain) {
        // Proceed with Apple Pay collection.
    }
}
```

For `PaymentMethodType.applePay`, you must supply a [`PKPaymentRequest`](https://developer.apple.com/documentation/passkit/pkpaymentrequest). You can use the `StripeCore` framework to generate one. The following example creates a payment request with a pending amount because fees aren’t determined until checkout:

```swift
let request = StripeAPI.paymentRequest(
    withMerchantIdentifier: "my_merchant_id",
    country: "US",
    currency: "USD"
)

request.paymentSummaryItems = [
    PKPaymentSummaryItem(
        label: "My Company",
        amount: .zero,
        type: .pending
    )
]
```

When you have the `PKPaymentRequest`, call `collectPaymentMethod` with `PaymentMethodType.applePay` when the customer taps **Apple Pay**:

```swift
do {
    let type = PaymentMethodType.applePay(paymentRequest: request)
    if let displayData = try await coordinator.collectPaymentMethod(
        type: type,
        from: presentingViewController
    ) {
        // Apple Pay payment method selected.
    } else {
        // The customer canceled Apple Pay.
    }
} catch {
    // Handle thrown errors.
}
```

The `CryptoOnrampCoordinator` instance tracks the most recently selected payment method and uses it in the next transaction.

### Create a payment token (Client-side)

Create a payment token for the selected payment method by calling `createCryptoPaymentToken()`. Use the returned token when you create the `CryptoOnrampSession`.

```swift
do {
    let token = try await coordinator.createCryptoPaymentToken()
    // Payment token created. Proceed to session creation and checkout.
} catch {
    // Handle thrown errors.
}
```

### Create a crypto onramp session (Server-side)

From your UI, determine the amount, source currency such as `usd`, destination currency such as `usdc`, and network. Your back end calls the [Create a CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/create.md) API. The iOS SDK doesn’t provide APIs for session creation. Your back end handles this step. The following example shows how a client application might call your back end.

#### Client-side

```swift
let request = CreateOnrampSessionRequest(
    paymentToken: paymentToken,
    sourceAmount: 100.0,
    sourceCurrency: "usd",
    destinationCurrency: "usdc",
    destinationNetwork: wallet.network,
    walletAddress: wallet.walletAddress
)
do {
    let sessionResponse = try await clientBackend.createOnrampSession(request: request)
    // Session created. Use sessionResponse.sessionId for checkout.
} catch {
    // Handle thrown errors.
}
```

#### Server-side

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN" \
  -d "ui_mode=headless" \
  -d "crypto_customer_id=crc_xxx" \
  -d "payment_token=cpt_xxx" \
  -d "source_amount=100" \
  -d "source_currency=usd" \
  -d "destination_currency=usdc" \
  -d "destination_network=base" \
  -d "destination_networks[]=base" \
  -d "wallet_address=0x1234567890abcdef1234567890abcdef12345678" \
  -d "customer_ip_address=203.0.113.1"
```

Pass `source_amount` or `destination_amount`, not both. Use the singular `destination_network` to pin the transaction to a network. When you set `wallet_address`, you must also set `destination_networks[]`, and its value must match `destination_network`.

### Perform checkout (Client-side) (Server-side)

To perform checkout, your view controller must conform to [`STPAuthenticationContext`](https://stripe.dev/stripe-ios/stripe/documentation/stripe/stpauthenticationcontext) so the SDK can present authentication challenges:

```swift
extension MyCheckoutViewController: STPAuthenticationContext {
    func authenticationPresentingViewController() -> UIViewController {
        self
    }
}
```

Call `performCheckout(onrampSessionId:authenticationContext:clientSecretProvider:)` with the session ID and a closure that retrieves the checkout client secret from your back end. The closure might be called more than once during a single checkout, for example, after handling a 3D Secure challenge.

#### Client-side

```swift
do {
    let checkoutResult = try await coordinator.performCheckout(
        onrampSessionId: sessionResponse.sessionId,
        authenticationContext: presentingViewController
    ) { onrampSessionId in
        let result = try await APIClient.shared.checkout(onrampSessionId: onrampSessionId)
        return result.clientSecret
    }
    switch checkoutResult {
    case .completed:
        // Checkout completed successfully.
    case .canceled:
        // Checkout canceled during an authentication challenge.
    }
} catch {
    // Handle thrown errors.
}
```

#### Server-side

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions/{sessionId}/checkout \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

When the API returns `200` or `202` but the purchase isn’t complete, the response body includes the [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions.md) object with `transaction_details.last_error` set. Use that value to determine the next step:

| `last_error`                    | Description                                                   | How to handle                                                                                                |
| ------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `action_required`               | The customer must complete a payment step, such as 3D Secure. | Run the SDK’s 3DS handling. After the customer completes the step, call checkout again.                      |
| `missing_kyc`                   | KYC verification is required.                                 | Have the customer complete KYC in the SDK, for example, `attachKYCInfo`. Then call checkout again.           |
| `missing_document_verification` | Identity document verification is required.                   | Have the customer complete verification in the SDK, for example, `verifyIdentity`. Then call checkout again. |
| `charged_with_expired_quote`    | The quote expired.                                            | Refresh the quote on your back end, then call checkout again.                                                |
| `transaction_limit_reached`     | The customer exceeded their limit.                            | Display an error message.                                                                                    |
| `location_not_supported`        | We don’t support the customer’s location.                     | Show that the service isn’t available in their region.                                                       |
| `transaction_failed`            | A generic failure occurred.                                   | Display a generic error message.                                                                             |
| `missing_consumer_wallet`       | The wallet address doesn’t exist for the current customer.    | Have the customer register the wallet, then call checkout again.                                             |

## Troubleshoot the integration

### Configuration error

| Error                                              | Cause and fix                                                                                                                                                                             |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CryptoOnrampCoordinator.create()` throws an error | The `create()` factory method can fail if the underlying Link SDK fails to initialize. A common cause is an invalid publishable key set on `STPAPIClient`. Inspect the error for details. |

### Authentication error

| Error                                                      | Cause and fix                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LinkController.IntegrationError.noActiveLinkConsumer`     | The customer’s session wasn’t established or expired. Make sure that they completed authentication through `authorize` or `authenticateUserWithToken` before you call other APIs. This error can come from `authenticateUserWithToken`, `registerWalletAddress`, `attachKYCInfo`, `verifyKYCInfo`, `verifyIdentity`, `collectPaymentMethod`, and `createCryptoPaymentToken`. Re-authenticate the customer by calling `authorize` again. |
| `CryptoOnrampCoordinator.Error.seamlessSignInTokenInvalid` | `authenticateUserWithToken` throws this error when the provided token expired, was already used, or was revoked. Fall back to `authorize` to sign in the customer manually and clear any stored tokens.                                                                                                                                                                                                                                 |

### Registration error

| Error                                                    | Cause and fix                                                                                                                                                                                                                            |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CryptoOnrampCoordinator.Error.linkAccountAlreadyExists` | `registerLinkUser` throws this error if the email is already associated with an existing Link account. Use `hasLinkAccount(with:)` to check before you attempt registration, or direct the customer to sign in with `authorize` instead. |
| `CryptoOnrampCoordinator.Error.invalidPhoneFormat`       | `registerLinkUser` throws this error if the phone number isn’t in E.164 format, for example, `+12125551234`. Validate the format before you call this API.                                                                               |

### Payment error

| Error                                                        | Cause and fix                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CryptoOnrampCoordinator.Error.missingCryptoCustomerID`      | `createCryptoPaymentToken` throws this error. A crypto customer ID is created during `authorize`, `authenticateUserWithToken`, or `registerLinkUser`. Make sure that one of these steps completed before you try to create a payment token.                                                                                                                           |
| `CryptoOnrampCoordinator.Error.invalidSelectedPaymentSource` | `createCryptoPaymentToken` throws this error if no payment method has been collected. `collectPaymentMethod` can also throw it if the selected method can’t be resolved internally. Make sure that `collectPaymentMethod` succeeded before you call `createCryptoPaymentToken`. If the error occurs during payment collection, retry the `collectPaymentMethod` call. |
| `CryptoOnrampCoordinator.Error.linkAccountNotVerified`       | `collectPaymentMethod` throws this error for Link payment types (`.card`, `.bankAccount`) when the Link account session isn’t in a verified state. Make sure that the customer’s authentication and verification flow completed before you collect a payment method.                                                                                                  |

### Checkout error

| Error                                | Cause and fix                                                                                                                                                                                                                           |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CheckoutError.paymentFailed`        | The underlying `PaymentIntent` reached a terminal failure state, for example, a declined card, processing error, or failed 3D Secure. Inspect the error and offer the customer an option to retry or select a different payment method. |
| `CheckoutError.missingPaymentMethod` | The `PaymentIntent` doesn’t have an associated payment method. Make sure that a payment method was collected successfully before you initiate checkout.                                                                                 |
| `CheckoutError.unexpectedError`      | A catch-all error for unexpected states during checkout. Log the surrounding context and retry the checkout.                                                                                                                            |

### Identity and KYC error

| Error                                               | Cause and fix                                                                                                                                                                                                                     |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CryptoOnrampCoordinator.Error.missingEphemeralKey` | `verifyIdentity` throws this error when the server responds without an ephemeral key. This usually indicates a back-end configuration issue. Make sure that the customer’s account is set up correctly for identity verification. |
| `VerifyKYCResult.updateAddress`                     | This isn’t an error. When `verifyKYCInfo` returns `.updateAddress`, show your own address form and call `verifyKYCInfo(updatedAddress:from:)` again with the new address.                                                         |

### General guidance

- All errors thrown by `CryptoOnrampCoordinator` APIs conform to `LocalizedError`. Use the `localizedDescription` property for detailed diagnostics.
- Only one `CryptoOnrampCoordinator` instance can be active at a time. Creating multiple instances can lead to undefined behavior.
- Always call `logOut()` when the customer logs out of your app to clean up SDK state and avoid stale session issues.
- When you use a test mode publishable key that contains `test`, the SDK operates against the Stripe test environment. No real transactions are processed, and no actual identity verification occurs.

## Test the integration

> You can test your integration either using a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes) with test API keys or in *live mode* (Use this mode when you’re ready to launch your app. Card networks or payment providers process payments) with live API keys. Both require your app to be registered as a trusted application with Stripe before any SDK calls succeed. The OAuth client ID and client secret are the same for both sandboxes and live mode.

### Test in a sandbox

Use a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes) to build and verify your integration without real charges or real KYC. Use your `sk_test_...` secret key and `pk_test_...` publishable key.

#### Use test values

Use the following values when you test each step of the flow in a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes):

| Step           | Field                    | Test value                                           |
| -------------- | ------------------------ | ---------------------------------------------------- |
| Authentication | SMS or OTP code          | `000000`                                             |
| KYC            | SSN (`idNumber`)         | `000000000`                                          |
| KYC            | Address line 1 (`line1`) | `address_full_match`                                 |
| KYC            | State (`state`)          | Two-letter code, for example, `WA`, not `Washington` |
| Payment        | Credit card number       | `4242 4242 4242 4242`                                |

#### Verify identity

The `verifyIdentity` step presents a test mode UI that lets you select the verification outcome directly without uploading a real document or selfie. This lets you test verification outcomes such as success and failure without manual review delays.

### Test in live mode

Live mode testing validates that the onramp flow works in production, and it has stricter requirements than sandbox testing.

#### Meet live mode requirements

- **Live API keys**: Use your `sk_live_...` secret key and `pk_live_...` publishable key. Test keys don’t work in live mode.
- **Physical device**: The SDK requires a physical iOS device. We don’t support simulators for live mode.
- **Real card charge**: Live mode transactions charge a real payment method. Test card numbers don’t work.
- **Real KYC**: customers must complete real identity verification. Sandbox test values don’t apply in live mode.
