# Integrate the Android onramp

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

This guide explains how to build your integration with the Android 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, but isn’t limited to:
     - 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 Android SDK](https://github.com/stripe/stripe-android) is open source and [fully documented](https://stripe.dev/stripe-android/index.html). Add `crypto-onramp` to the `dependencies` block of your [app/build.gradle](https://developer.android.com/studio/build/dependencies) file:

```kotlin
dependencies {
    implementation 'com.stripe:crypto-onramp:23.1.0'
}
```

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

The SDK is in private preview. You must opt in with the `ExperimentalCryptoOnramp` annotation. Choose one of the following approaches:

#### Per type

```kotlin
@ExperimentalCryptoOnramp
class MyOnrampActivity : AppCompatActivity() {
    // ...
}
```

#### Per file

```kotlin
@file:OptIn(com.stripe.android.crypto.onramp.ExperimentalCryptoOnramp::class)
```

#### Per module

```kotlin
// build.gradle
android {
    kotlin {
        compilerOptions {
            freeCompilerArgs.addAll([
                "-opt-in=com.stripe.android.crypto.onramp.ExperimentalCryptoOnramp",
            ])
        }
    }
}
```

### Create the `OnrampCoordinator` (Client-side)

Create an `OnrampCoordinator` instance to use for all onramp features. Don’t use more than one `OnrampCoordinator` at a time because it relies on a shared internal state.

```kotlin
val onrampCoordinator: OnrampCoordinator =
    OnrampCoordinator
        .Builder()
        .build(application, savedStateHandle, callbacks)
```

### Configure the SDK (Client-side)

Before you call any onramp APIs successfully, configure the SDK with the `configure` function on `OnrampCoordinator`. This lets you customize your business display name and appearance so the minimal Stripe UI matches your app. You can also enable Google Pay as a payment option.

```kotlin
val configuration = OnrampConfiguration()
    .merchantDisplayName(merchantDisplayName = "Onramp Example")
    .publishableKey(publishableKey = "pk_test_key")
    .appearance(
        appearance = LinkAppearance()
            .lightColors(
                LinkAppearance.Colors()
                    .primary(Color(0xFF635BFF))
                    .contentOnPrimary(Color.White)
                    .borderSelected(Color.Black)
            )
            .darkColors(
                LinkAppearance.Colors()
                    .primary(Color(0xFF9886E6))
                    .contentOnPrimary(Color(0xFF222222))
                    .borderSelected(Color.White)
            )
            .style(LinkAppearance.Style.ALWAYS_DARK)
            .primaryButton(LinkAppearance.PrimaryButton())
    )

onrampCoordinator.configure(configuration = configuration)
```

### View Google Pay configuration example

```kotlin
val configuration = OnrampConfiguration()
    .merchantDisplayName(merchantDisplayName = "Onramp Example")
    .publishableKey(publishableKey = "pk_test_key")
    .googlePayConfig(
        GooglePayPaymentMethodLauncher.Config(
            environment = GooglePayEnvironment.Test,
            merchantCountryCode = "US",
            merchantName = "Onramp Example",
            billingAddressConfig = GooglePayPaymentMethodLauncher.BillingAddressConfig(
                isRequired = true,
                format = GooglePayPaymentMethodLauncher.BillingAddressConfig.Format.Full,
                isPhoneNumberRequired = false
            ),
            existingPaymentMethodRequired = false
        )
    )

onrampCoordinator.configure(configuration = configuration)
```

### Create a presenter (Client-side)

For UI-based features such as authorization, identity verification, payment collection, and checkout, create an `OnrampCoordinator.Presenter`:

```kotlin
val onrampPresenter = onrampCoordinator.createPresenter(yourActivity)
```

Make sure that the hosting activity uses a Material theme. In your `app/src/main/AndroidManifest.xml`, set `android:theme` to a child of a Material theme such as `Theme.MaterialComponents.DayNight`. We require this for identity verification.

## Authenticate the user

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

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

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

```kotlin
when (val result = onrampCoordinator.hasLinkAccount(currentEmail)) {
    is OnrampHasLinkAccountResult.Completed -> {
        if (result.hasLinkAccount) {
            // Proceed to authorization.
        } else {
            // Register the user first (see next step).
        }
    }
    is OnrampHasLinkAccountResult.Failed -> {
        // Lookup failed. Inspect result.error and stop.
    }
}
```

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

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

```kotlin
val userInfo = LinkUserInfo(
    email = "stripe@stripe.com",
    fullName = "Stripe",
    phone = "+17777777777",
    country = "US"
)

when (val result = onrampCoordinator.registerLinkUser(userInfo)) {
    is OnrampRegisterLinkUserResult.Completed -> {
        // Registration successful. Proceed to authorization.
    }
    is OnrampRegisterLinkUserResult.Failed -> {
        // Registration failed. Inspect result.error and let the user fix the data.
    }
}
```

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

OAuth scopes that you use when creating a `LinkAuthIntent`:

| Scope (string)    | Description                                                            |
| ----------------- | ---------------------------------------------------------------------- |
| `kyc.status:read` | Read the user’s KYC verification status.                               |
| `crypto:ramp`     | Add crypto wallets to deposit from the user’s account on their behalf. |

#### Client-side

```kotlin
// createAuthIntent is a client-side function that you implement.
// Call your backend to create a LinkAuthIntent with the API.
val result = clientBackend.createAuthIntentId(oauthScopes, authToken)
val authIntentId = result.linkAuthIntentId
```

#### 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 user consent

The client calls `authorize` on `OnrampCoordinator.Presenter` with the `authIntentId` to complete consent. This presents the OTP dialog to authorize the user. Configure the `authenticateUserCallback` as part of `OnrampCallbacks` to handle the result.

```kotlin
// Present the authorization dialog.
onrampPresenter.authorize(authIntentId)
```

```kotlin
// Handle the result via callbacks.
OnrampCallbacks()
    .authenticateUserCallback { result ->
        when (result) {
            is OnrampAuthenticateResult.Completed -> {
                // User consented. Call your backend to exchange for access token.
            }
            is OnrampAuthenticateResult.Cancelled -> {
                // User canceled. Let them try again.
            }
            is OnrampAuthenticateResult.Failed -> {
                // Authentication failed. Inspect result.error.
            }
        }
    }
```

#### Request access tokens

If the result is `Completed`, 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
  }
}
```

#### Log out

Call `logOut` when the user logs out of your app to clear all SDK state, including authorization, selected payment method, and crypto payment token.

```kotlin
when (val result = onrampCoordinator.logOut()) {
    is OnrampLogOutResult.Completed -> {
        // Successfully logged out.
    }
    is OnrampLogOutResult.Failed -> {
        // Log out failed. Inspect result.error.
    }
}
```

## Verify identity

For details on 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` to collect and submit KYC data. Present your own interface to collect this information.

```kotlin
val collectedKycInfo = KycInfo(
    firstName = "FirstName",
    lastName = "LastName",
    idNumber = "000000000",
    dateOfBirth = DateOfBirth(day = 1, month = 1, year = 1990),
    address = Address(
        line1 = "123 Main St",
        line2 = "Apt 4B",
        city = "San Francisco",
        state = "CA",
        postalCode = "94111",
        country = "US"
    )
)

when (val result = onrampCoordinator.attachKycInfo(collectedKycInfo)) {
    is OnrampAttachKycInfoResult.Completed -> {
        // KYC attached. Proceed to identity verification if needed or payment flow.
    }
    is OnrampAttachKycInfoResult.Failed -> {
        // KYC failed to attach. Inspect result.error and let the user fix the data.
    }
}
```

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

When a customer already has KYC information, use `verifyKycInfo` on `OnrampCoordinator.Presenter` so the SDK can present a screen with the customer’s existing KYC information for verification. Configure the `verifyKycCallback` as part of `OnrampCallbacks` to handle the result. If the customer needs to update their address, call `verifyKycInfo` again with the updated address.

```kotlin
// Present the KYC verification screen.
onrampPresenter.verifyKycInfo()
```

```kotlin
// Handle the result via callbacks.
OnrampCallbacks()
    .verifyKycCallback { result ->
        when (result) {
            is OnrampVerifyKycInfoResult.Confirmed -> {
                // KYC verified. Proceed to identity verification or payment flow.
            }
            is OnrampVerifyKycInfoResult.UpdateAddress -> {
                // User needs to update their address.
                // Show your address form, then call verifyKycInfo(updatedAddress).
            }
            is OnrampVerifyKycInfoResult.Cancelled -> {
                // User canceled. Let them try again.
            }
            is OnrampVerifyKycInfoResult.Failed -> {
                // Verification failed. Inspect result.error.
            }
        }
    }
```

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

Some customers must verify their identity before they continue to checkout. When required, call `verifyIdentity` on `OnrampCoordinator.Presenter`. It presents 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 see the result.

```kotlin
onrampPresenter.verifyIdentity()
```

```kotlin
// Handle the result via callbacks.
OnrampCallbacks()
    .verifyIdentityCallback { result ->
        when (result) {
            is OnrampVerifyIdentityResult.Completed -> {
                // Identity verified. Proceed to payment flow.
            }
            is OnrampVerifyIdentityResult.Cancelled -> {
                // User canceled. Let them try again.
            }
            is OnrampVerifyIdentityResult.Failed -> {
                // Verification failed. Inspect result.error.
            }
        }
    }
```

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

```kotlin
when (val result = onrampCoordinator
                    .registerWalletAddress("address", CryptoNetwork.Bitcoin)) {
    is OnrampRegisterWalletAddressResult.Completed -> {
        // Wallet registered. Proceed to collect payment method.
    }
    is OnrampRegisterWalletAddressResult.Failed -> {
        // Registration failed. Inspect result.error and let the user retry.
    }
}
```

### 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 user already has. If the list is empty or the customer wants to use a different method, have the client call `collectPaymentMethod` on `OnrampCoordinator.Presenter`.

We support cards, bank accounts, and Google Pay. For card and bank account, `collectPaymentMethod` presents the Stripe wallet UI, which lists existing stored payment methods, lets the user add new ones, and lets the user select one.

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

```kotlin
onrampPresenter.collectPaymentMethod(PaymentMethodSelection.Card())
```

```kotlin
// Handle the result via callbacks.
OnrampCallbacks()
    .collectPaymentCallback { result ->
        when (result) {
            is OnrampCollectPaymentMethodResult.Completed -> {
                // Payment method selected. Use displayData in your UI,
                // then call createCryptoPaymentToken.
            }
            is OnrampCollectPaymentMethodResult.CompletedWithKycInfo -> {
                // Payment method selected and KYC information was available.
            }
            is OnrampCollectPaymentMethodResult.Cancelled -> {
                // User canceled. Let them try again.
            }
            is OnrampCollectPaymentMethodResult.Failed -> {
                // Collection failed. Inspect result.error.
            }
        }
    }
```

After payment method selection succeeds, the callback returns a `PaymentMethodDisplayData` instance with `icon`, `label`, `type`, and `sublabel` properties that you can use in your UI to show the selected payment method.

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

Create a payment token by calling `createCryptoPaymentToken`. Use the returned token when you create the `CryptoOnrampSession`.

```kotlin
when (val result = onrampCoordinator.createCryptoPaymentToken()) {
    is OnrampCreateCryptoPaymentTokenResult.Completed -> {
        // Token created. Pass the token to createOnrampSession.
    }
    is OnrampCreateCryptoPaymentTokenResult.Failed -> {
        // Token creation failed. Inspect result.error.
    }
}
```

### 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 to create a [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md). The Android SDK doesn’t provide APIs for session creation. Your back end handles this step. The example shows how a client application might call your back end.

#### Client-side

```kotlin
val result = clientBackend.createOnrampSession(
    paymentToken = paymentToken,
    walletAddress = wallet.address,
    authToken = authToken,
    destinationNetwork = wallet.network
)

when (result) {
    is Result.Success -> {
        // Session created. Use result.sessionId for checkout.
    }
    is Result.Failure -> {
        // Creation failed. Show error and let the user retry.
    }
}
```

#### 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" \              # Pass `source_amount` or `destination_amount`, not both.
  -d "source_currency=usd" \
  -d "destination_currency=usdc" \
  -d "destination_network=base" \       # Singular. Pins the transaction to this network.
  -d "destination_networks[]=base" \    # Array. Required when `wallet_address` is set. Must match `destination_network`.
  -d "wallet_address=0x1234567890abcdef1234567890abcdef12345678" \
  -d "customer_ip_address=203.0.113.1"
```

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

Call `performCheckout` on `OnrampCoordinator.Presenter` to run the checkout flow. It presents a UI for required actions, such as `3DS`.

You must implement the `onrampSessionClientSecretProvider` callback as part of `OnrampCallbacks`. The SDK invokes it to retrieve the checkout client secret. Have it call your back end, which calls the onramp session checkout endpoint with the session ID. The response includes the `client_secret`. This callback might be called more than once during a single checkout.

#### Client-side

```kotlin
// Configure the client secret provider in your callbacks.
OnrampCallbacks()
    .onrampSessionClientSecretProvider { sessionId ->
        // Return the client secret for the given sessionId
        // from your backend API.
        return getClientSecretForSessionId(sessionId)
    }
```

```kotlin
// Start checkout.
onrampPresenter.performCheckout(onrampSessionId = sessionId)
```

```kotlin
// Handle the result via callbacks.
OnrampCallbacks()
    .checkoutCallback { result ->
        when (result) {
            is OnrampCheckoutResult.Completed -> {
                // Purchase complete. Show success.
            }
            is OnrampCheckoutResult.Cancelled -> {
                // User canceled. Let them try again.
            }
            is OnrampCheckoutResult.Failed -> {
                // Checkout failed. Inspect result.error.
            }
        }
    }
```

#### 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 user must complete a payment step, such as 3D Secure. | Run the SDK’s `3DS` handling. After the user completes the step, call checkout again.                    |
| `missing_kyc`                   | KYC verification is required.                             | Have the user complete KYC in the SDK, for example, `attachKycInfo`. Then call checkout again.           |
| `missing_document_verification` | Identity document verification is required.               | Have the user 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 user exceeded their limit.                            | Display an error message.                                                                                |
| `location_not_supported`        | We don’t support the user’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 user.    | Have the user register the wallet, then call checkout again.                                             |

## Troubleshoot the integration

### Configuration error

| Error                                                                                                     | Cause and fix                                                                                                                                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `IllegalArgumentException`: `merchantDisplayName` must not be null and `publishableKey` must not be null. | Both `merchantDisplayName` and `publishableKey` are required on `OnrampConfiguration`. Set both before you call `onrampCoordinator.configure(configuration)`.                                                                                                                              |
| `IllegalArgumentException`: Callback must not be null                                                     | All callbacks on `OnrampCallbacks` are required except `googlePayIsReadyCallback`. Set `verifyIdentityCallback`, `verifyKycCallback`, `collectPaymentCallback`, `authorizeCallback`, `checkoutCallback`, and `onrampSessionClientSecretProvider` before you build the `OnrampCoordinator`. |
| `OnrampConfigurationResult.Failed`                                                                        | The `configure` call can fail if the underlying Link SDK fails to initialize. Inspect the `error` property. A common cause is an invalid publishable key.                                                                                                                                  |

### Authentication error

| Error                                                     | Cause and fix                                                                                                                                                                                                                                                                                                   |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MissingConsumerSecretException`: Missing consumer secret | The user’s session wasn’t established or expired. Make sure that the user completed authentication through `authorize` or `authenticateUserWithToken` before you call other APIs. This error can come from `registerLinkUser`, `registerWalletAddress`, `attachKycInfo`, `verifyKycInfo`, and `verifyIdentity`. |
| Link authorization error or forced re-authentication      | If an API call returns an authorization error, the SDK automatically clears the cached Link account state. Subsequent calls fail with `MissingConsumerSecretException`. Re-authenticate the user by calling `authorize` again.                                                                                  |

### Payment error

| Error                                                        | Cause and fix                                                                                                                                                                                                                |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MissingCryptoCustomerException`: Missing crypto customer ID | No crypto customer is associated with the current session. A crypto customer ID is created during `authorize` or `registerLinkUser`. Make sure that one of these calls completed before you call `createCryptoPaymentToken`. |
| `MissingPaymentMethodException`: Missing payment method      | Payment method collection appeared to succeed, but we couldn’t resolve the selected method internally. Retry `collectPaymentMethod`.                                                                                         |

### Checkout error

| Error                                        | Cause and fix                                                                                                                                                                                                                                                                         |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PaymentFailedException`: Payment failed     | The underlying `PaymentIntent` reached a terminal failure state, such as a declined card, processing error, or unresolvable session after process failure. Inspect the error and offer the user an option to retry or select a different payment method.                              |
| `onrampSessionClientSecretProvider` failures | This callback might be called more than once during a single checkout, initially and again after handling a required next action such as `3DS`. Make sure that your back end can handle repeated calls for the same session ID. If this callback results in an error, checkout fails. |

### Identity and KYC error

| Error                                                             | Cause and fix                                                                                                                                                                      |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OnrampVerifyIdentityResult.Failed` with “No ephemeral key found” | The server responded without an ephemeral key. This usually indicates a back-end configuration issue. Ensure that the user’s account is properly set up for identity verification. |
| `OnrampVerifyKycInfoResult.UpdateAddress`                         | This isn’t an error. The user indicated that their address needs updating. Show your address form, then call `onrampPresenter.verifyKycInfo(updatedAddress)` again.                |

### General guidance

- All `Failed` result types include an `error: Throwable` property. Log or inspect it for detailed diagnostics.
- Only one `OnrampCoordinator` instance can be active at a time. Creating multiple instances can lead to undefined behavior.
- Always call `logOut()` when the user 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 in two ways: 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 and 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 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 value

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 identity verification step, `verifyIdentity`, presents a test mode UI that lets you select the verification outcome directly without uploading a real document or selfie. This lets you test all 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 Android device. We don’t support emulators for live mode.
- **Real card charge**: Live mode transactions charge a real payment method. Test card numbers don’t work.
- **Real KYC**: Users must complete real identity verification. Sandbox test values don’t apply in live mode.
