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 provides step-by-step instructions for you to build your integration. Use this when you need full control over the on-ramp flow, want to understand each API or want to customise the flow for your app. Alternatively, see the quickstart for a minimal example that shows the full flow, or explore the example app for a complete React Native project that demonstrates the full crypto purchase flow.
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:
- Submit your application .
- 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`,
});
Mobile SDK configuration
Step 1: Install the Stripe React Native SDK
For Expo projects, run the following to automatically install the version compatible with your Expo SDK:
npx expo install @stripe/stripe-react-native
For bare React Native projects, follow the installation instructions in the the related setting. See the requirements section for the minimum compatible Expo SDK, React Native, iOS, and Android versions.
Step 2: Add the on-ramp dependency Client-side
By default, the on-ramp dependency isn’t included in the Stripe React Native SDK to reduce bundle size. Include it as follows, depending on your platform.
# android/gradle.properties
StripeSdk_includeOnramp=true
# ios/Podfile – add pod
pod 'stripe-react-native/Onramp', path: '../node_modules/@stripe/stripe-react-native'
Step 3: Use StripeProvider Client-side
Wrap your app with StripeProvider at a high level so Stripe functionality is available throughout your component tree. Key properties:
- publishableKey : Your Stripe publishable key.
- merchantIdentifier : Your Apple Merchant ID (required for Apple Pay).
- urlScheme : Required for return URLs in authentication flows.
You need this component to initialise the Stripe SDK in your React Native application before using payment-related features.
import { StripeProvider } from '@stripe/stripe-react-native';
function App() {
return (
<StripeProvider
publishableKey="pk_test_..."
merchantIdentifier="merchant.identifier"
urlScheme="your-url-scheme"
>
{/* Your app components */}
</StripeProvider>
);
}
Step 4: Configure the on-ramp SDK Client-side
Before you can successfully call any on-ramp APIs, you need to configure the SDK using the configure method. It’s provided by the useOnramp() hook. The configure method takes an instance of Onramp.Configuration to customise your business display name and lightly customise elements in Stripe-provided interfaces, such as the user’s wallet, one-time passcode authorisation, and identity verification UI.
import { useOnramp } from '@stripe/stripe-react-native';
function OnrampComponent() {
const { configure } = useOnramp();
React.useEffect(() => {
const setupOnramp = async () => {
const result = await configure({
merchantDisplayName: 'My Crypto App',
appearance: {
lightColors: {
primary: '#2d22a1',
contentOnPrimary: '#ffffff',
borderSelected: '#07b8b8'
},
darkColors: {
primary: '#800080',
contentOnPrimary: '#ffffff',
borderSelected: '#526f3e'
},
style: 'ALWAYS_DARK',
primaryButton: { cornerRadius: 8, height: 48 }
}
});
if (result.error) {
console.error('Configuration failed:', result.error.message);
}
};
setupOnramp();
}, [configure]);
return null;
}
Authentication
Step 1: Check for a Link account Client-side
The customer must have a Link account to use the on-ramp APIs. Use hasLinkAccount to determine if the customer’s email is associated with an existing Link account. See the HasLinkAccountResult for the return type and the OnrampError for the error type.
- If they have an account, proceed to Authorise .
- If they don’t, use Register a new Link user , then proceed to Authorise .
const { hasLinkAccount, registerLinkUser, authorize } = useOnramp();
const linkResult = await hasLinkAccount('user@example.com');
if (linkResult.error) return;
if (linkResult.hasLinkAccount) {
// Proceed to authorization.
} else {
// Register the user first (see next step).
}
View example
Step 2: Register a new Link user (if needed) Client-side
If the customer doesn’t have a Link account, use registerLinkUser to create one with the customer information collected from your UI. Upon successful account creation, proceed to Authorise. See the RegisterLinkUserResult for the return type and the OnrampError for the error type.
const userInfo = {
email: 'user@example.com', // Standard email format, max 800 characters.
phone: '+12125551234', // E.164 formatted phone number.
country: 'US',
fullName: 'John Smith',
};
const registerResult = await registerLinkUser(userInfo);
if (registerResult.error) return;
if (registerResult.customerId) {
// Proceed to authorization.
}
Step 3: Authorise Client-side Server-side
The primary method of authentication is through two-factor authorisation.
Create a LinkAuthIntent
A LinkAuthIntent tracks 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. LinkAuthIntent will return a authIntentId, which your back end can share with your client application.
// createAuthIntent is a client-side function you must implement.
// Call your back end to create a LinkAuthIntent using the API.
const authIntentResponse = await createAuthIntent(
email,
// This is your OAUTH_CLIENT_ID, which identifies your application in the Link OAuth flow.
authToken,
'kyc.status:read,crypto:ramp'
);
const authIntentId = authIntentResponse.data.authIntentId;
User consents
The client calls authorize with the authIntentId to complete consent. The authorize SDK looks up and verifies the user’s Link session, shows them what your app is requesting (the OAuth scopes) on a consent screen or inline on the OTP screen, and collects their approval.
The SDK then sends that consent to Stripe so your backend can exchange the intent for an access token and finish the flow. The result includes a customerId that must be used for all subsequent on-ramp API calls. See the AuthoriseResult for the return type and the OnrampError for the error type.
const result = await authorize(authIntentId);
if (result?.error) {
// Error occurred. Show result.error.message and stop.
} else if (result?.status === 'Consented' && result.customerId) {
// User consented. Store the authIntentId securely for future visits.
// Call your backend to retrieve an OAuth access token, then proceed to identity flow.
} else if (result?.status === 'Denied') {
// User denied. Explain they need to consent to continue, or let them try again.
} else {
// User canceled. Dismiss and let them try again.
}
Request access tokens
If the result is Consented, your back end calls the Retrieve Access Tokens API to request an OAuth access token and, when issued, a refresh token. Your back end can retrieve an OAuth access token when needed or securely store the returned tokens 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 on subsequent on-ramp API requests (for example, in the Stripe-OAuth-Token header).
Use Seamless Sign-In for a returning customer Client-side Server-side
To reduce friction for a returning Customer, you can skip the OTP dialog by storing the authIntentId after a successful authorise call. On the next visit, send the authIntentId to your back end, or use the Customer’s authenticated app session to look it up. Your back end uses the authIntentId to retrieve the OAuth tokens associated with the previous authorisation, or loads tokens Stored after the original authorisation. If the access token expired, use the refresh token to obtain a new one. Then call POST /the relevant part of the product to create a linkAuthTokenClientSecret. This Request requires the OAuth access token and your Stripe Secret key – it doesn’t accept or require the authIntentId. Pass the linkAuthTokenClientSecret to authenticateUserWithToken from useOnramp to sign in without Customer interaction.
Note
Seamless sign-in requires your Stripe account to be enabled for this feature. Work with your Stripe account executive or solutions architect to enable seamless sign-in for your account.
If authentication fails, for example because the linkAuthTokenClientSecret expired, clear the Stored authIntentId and fall back to the standard hasLinkAccount and authorise flow.
const { authenticateUserWithToken } = useOnramp();
const result = await authenticateUserWithToken(linkAuthTokenClientSecret);
if (result?.error) {
// Seamless sign-in failed. Clear the stored authIntentId and fall back to authorize.
} else {
// Customer authenticated. Proceed to the onramp session.
}
For a complete Integration guide including how to Request the required scope, store the authIntentId, retrieve an OAuth access token and handle failures, see Add seamless sign-in to your React Native on-ramp Integration.
Identity
For details on KYC tiers and their identity requirements, see the KYC integration guide. For EU-specific identity requirements, see the EU KYC integration guide.
Step 1: Check if KYC collection is needed Server-side
Your back end calls the Retrieve a CryptoCustomer API with the customerId. Inspect the response verifications array. If it includes an entry with type kyc_verified and status not_started, proceed to Collect KYC.
Select a language
Node.js
curl
No results
const customer = await stripe.crypto.customers.retrieve(
customerId,
{},
{headers: {'Stripe-OAuth-Token': accessToken}}
);
View example
Step 2: Collect KYC (if needed) Client-side
If the customer needs KYC verification, your client calls attachKycInfo to collect and submit user KYC data. Present your own interface to the user to collect this KYC information. See KycInfo for the full parameter type and the OnrampError for the error type.
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.
Step 3: Verify KYC and update address (if needed) Client-side
When a user already has KYC information, use presentKycInfoVerification to let them review and update it. This method presents a Stripe-hosted screen showing the user’s existing KYC data. See the VerifyKycResult for the return type and the OnrampError for the error type.
Note
Only address updates are currently supported. Other KYC fields can’t be modified.
The typical flow is:
- Call presentKycInfoVerification(null) to show existing KYC data. The SDK returns Confirmed if the user accepts, or UpdateAddress if they want to edit their address.
- If the result status is UpdateAddress , show your address form to collect a new address.
- Call presentKycInfoVerification(updatedAddress) with the new address to submit and verify it.
- If the result status is Confirmed , the address is updated.
import { useOnramp } from '@stripe/stripe-react-native';
function VerifyKYCComponent() {
const { presentKycInfoVerification } = useOnramp();
const handlePresentKycVerification = async () => {
// Step 1: Show existing KYC data for review.
const reviewResult = await presentKycInfoVerification(null);
if (reviewResult?.error) {
// Verification failed or user canceled.
return;
}
if (reviewResult?.status === 'Confirmed') {
// User confirmed existing data. Proceed to identity verification (if needed) or payment flow.
return;
}
if (reviewResult?.status === 'UpdateAddress') {
// Step 2: User wants to edit their address. Show your address form and collect input.
const updatedAddress = await collectAddressFromUser();
// Step 3: Submit the updated address.
const updateResult = await presentKycInfoVerification({
line1: updatedAddress.line1,
line2: updatedAddress.line2,
city: updatedAddress.city,
state: updatedAddress.state,
postalCode: updatedAddress.postalCode,
country: updatedAddress.country,
});
if (updateResult?.error) {
// Update failed. Show updateResult.error.message and let the user retry.
} else if (updateResult?.status === 'Confirmed') {
// Address updated. Proceed to identity verification (if needed) or payment flow.
}
}
};
return <Button title="Verify KYC" onPress={handlePresentKycVerification} />;
}
Step 4: Verify identity (if needed) Client-side
Some users must verify their identity before continuing with checkout. When required, use the verifyIdentity method. It presents a Stripe-hosted flow where the user uploads an identity document and a selfie.
If the user 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 user re-upload their identity documents.
Verification is asynchronous. After the user completes the flow, your back-end can call the Retrieve a CryptoCustomer API and inspect the verifications array to see the results.
On Android, the Stripe Identity SDK requires the app’s theme to extend Theme.MaterialComponents. For example, Expo defaults to Theme.AppCompat, so you need a config plugin to change the theme.
import { useOnramp } from '@stripe/stripe-react-native';
function VerifyIdentityComponent() {
const { verifyIdentity } = useOnramp();
const handleVerifyIdentity = async () => {
const result = await verifyIdentity();
if (result?.error?.code === 'Canceled') {
// User canceled. Dismiss and let them try again.
} else if (result?.error) {
// Verification failed. Show result.error.message and let the user retry.
} else {
// Identity verified. Proceed to payment flow (register wallet, collect payment method).
}
};
return <Button title="Verify Identity" onPress={handleVerifyIdentity} />;
}
Payment
Step 1: Register a crypto wallet Client-side Server-side
A ConsumerWallet must be registered before you can create a PaymentToken. This validates that the address is valid for the given network. Your back end can call the List ConsumerWallets API to see whether the user already has wallets on file.
If the list is empty or the user wants to add another address, have the client call registerWalletAddress with the user’s chosen address and network. Replace the address and network with user-provided values. You can use a previously registered wallet in future sessions. For all valid network values, see Network.
Select a language
Node.js
curl
No results
const wallets = await stripe.crypto.customers.listConsumerWallets(
customerId,
{},
{headers: {'Stripe-OAuth-Token': accessToken}}
);
Step 2: Collect a payment method Client-side Server-side
You must first collect a payment method before a transaction can occur. Your back end can call the List PaymentTokens API to see which payment methods the user already has. If the list is empty or the user wants to use a different method, have the client call collectPaymentMethod.
Pass Card (card only), BankAccount (bank account only) or CardAndBankAccount (lets the user choose either) to collectPaymentMethod. For Apple Pay or Google Pay, pass PlatformPay with platform-specific params. For Card and Bank Account, collectPaymentMethod presents Stripe’s wallet user interface, which lists existing stored payment methods, allows the user to add new ones, and select one. Upon successful payment method selection, it returns an instance of CollectPaymentMethodResult, which includes a displayData property (icon, label, sublabel) that you can use in your UI to show the selected payment method.
Regional considerations EU
EU sessions support card payments. Pass '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}}
);
Collect Apple Pay
To collect Apple Pay, first check isPlatformPaySupported in useStripe(). See Apple Pay on React Native. If the user chooses Apple Pay, pass an instance of PlatformPay.PaymentMethodParams into collectPaymentMethod.
Collect Google Pay
To collect Google Pay, first check isPlatformPaySupported in useStripe(). See Google Pay on React Native. If the user chooses Google Pay, pass an instance of PlatformPay.PaymentMethodParams into collectPaymentMethod.
Step 3: Create a payment token Client-side
Create a PaymentToken by calling createCryptoPaymentToken. Use the returned token when creating the CryptoOnrampSession.
import { useOnramp } from '@stripe/stripe-react-native';
function CreatePaymentTokenComponent() {
const { createCryptoPaymentToken } = useOnramp();
const handleCreateCryptoPaymentToken = async () => {
const result = await createCryptoPaymentToken();
if (result?.error) {
// Token creation failed. Show result.error.message and let the user retry.
} else {
// Token created. Pass result.cryptoPaymentToken to createOnrampSession.
}
};
return <Button title="Create Payment Token" onPress={handleCreateCryptoPaymentToken} />;
}
Step 4: Create a crypto on-ramp session Server-side
From your UI, determine the amount, source currency (for example, usd), destination currency (for example, usdc) and network. Your back end calls the Create a CryptoOnrampSession API to create a CryptoOnrampSession. The Stripe React Native SDK doesn’t provide APIs for creating a crypto onramp session. It happens on your back end. The example below shows how a client application might call your back end. Adapt it to your use case.
Regional considerations EU
For EU sessions, set source_currency to eur.
function CreateOnrampSessionComponent() {
const handleCreateOnrampSession = async () => {
// createOnrampSession is a client-side function you must implement.
// Call your back end to create a CryptoOnrampSession using the API.
const result = await createOnrampSession({
uiMode: "headless",
cryptoCustomerId,
cryptoPaymentToken,
sourceAmount: 100.0, // Pass source_amount OR destination_amount, not both.
sourceCurrency: "usd",
destinationCurrency: "usdc",
destinationNetwork: Onramp.CryptoNetwork.bitcoin, // Singular: pins the transaction to this network.
destinationNetworks: [Onramp.CryptoNetwork.bitcoin], // Array: must be set when walletAddress is set.
walletAddress,
customerIpAddress,
});
if (result.success) {
const sessionId = result.data.id;
// Call performCheckout with sessionId.
} else {
// Creation failed. Show error and let the user retry.
}
};
return <Button title="Create onramp Session" onPress={handleCreateOnrampSession} />;
}
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.
Step 5: Perform checkout Client-side Server-side
Call performCheckout to run the checkout flow for a crypto on ramp session. It presents a UI for any required actions such as 3DS.
You must implement a client-side callback that the SDK invokes 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.
Optional Display a price estimate
React Native SDK
| SDK method | Presents a UI | What the user sees |
|---|---|---|
authorise(authIntentId) | Yes | Link consent screen (or consent inline on OTP screen). |
attachKycInfo | Optional | Initial KYC submission only. Collect KYC in your own UI and pass data in. Errors if the user is already verified. |
presentKycInfoVerification | Yes | Review KYC and update addresses for verified users. Pass null to review existing data, or an address object to update. |
verifyIdentity | Yes | The Stripe-hosted flow (document + selfie). Can be called again to re-upload identity documents. |
collectPaymentMethod (Card / BankAccount) | Yes | The Stripe wallet UI: list saved methods, add new, choose one. |
performCheckout | Maybe | Only when needed (for example, 3DS). |
registerWalletAddress | No | No UI. You pass the address and network. |
Troubleshooting
App attestation is missing or device can’t use native Link
The Embedded Components on-ramp SDKs require device attestation to verify that API requests come from a legitimate app. To troubleshoot app attestation errors, check the following:
- Confirm your app is registered as a trusted application with Stripe. Contact your Stripe account executive or solutions architect to register your app. Stripe requires registration for both sandbox and live mode. Make sure the bundle identifier on iOS or package name on Android matches the value registered with Stripe.
- Confirm your app includes the App Attest entitlement on iOS. Your app must include the
com.apple.developer.devicecheck.appattest-environmententitlement. - Confirm you’re running on a supported device and using a supported distribution method. We support simulators in sandboxes, but not in live mode. In live mode, use a physical device. On iOS, you can run from Xcode on a physical device, distribute through TestFlight, or publish to the App Store. When running from Xcode, set
com.apple.developer.devicecheck.appattest-environmenttoproductionin your entitlements file, and delete and reinstall the app if you previously used thedevelopmentenvironment. - If you’re testing on an Android emulator and see the error
Native Link is not available, confirm that your emulator uses a system image that includes Google APIs or Google Play. Standard emulator images without Google APIs don’t support the app attestation required by the SDK.
Unrecognised request URL
If your API calls return a 404 with an invalid_request_error and the message Unrecognised request URL, your account might not be enrolled in the private preview or might be missing one or more required feature gates.
Contact your Stripe account executive or solutions architect to confirm that your account has access to all required feature gates for the Embedded Components on-ramp.
Supported networks and currencies
Live mode
| Currency | Network | Address |
|---|---|---|
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)
| Currency | Network | Supported 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:
| 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 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
Verifiedas 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
verifyIdentitySDK 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: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 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
| 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 the related setting 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 on-ramp). 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
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
| 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. |
scope | string | The OAuth scopes granted, separated by spaces. |
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. |
refresh.refresh_token | string | OAuth refresh token. Store it securely and use it to obtain new access tokens. See 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 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
| 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 |
|---|---|---|
access_token | string | OAuth access token. Expires in 1 hour. Send it on subsequent API requests (for example, in the Stripe-OAuth-Token header). |
refresh_token | string | A new OAuth refresh token. Store it securely for obtaining new access tokens when the current one expires. |
token_type | string | Token type. Always Bearer. |
expires_in | integer | TTL in seconds (3600, that is, 1 hour). |
scope | string | The 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.