RyzeDeskRyzeDesk

Stripe

4105 articles

Integrate the Embedded Components on-ramp


Private preview

Integrate the Embedded Components on-ramp Private preview

Step-by-step integration guide for the Embedded Components on-ramp.

Web

React Native

Android

iOS

This guide explains how to build your integration with the Android Crypto Onramp SDK. Use it when you need full control over the on-ramp flow, want to understand each API, or want to customise the flow for your app.

Before you begin

  • The Embedded Components on-ramp 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 . To request access:
  1. Submit your application .
  2. 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 on-ramp APIs and Link OAuth APIs.
  • Enable Link as a payment method in your Dashboard .
  • Obtain your OAuth client ID and client secret. Stripe provisions these credentials, and you need them for the authentication flow .
  • 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 from the API keys page .
  • The server-side SDK bindings in this guide are only available in private-preview releases. Install the latest private-preview version listed in the server-side SDKs .

Configure a Stripe server-side client before using the server-side examples:

const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET_KEY, {
 apiVersion: `${Stripe.API_VERSION};crypto_onramp_beta=v2`,
});

Configure the mobile SDK

Add the on-ramp dependency Client-side

The Stripe Android SDK is open source and fully documented. Add crypto-onramp to the dependencies block of your app/build.gradle file:

dependencies {
 implementation 'com.stripe:crypto-onramp:23.20.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:

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

Create the OnrampCoordinator Client-side

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

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

Configure the SDK Client-side

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

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

Create a presenter Client-side

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

val onrampPresenter = onrampCoordinator.createPresenter(yourActivity)

Make sure that the hosting activity uses a Material theme. In your the relevant part of the product, set android:theme to a child of a Material theme such as Theme.MaterialComponents.DayNight. We require this for identity verification.

Authenticate the user

The user must have a Link account to use the on-ramp 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 Authorise .
  • If the user doesn’t have an account, register a new Link user , then go to Authorise .
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.
 }
}

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

val userInfo = LinkUserInfo(
 email = "stripe@stripe.com", // Standard email format, max 800 characters.
 fullName = "Stripe",
 phone = "+17777777777", // E.164 formatted phone number.
 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.
 }
}

Authorise Client-side Server-side

The primary authentication method uses two-factor authorisation.

Create a LinkAuthIntent

A LinkAuthIntent tracks the scopes of the OAuth requests and the status of user consent. Your back-end calls the Create a LinkAuthIntent API with your the related setting and the on-ramp OAuth scopes, receives the authIntentId, and sends it to the client.

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

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

// Present the authorization dialog.
onrampPresenter.authorize(authIntentId)
// 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 on-ramp API requests, for example, in the Stripe-OAuth-Token header.

Log out

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

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. For EU-specific identity requirements, see the EU KYC integration guide.

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.

Select a language

Node.js

curl

No results

const customer = await stripe.crypto.customers.retrieve(
 customerId,
 {},
 {headers: {'Stripe-OAuth-Token': accessToken}}
);

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.

Regional considerations EU

EU customers require additional fields such as nationalities and birthCountry. See the EU KYC integration guide for the complete list of required fields.

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.

// Present the KYC verification screen.
onrampPresenter.verifyKycInfo()
// 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.

If the customer uploads the wrong documents or retrieve a Cryptocurrency Customer returns a failed or rejected verification for the L2 tier, call verifyIdentity again to start a new verification session to let the customer re-upload their identity documents.

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.

onrampPresenter.verifyIdentity()
// 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.

Select a language

Node.js

curl

No results

const wallets = await stripe.crypto.customers.listConsumerWallets(
 customerId,
 {},
 {headers: {'Stripe-OAuth-Token': accessToken}}
);

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 ( PaymentMethodSelection.Card()), bank accounts ( PaymentMethodSelection.BankAccount()) or both ( PaymentMethodSelection.CardAndBankAccount()) and Google Pay ( PaymentMethodSelection.GooglePay()). 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.

Regional considerations EU

EU sessions support card payments. Pass PaymentMethodSelection.Card() as the payment method type. Bank account payment methods aren’t supported for EU transactions.

Select a language

Node.js

curl

No results

const paymentTokens = await stripe.crypto.customers.listPaymentTokens(
 customerId,
 {},
 {headers: {'Stripe-OAuth-Token': accessToken}}
);

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.

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 on-ramp 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 API to create a CryptoOnrampSession. 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.

Regional considerations EU

For EU sessions, set source_currency to eur.

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

Note

If the API returns an HTTP 400 error with the crypto_onramp_missing_document_verification code even though the transaction amount is within the customer’s current tier limit, the customer will need to complete an identity challenge.

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 perform the onramp checkout. Have it call your back end, which calls the onramp session checkout endpoint with the session ID. Your back end returns the client_secret from the response, which your callback then returns to the SDK. This callback might be called more than once during a single checkout (for example, after the SDK handles a required next action such as 3DS).

Caution

Always call the onramp session checkout endpoint from within this callback – never call it directly from your back end outside of performCheckout. The SDK invokes the callback because it must handle any required payment next actions, such as 3DS authentication, between checkout calls. Calling the checkout endpoint directly might appear to succeed in a sandbox (where next actions are rarely required), but the transaction isn’t considered finalised until performCheckout returns a successful result.

For ACH, the API may indicate that mandate_data is missing. Collect acceptance and send it on a later checkout call if required.

// Start checkout.
onrampPresenter.performCheckout(onrampSessionId = sessionId)
// 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.
 }
 }
 }

Optional Display a price estimate

Troubleshoot the integration

For errors and fixes specific to the Android integration, see Handle errors.

Supported networks and currencies

Live mode

CurrencyNetworkAddress
the related setting ( usdc)Solana ( solana)EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
the related setting ( usdc)Base ( base)0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
the related setting ( ousd)Base ( base)Coming soon
the related setting ( ousd)Solana ( solana)Coming soon
the related setting ( ousd)Ethereum ( ethereum)Coming soon
the related setting ( ousd)Tempo ( tempo)Coming soon
the related setting ( usdc)Sui ( sui)0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::the related setting
the related setting.e ( usdc)Tempo ( tempo)0x20c000000000000000000000b9537d11c60e8b50
the related setting ( usdc)Ethereum ( ethereum)0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
the related setting ( usdb)Solana ( solana)ENL66PGy8d8j5KNqLtCcg4uidDUac5ibt45wbjH9REzB
USDsui ( usdsui)Sui ( sui)0x44f838219cf67b058f3b37907b655f226153c18e33dfcd0da559a844fea9b1c1::usdsui::the related setting
the related setting ( usdc)Arbitrum ( arbitrum)0xaf88d065e77c8cC2239327C5EDb3A432268e5831
the related setting ( usdc)World Chain ( worldchain)0x79A02482A880bCE3F13e09Da970dC34db4CD24d1
the related setting ( usdc)Polygon ( polygon)0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
the related setting ( usdt)Ethereum ( ethereum)0xdac17f958d2ee523a2206206994597c13d831ec7
the related setting ( usdc)Celo ( celo)0xcebA9300f2b948710d2653dD7B07f33A8B32118C
RipUSD ( ripusd)Solana ( solana)ripNyDf7Vi6g4qoBp9F9ZfVSTNd1jvmFHQbRVRSXpaj
Phantom Cash ( phantom_cash)Solana ( solana)CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH

Sandbox (test mode)

CurrencyNetworkSupported regions
the related setting ( usdc)Solana ( solana)US
the related setting ( usdc)Ethereum ( ethereum)US, EU
the related setting ( usdc)Base ( base)US

Testing

Note

You can test your Integration in two ways, in a sandbox using test API keys, or in Live mode 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 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:

StepFieldTest value
AuthenticationSMS / OTP code000000
KYCNameJohn Verified
KYCID Number (SSN)000000000
KYCAddress line 1address_full_match
KYCStateTwo-letter code (for example, WA, not Washington)
PaymentCredit card number4242 4242 4242 4242

When testing, use a purchase amount of US$100 or less. The liquidity partner enforces a US$100 limit in the testing environment.

In a sandbox, the source amount used to execute the trade is hardcoded. As a result, the destination_crypto_amount returned in the API response reflects the original quote and might not match the amount that was actually executed. This is expected sandbox behaviour. In production, the settled amount reflects what was actually executed.

Verification

To test verification behaviour in a ‘ ‘sandbox’ ’:

  • 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 . 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 flow. Send the OAuth client id and scopes you need. The API returns an intent id and expiration.

To obtain your the related setting, 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:readRead the customer’s KYC verification status.
crypto:rampAdd crypto wallets to deposit from the customer’s account on their behalf.
auth.persist_login:readAllow your app to create authentication tokens for seamless sign-in on future visits. (For Android, iOS and React Native)
// Response
{
 "id": "lai_xxxx",
 "expires_at": 1756238966
}

Parameters

ParameterTypeDescription
emailstring (required)The user’s email for looking up an existing Link customer. Provide either email or hashed_email, not both.
hashed_emailstring (required*)A the related setting hash of the plain text email for privacy-sensitive flows. Provide either email or hashed_email, not both.
oauth_client_idstring (required)Your OAuth client id (for example, from Link). Identifies your application in the OAuth flow.
oauth_scopesstring (required)Comma-separated list of OAuth scopes (for example, kyc.status:read,crypto:ramp). Defines the permissions you’re requesting.
data_sharing_merchantstring (optional)When set, the recipient business ID for data-sharing (for example, crypto on-ramp). Must be a valid business ID enabled to receive OAuth tokens.

Returns

FieldTypeDescription
idstringUnique identifier for the LinkAuthIntent (for example, lai_xxx).
expires_atintegerUnix timestamp when the intent expires.

Errors

HTTP statusCause
400Missing or invalid request body.
403The CreateLinkAuthIntent isn’t enabled for the business or the API key is invalid or missing.
404Can’t find the OAuth client for authIntentId, or the provided email has no active Link customer.
409The Link customer previously revoked the connection with this partner.

Retrieve access tokens

The Retrieve Access Tokens API returns the OAuth tokens associated with a consented LinkAuthIntent: an access token and, when issued, a refresh token. After the user completes authorisation, your back end can call this endpoint when it needs the tokens or securely store them for reuse. To limit credential exposure, we recommend that you don’t send OAuth access tokens or refresh tokens to the client. Use the access token (for example, in the Stripe-OAuth-Token header) in subsequent on-ramp API requests for that user.

// Response
{
 "access_token": "liwltoken_xxx",
 "expires_in": 3600,
 "token_type": "Bearer",
 "scope": "kyc.status:read crypto:ramp",
 "refresh": {
 "refresh_token": "liwlrefresh_xxx",
 "expires_in": 7776000
 }
}

Parameters

ParameterTypeDescription
idstring (required)The Link Auth Intent id (for example, lai_xxx).

Returns

FieldTypeDescription
access_tokenstringOAuth access token. Send it on subsequent API requests for this user (for example, in the Stripe-OAuth-Token header).
token_typestringToken type. Always Bearer.
expires_inintegerSeconds until the access token expires.
scopestringThe OAuth scopes granted, separated by spaces.
refreshobject (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.
refresh.refresh_tokenstringOAuth refresh token. Store it securely and use it to obtain new access tokens. See Refresh an Access Token.
refresh.expires_inintegerSeconds until the refresh token expires.

Errors

HTTP statusCause
403Feature not available.
403LinkAuthIntent hasn’t been consented by the user.
403Invalid or missing API key.
404LinkAuthIntent 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 API to obtain a new access token without requiring the user to re-authorise.

To obtain your the related setting, contact your Stripe account executive or solutions architect. Stripe provisions the credential as part of your onboarding.

// Response
{
 "access_token": "liwltoken_xxx",
 "refresh_token": "liwlrefresh_xxx",
 "token_type": "Bearer",
 "expires_in": 3600,
 "scope": "kyc.status:read,crypto:ramp"
}

Parameters

ParameterTypeDescription
grant_typestring (required)Must be refresh_token.
refresh_tokenstring (required)The refresh token previously obtained from the Retrieve Access Tokens API.
client_idstring (required)Your OAuth client ID provided by Link.
client_secretstring (required)Your OAuth client secret provided by Link.

Returns

FieldTypeDescription
access_tokenstringOAuth access token. Expires in 1 hour. Send it on subsequent API requests (for example, in the Stripe-OAuth-Token header).
refresh_tokenstringA new OAuth refresh token. Store it securely for obtaining new access tokens when the current one expires.
token_typestringToken type. Always Bearer.
expires_inintegerTTL in seconds (3600, that is, 1 hour).
scopestringThe OAuth scopes granted, separated by commas.

Listen to webhook events

Stripe sends a crypto.onramp_session.updated webhook every time the status of an onramp session changes after creation. Stripe doesn’t send an event when a new session is created. Configure webhooks in the Dashboard.

The SDK’s performCheckout callback covers the payment step synchronously on the client. Use the webhook on your server to track asynchronous fulfilment. In particular, the transition from fulfillment_processing to fulfillment_complete, which occurs after checkout resolves as the cryptocurrency delivery is confirmed on-chain.

The session status field progresses through these states: initialized > requires_payment > fulfillment_processing > fulfillment_complete. The session moves to rejected if the session is blocked.

The webhook payload uses the CryptoOnrampSession resource:

{
 "id": "evt_123",
 "object": "event",
 "data": {
 "object": {
 "id": "cos_0MYvv9589O8KAxCGPm84FhVR",
 "object": "crypto.onramp_session",
 "client_secret": "cos_0MYvv9589O8KAxCGPm84FhVR_secret_IGBYKVlTlnJL8UGxji48pKxBO00deNcBuVc",
 "created": 1675794575,
 "livemode": false,
 "status": "fulfillment_complete",
 "transaction_details": {
 "destination_currency": "eth",
 "destination_amount": null,
 "destination_network": "ethereum",
 "fees": null,
 "lock_wallet_address": false,
 "source_currency": "usd",
 "source_amount": null,
 "destination_currencies": ["eth"],
 "destination_networks": ["ethereum"],
 "transaction_id": null,
 "wallet_address": null,
 "wallet_addresses": {
 "bitcoin": null,
 "ethereum": "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2",
 "polygon": null,
 "solana": null,
 "stellar": null,
 "destination_tags": null
 }
 }
 }
 }
}

Handle identity challenges

Risk rules can require a customer to complete an identity challenge even when their transaction amount is within the limit for their current verification tier. In this case, the Create a CryptoOnrampSession API returns an HTTP 400 error with the crypto_onramp_missing_document_verification code.

Retrieve the CryptoCustomer and check their current verification tier. If the customer is only verified at L0, first prompt them to complete the KYC collection step by submitting their date of birth and Social Security number.

After the customer reaches L1, prompt them to complete the identity verification step, where they provide a photo ID and selfie. After they complete the flow, retrieve the CryptoCustomer again and check the L2 verification status. When the status is verified, retry creating the CryptoOnrampSession.

Completing the identity challenge also verifies the customer at L2, so their future transactions use the L2 transaction limits.

Last verified 2026-09-27

Is this helpful?