# Integrate the Embedded Components onramp Step-by-step integration guide for the Embedded Components onramp. # Android 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 to users in the US (excluding New York). - 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. - 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). - Confirm that your app is registered as a trusted application. We require this before you can use the SDK, including for simulator testing. - 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-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-register-a-new-link-user-if-needed), then go to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-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-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. #### 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" \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "oauth_client_id": "$OAUTH_CLIENT_ID", "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-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. For ACH, the API may indicate that `mandate_data` is missing. Collect acceptance and send it on a later checkout call if required. #### 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. For SDK integrations, `performCheckout` handles payment next actions such as `3DS` internally, so the table below only covers `last_error` values that require explicit action from your integration: | `last_error` | Description | How to handle | | ------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | `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. ## Supported networks and currencies ### Livemode | Currency | Network | Address | | ----------------------------- | -------------------------- | ---------------------------------------------------------------------------------- | | USDC (`usdc`) | Solana (`solana`) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | | USDC (`usdc`) | Base (`base`) | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | | USDC (`usdc`) | Sui (`sui`) | 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC | | USDC.e (`usdc`) | Tempo (`tempo`) | 0x20c000000000000000000000b9537d11c60e8b50 | | USDC (`usdc`) | Ethereum (`ethereum`) | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | USDB (`usdb`) | Solana (`solana`) | ENL66PGy8d8j5KNqLtCcg4uidDUac5ibt45wbjH9REzB | | USDsui (`usdsui`) | Sui (`sui`) | 0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::USDSUI | | USDC (`usdc`) | Arbitrum (`arbitrum`) | 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 | | USDC (`usdc`) | World Chain (`worldchain`) | 0x79A02482A880bCE3F13e09Da970dC34db4CD24d1 | | USDT (`usdt`) | Ethereum (`ethereum`) | 0xdac17f958d2ee523a2206206994597c13d831ec7 | | Phantom Cash (`phantom_cash`) | Solana (`solana`) | CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH | ### Sandbox (test mode) | Currency | Network | | ------------- | --------------------- | | USDC (`usdc`) | Solana (`solana`) | | USDC (`usdc`) | Ethereum (`ethereum`) | | USDC (`usdc`) | Base (`base`) | ## Testing > You can test your integration in two ways, 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) using 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) using live API keys. Both require your app to be registered as a trusted application with Stripe before any SDK calls succeed, including on a simulator. ### Sandbox testing 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. We recommend using a sandbox account rather than legacy test mode — legacy test mode is on a deprecation path, and sandboxes are the current, supported way to test. If you’re testing on an Android emulator, use a system image that includes Google APIs or Google Play to prevent SDK calls from failing because of app attestation errors. #### Test values Use the following values when testing each step of the flow in *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 / OTP code | `000000` | | KYC | Name | `John Verified` | | KYC | ID Number (SSN) | `000000000` | | KYC | Address line 1 | `address_full_match` | | KYC | State | Two-letter code (for example, `WA`, not `Washington`) | | Payment | Credit card number | `4242 4242 4242 4242` | When testing, use a purchase amount of 100 USD or less. The liquidity partner enforces a 100 USD limit in the testing environment. #### Verification To test verification behavior 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): - For phone verification, use `Verified` as the last name. This returns a verified result. Phone verification is asynchronous, so your integration should poll the verification status until the status is verified or rejected. - For ID document verification, the `verifyIdentity` SDK presents a testmode UI that lets you select the verification outcome directly, without uploading a real document or selfie. This lets you test all verification outcomes (success, failure, and so on) without manual review delays. ### Live mode testing Live mode testing validates production mode and has stricter requirements than sandbox testing. #### Requirements - **Live API keys**: Use your `sk_live_...` secret key and `pk_live_...` publishable key. Test keys don’t work in live mode. - **Real card charges**: 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. - **Physical device**: The SDK requires a physical iOS or Android device. Live mode doesn’t support simulators or emulators. On iOS, you can run your app from Xcode on a physical device or distribute it through [TestFlight](https://developer.apple.com/testflight/). When running from Xcode, set the `com.apple.developer.devicecheck.appattest-environment` entitlement to `production`. If you previously ran with the `development` environment, delete and reinstall the app to clear any cached states. ## LinkAuthIntent APIs ### Create a LinkAuthIntent Creates a `LinkAuthIntent` to start a [Log in with Link](https://link.com/) flow. Send the OAuth client id and scopes you need. The API returns an intent id and expiration. To obtain your `OAUTH_CLIENT_ID`, contact your Stripe account executive or solutions architect. Stripe provisions the credential as part of your onboarding. OAuth scopes used when creating 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. (For Android, iOS, and React Native) | ```shell curl -X POST https://login.link.com/v1/link_auth_intent \ -H "Authorization: Bearer $STRIPE_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "oauth_client_id": "$OAUTH_CLIENT_ID", "oauth_scopes": "kyc.status:read,crypto:ramp"}' ``` ```json // Response { "id": "lai_xxxx", "expires_at": 1756238966 } ``` **Parameters** | Parameter | Type | Description | | ----------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | string (required) | The user’s email for looking up an existing Link customer. Provide either `email` or `hashed_email`, not both. | | `hashed_email` | string (required*) | A SHA256 hash of the plain text email for privacy-sensitive flows. Provide either `email` or `hashed_email`, not both. | | `oauth_client_id` | string (required) | Your OAuth client id (for example, from Link). Identifies your application in the OAuth flow. | | `oauth_scopes` | string (required) | Comma-separated list of OAuth scopes (for example, `kyc.status:read,crypto:ramp`). Defines the permissions you’re requesting. | | `data_sharing_merchant` | string (optional) | When set, the recipient business ID for data-sharing (for example, crypto onramp). Must be a valid business ID enabled to receive OAuth tokens. | **Returns** | Field | Type | Description | | ------------ | ------- | -------------------------------------------------------------------- | | `id` | string | Unique identifier for the `LinkAuthIntent` (for example, `lai_xxx`). | | `expires_at` | integer | Unix timestamp when the intent expires. | **Errors** | HTTP status | Cause | | ----------- | ------------------------------------------------------------------------------------------------ | | 400 | Missing or invalid request body. | | 403 | The CreateLinkAuthIntent isn’t enabled for the business or the API key is invalid or missing. | | 404 | Can’t find the OAuth client for authIntentId, or the provided email has no active Link customer. | | 409 | The Link customer previously revoked the connection with this partner. | ### Retrieve access tokens Retrieving access tokens exchanges a consented `LinkAuthIntent` for an OAuth access token. Call this after the user completes authorization. Use the access token (for example, in the `Stripe-OAuth-Token` header) in subsequent onramp API requests for that user. ```shell curl -X POST https://login.link.com/v1/link_auth_intent/{authIntentId}/tokens \ -H "Authorization: Bearer $STRIPE_SECRET_KEY" ``` ```json // Response { "access_token": "liwltoken_xxx", "expires_in": 3600, "token_type": "Bearer", "refresh": { "refresh_token": "liwlrefresh_xxx", "expires_in": 7776000 } } ``` **Parameters** | Parameter | Type | Description | | --------- | ----------------- | ------------------------------------------------- | | `id` | string (required) | The Link Auth Intent id (for example, `lai_xxx`). | **Returns** | Field | Type | Description | | ----------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `access_token` | string | OAuth access token. Send it on subsequent API requests for this user (for example, in the `Stripe-OAuth-Token` header). | | `token_type` | string | Token type. Always `Bearer`. | | `expires_in` | integer | Seconds until the access token expires. | | `refresh` | object (optional) | Present when a refresh token was issued. Use it to obtain a new access token when the current one expires. See [Refresh an Access Token](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#refresh-an-access-token). | | `refresh.refresh_token` | string | OAuth refresh token. Store it securely and use it to obtain new access tokens. See [Refresh an Access Token](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#refresh-an-access-token). | | `refresh.expires_in` | integer | Seconds until the refresh token expires. | **Errors** | HTTP status | Cause | | ----------- | -------------------------------------------------------------------------------------- | | 403 | Feature not available. | | 403 | `LinkAuthIntent` hasn’t been consented by the user. | | 403 | Invalid or missing API key. | | 404 | `LinkAuthIntent` not found (an invalid id, or the intent belongs to another business). | ### Refresh an access token Exchanges a refresh token for a new access token. When your access token expires, use the refresh token you received from [Retrieve Access Tokens](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#retrieve-access-tokens) API to obtain a new access token without requiring the user to re-authorize. To obtain your `OAUTH_CLIENT_SECRET`, contact your Stripe account executive or solutions architect. Stripe provisions the credential as part of your onboarding. ```shell curl -X POST https://login.link.com/auth/token \ -H "Authorization: Bearer $STRIPE_SECRET_KEY" \ -d "grant_type=refresh_token" \ -d "refresh_token=$REFRESH_TOKEN" \ -d "client_id=$OAUTH_CLIENT_ID" \ -d "client_secret=$OAUTH_CLIENT_SECRET" ``` ```json // Response { "access_token": "liwltoken_xxx", "refresh_token": "liwlrefresh_xxx", "token_type": "Bearer", "expires_in": 3600, "scope": "kyc.status:read crypto:ramp" } ``` **Parameters** | Parameter | Type | Description | | --------------- | ----------------- | -------------------------------------------------------------------------- | | `grant_type` | string (required) | Must be `refresh_token`. | | `refresh_token` | string (required) | The refresh token previously obtained from the Retrieve Access Tokens API. | | `client_id` | string (required) | Your OAuth client ID provided by Link. | | `client_secret` | string (required) | Your OAuth client secret provided by Link. | **Returns** | Field | Type | Description | | ----------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `token_type` | string | Token type. Always `Bearer`. | | `access_token` | string | OAuth access token. Expires in 1 hour. Send it on subsequent API requests (for example, in the `Stripe-OAuth-Token` header). | | `expires_in` | integer | TTL in seconds (3600, that is, 1 hour). | | `refresh` | object (optional) | Present when a new refresh token was issued. A new refresh token is returned each time you use the old one. Store it for future refresh requests. | | `refresh.refresh_token` | string | An OAuth refresh token. Store it securely for obtaining new access tokens when the current one expires. | | `refresh.expires_in` | integer | Seconds until the refresh token expires. | | `scope` | string | The OAuth scopes granted. |