# Integrate the Embedded Components onramp Step-by-step integration guide for the Embedded Components onramp. # Web This guide provides step-by-step instructions for you to build your integration. Use this when you need full control over the onramp flow, want to understand each API, or want to customize the flow for your website. Alternatively, see the [quickstart](https://docs.stripe.com/crypto/onramp/embedded-components-quickstart.md) for a minimal example that shows the full flow. ## 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). 2. [Sign up to join the waitlist](https://docs.stripe.com/crypto/onramp.md#sign-up). 3. 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). - 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). ## SDK configuration ### Step 1: Install the Stripe Crypto SDK ```shell npm install @stripe/crypto ``` Or with yarn: ```shell yarn add @stripe/crypto ``` ### Step 2: Load and initialize the SDK (Client-side) Call `loadCryptoOnrampAndInitialize` with your publishable key and optional configuration to initialize the SDK. You can customize the appearance (for example, colors) so the minimal Stripe UI matches your website. ```javascript import { loadCryptoOnrampAndInitialize } from '@stripe/crypto'; const onramp = await loadCryptoOnrampAndInitialize('pk_test_...', { theme: 'stripe', }); ``` ## Authentication ### Step 1: Check for a Link account (Server-side) The customer must have a [Link](https://link.com) account to use the onramp APIs. Create a `LinkAuthIntent` to determine if the customer’s email is associated with an existing Link account. - If they have an account, proceed to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-authorize). - If they don’t, use [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 proceed to [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-authorize). A `LinkAuthIntent` tracks scopes of the OAuth requests and the status of user consent. Your back end calls the [Create a LinkAuthIntent](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#create-a-linkauthintent) API with your `OAUTH_CLIENT_ID` and the onramp OAuth scopes. LinkAuthIntent returns an `authIntentId`, which your back end can share with your client application. #### Client-side ```javascript const response = await fetch("/create-link-auth-intent", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email }), }); if (response.ok) { // Successfully created a LinkAuthIntent, proceed to Authorize } if (response.status === 404) { // Request to POST https://login.link.com/v1/link_auth_intent returned a 404 response // There isn't an existing Link account associated with the email // Handle registering a new Link User } ``` #### Server-side ```javascript app.post('/create-link-auth-intent', async (req, res) => { const { email } = req.body; const linkRes = await fetch('https://login.link.com/v1/link_auth_intent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, }, body: JSON.stringify({ email, oauth_scopes: process.env.LINK_OAUTH_SCOPES, oauth_client_id: process.env.LINK_CLIENT_ID, }), }); const data = await linkRes.json(); if (data.id) { res.json({ authIntentId: data.id }); } else { res.status(linkRes.status).json(data); } }); ``` ### 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 [Authorize](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-authorize). > The `country` field determines which KYC requirements apply. Set it to the customer’s country of residence. ```javascript const userInfo = { email: 'user@example.com', // Standard email format, max 800 characters. phone: '+12125551234', // E.164 formatted phone number. country: 'US', // ISO 3166-1 alpha-2 code that's used to determine regional eligibility and KYC requirements. fullName: 'John Smith', }; const registerResult = await onramp.registerLinkUser(userInfo); if (registerResult.created) { // Proceed to authorization. } ``` ### Step 3: Authorize (Client-side) Call the `authenticate` SDK method, passing the LinkAuthIntent Id and a callback to start the authentication flow. Your callback is called when the user completes authentication. If the user successfully authenticates, the callback parameter contains `crypto_customer_id`. The `crypto_customer_id` is a unique identifier for the user that you need to [Create Onramp Session](https://docs.stripe.com/api/crypto/onramp_sessions/create.md) later. `authenticate` returns an HTMLElement. Present this to the user to have them authenticate. We recommend presenting this to them as a modal. If authentication isn’t required, your callback is called immediately. Don’t present the HTMLElement to the user in this case. ```javascript const authenticationElement = await onramp.authenticate(linkAuthIntentId, async (result) => { if (result.result === 'success') { if (result.crypto_customer_id) { // The user successfully authenticated // persist their crypto_customer_id in your backend, you will need it later to onramp } } else if (result.result === 'abandoned') { // The user cancelled. Dismiss and let them try again } else if(result.result === 'declined') { // The user declined the OAuth Consent Screen. Explain they need to consent to continue, or let them try again } }); // `authenticate` returns a Promise // Render this in your UI, we recommend presenting this in a modal document.getElementById('auth-container').replaceChildren(authenticationElement); ``` #### Request access tokens After the user authenticates, your back end calls the [Retrieve Access Tokens](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#retrieve-access-tokens) API to request access tokens. Store the access token and use it on all subsequent onramp API requests (for example, in the `Stripe-OAuth-Token` header). ```javascript async function exchangeTokens(authIntentId) { const res = await fetch(`https://login.link.com/v1/link_auth_intent/${authIntentId}/tokens`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}` }, }); const data = await res.json(); if (!data.access_token) throw new Error('Token exchange failed'); const oauthToken = data.access_token; return oauthToken; } ``` ## Identity For details on KYC tiers and their identity requirements, see the [KYC integration guide](https://docs.stripe.com/crypto/onramp/kyc-integration-guide.md). For EU-specific identity requirements, see the [EU KYC integration guide](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md). ### Step 1: Check if KYC collection is needed (Server-side) Your back end calls the [Retrieve a CryptoCustomer](https://docs.stripe.com/api/crypto/customers/retrieve.md) 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](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-collect-kyc-if-needed). ### View example ```javascript async function getCryptoCustomer(req, res) { const { id } = req.params; const response = await fetch( `https://api.stripe.com/v1/crypto/customers/${id}`, { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, } ); const customer = await response.json(); const verifications = customer.verifications ?? []; const kycVerified = verifications.find((v) => v.name === 'kyc_verified'); const idDocVerified = verifications.find( (v) => v.name === 'id_document_verified' ); res.json({ customerId: customer.id, providedFields: customer.provided_fields ?? [], kycStatus: kycVerified?.status ?? 'not_started', idDocStatus: idDocVerified?.status ?? 'not_started', }); } ``` ### Step 2: Collect KYC (if needed) (Client-side) If the customer needs KYC verification, your client calls `submitKycInfo` to collect and submit user KYC data. Present your own interface to the user to collect this KYC information. > EU customers require additional fields such as `nationalities` and `birth_country`. See the [EU KYC integration guide](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md) for the complete list of required fields. ```javascript const kycInfo = { given_name: 'John', surname: 'Smith', id_number: { value: "000000000", // Full ID number — for US, only SSN is currently supported. type: 'us_ssn', }, date_of_birth: { // Object with numeric fields, not a date string. day: 1, // Day of month (1-31). month: 1, // Month of year (1-12). year: 1990, // Full 4-digit year. }, address: { line1: '123 Main St', line2: 'Apt 4B', city: 'San Francisco', state: 'CA', // 2-letter uppercase code; see https://en.wikipedia.org/wiki/ISO_3166-2:US postal_code: '94111', country: 'US', // 2-letter uppercase ISO code; see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 }, }; try { await onramp.submitKycInfo(kycInfo); } catch (e) { // Handle KYC Submission Errors } ``` ### Step 3: Verify identity (if needed) (Client-side) Some users must verify their identity before continuing with checkout. When required, use the `verifyDocuments` method. It presents a Stripe-hosted flow where the user uploads an identity document and a selfie. Verification is asynchronous. After the user completes the flow, your back end can call the [Retrieve a CryptoCustomer](https://docs.stripe.com/api/crypto/customers/retrieve.md) API and inspect the verifications array to see the results. ```javascript const result = await onramp.verifyDocuments(); if (result === 'abandoned') { // User canceled. Dismiss and let them try again. } else { // Identity verified. Proceed to payment flow (register wallet, collect payment method). } ``` ## Payment ### Step 1: Register a crypto wallet (Client-side) (Server-side) All wallet addresses must be registered to the user’s account before you can onramp to it. Your back end can call the [List ConsumerWallets](https://docs.stripe.com/api/crypto/consumer_wallets/list.md) 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. You can reuse a previously registered wallet in future sessions. For all valid network values, see [Network](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#supported-networks-and-currencies). #### List ConsumerWallets ```javascript const response = await fetch( `https://api.stripe.com/v1/crypto/customers/${req.params.id}/crypto_consumer_wallets`, { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, } ); const data = await response.json(); ``` #### Register a wallet ```javascript try { const result = await onramp.registerWalletAddress('3CgQg…', 'solana'); } catch { // Registration failed } ``` ### 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](https://docs.stripe.com/api/crypto/payment_tokens/list.md) 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`. `payment_method_types` accepts `'card'` (credit/debit card) and `'us_bank_account'` (US bank account via ACH). Pass one or both values depending on the payment methods you want to offer. `collectPaymentMethod` presents the Stripe Payment Element for users to enter their payment details. After collecting their payment method, retrieve the Crypto Payment Token from your callback and use its ID to create the Onramp Session. > EU sessions support card payments. Set `payment_method_types` to `['card']`. Bank account payment methods aren’t supported for EU transactions. #### List PaymentTokens ```javascript const response = await fetch( `https://api.stripe.com/v1/crypto/customers/${req.params.id}/payment_tokens`, { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, } ); const data = await response.json(); ``` #### Collect a payment method ```javascript try { // payment_method_types accepts 'card', 'us_bank_account', or both. // For EU sessions, pass ['card'] only — bank accounts aren't supported. const result = await onramp.collectPaymentMethod({payment_method_types: ['card', 'us_bank_account'], wallets: {applePay: 'auto', googlePay: 'auto'}}, (result) => { const cryptoPaymentToken = result.cryptoPaymentToken; // Use this token when creating an Onramp Session }); } catch { // Payment collection failed } ``` ### Step 3: Create a crypto onramp 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](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 example below shows how a client application might call your back end. Adapt it to your use case. > For EU sessions, set `source_currency` to `eur`. #### Client-side ```javascript // 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: 'solana', // Singular: pins the transaction to this network. destinationNetworks: ['solana'], // 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. } ``` #### Server-side ```javascript app.post('/create-onramp-session', async (req, res) => { const { authIntentId, crypto_customer_id, payment_token, source_amount, source_currency, destination_currency, destination_network, wallet_address, } = req.body; const params = new URLSearchParams({ ui_mode: 'headless', crypto_customer_id, payment_token, source_amount: String(source_amount), source_currency, destination_currency, 'destination_currencies[]': destination_currency, destination_network, 'destination_networks[]': destination_network, wallet_address, customer_ip_address: req.socket.remoteAddress, }); const stripeRes = await fetch('https://api.stripe.com/v1/crypto/onramp_sessions', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, body: params, }); const data = await stripeRes.json(); if (data.id) { res.json({ id: data.id, quote_expires_at: data.transaction_details?.quote_expiration }); } else { res.status(stripeRes.status).json(data); } }); ``` ### Step 4: Perform checkout (Client-side) (Server-side) Call `performCheckout` to run the checkout flow for a crypto onramp session. It handles any required actions such as 3DS in the browser. 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](https://docs.stripe.com/api/crypto/onramp_sessions/checkout.md) 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. > 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 finalized until `performCheckout` returns a successful result. For users paying with ACH, you also need to pass `mandate_data` and collect the user’s ip address and user agent. #### Client-side ```javascript const result = await onramp.performCheckout(sessionId, async (onrampSessionId) => { // Call your back end, which calls POST /v1/crypto/onramp_sessions/{sessionId}/checkout // with the session ID, and return the client_secret from the response. const { client_secret } = await fetch(`/checkout/${onrampSessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }).then(r => r.json()); return client_secret; }); if (result.successful) { // Purchase complete. Show success and that crypto was sent to their wallet. } ``` #### Server-side ```javascript app.post('/checkout/:sessionId', async (req, res) => { const stripeRes = await fetch( `https://api.stripe.com/v1/crypto/onramp_sessions/${req.params.sessionId}/checkout`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, // pass mandate_data if the user is paying with ACH body: new URLSearchParams({ 'mandate_data[customer_acceptance][type]': 'online', 'mandate_data[customer_acceptance][accepted_at]': String(Math.floor(Date.now() / 1000)), 'mandate_data[customer_acceptance][online][ip_address]': req.ip || req.socket.remoteAddress, 'mandate_data[customer_acceptance][online][user_agent]': req.headers['user-agent'], }), } ); const data = await stripeRes.json(); res.status(stripeRes.status).json({client_secret: data.client_secret}); }); ``` When the API returns 200 or 202 but the purchase isn’t done, 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 with `submitKycInfo`. Then call checkout again. | | `missing_document_verification` | Identity document verification is required. | Have the user complete verification with `verifyDocuments`. Then call checkout again. | | `charged_with_expired_quote` | Quote expired. | Call [Refresh a Quote](https://docs.stripe.com/api/crypto/onramp_sessions/quote.md) API, then call checkout again. | | `quote_rate_drifted` | The exchange rate changed since the quote was locked. | Create a new session with an updated quote. | | `transaction_limit_reached` | User’s limit exceeded. | Display an error message. | | `location_not_supported` | User’s location isn’t supported. | Show that the service isn’t available in their region. | | `transaction_failed` | Generic failure. | 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. | The following pseudocode shows how to handle these cases in a retry loop: ```javascript async function handleCheckout() { // Step 1: Create a new session on your backend before starting checkout. let sessionId = await createSessionOnBackend(); for (let attempt = 0; attempt < 5; attempt++) { try { const result = await onramp.performCheckout(sessionId, async (onrampSessionId: string) => { // Call your back end, which calls POST /v1/crypto/onramp_sessions/{sessionId}/checkout // with the session ID, and return the client_secret from the response. const { client_secret } = await fetch(`/checkout/${onrampSessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }).then(r => r.json()); return client_secret; }); if (result.successful) { // Purchase complete. return; } } catch { // Step 2: Inspect last_error from the session to decide the next action. const session = await fetchSessionFromBackend(sessionId); const lastError = session.transaction_details?.last_error; if (lastError === 'missing_kyc') { // Prompt user to provide KYC info, then retry checkout on the same session. await onramp.submitKycInfo({ given_name, surname, id_number, date_of_birth, address }); } else if (lastError === 'missing_document_verification') { // Prompt user to complete identity verification, then retry on the same session. await onramp.verifyDocuments(); } else if (lastError === 'missing_consumer_wallet') { // Prompt user to register a wallet, then retry on the same session. await onramp.registerWalletAddress(walletAddress, network); } else if (lastError === 'charged_with_expired_quote') { // Quote expired. Refresh the quote on your backend, then retry checkout on the same session. await refreshQuoteOnBackend(sessionId); } else if (lastError === 'quote_rate_drifted') { // Exchange rate moved too far. Create a new session with a fresh quote. sessionId = await createSessionOnBackend(); } else { // Terminal errors: transaction_limit_reached, location_not_supported, transaction_failed. // Do not retry. Show an appropriate error message. showError(lastError); return; } } } } ``` ## SDK reference ### loadCryptoOnrampAndInitialize(publishableKey, options) Loads and initializes the SDK. Returns a configured `OnrampCoordinator` instance. | Parameter | Type | Required | Description | | ---------------- | ------------------------- | -------- | ---------------------------- | | `publishableKey` | string | Yes | Your Stripe publishable key. | | `options` | `CryptoOnrampInitOptions` | No | SDK configuration options. | **`CryptoOnrampInitOptions`** | Property | Type | Required | Description | | -------- | --------------------------------- | -------- | --------------------------------------------- | | `theme` | `'stripe'` | `'night'` | `'flat'` | Yes | Visual theme for Stripe-provided UI elements. | Returns: `Promise` ### OnrampCoordinator #### registerLinkUser(email, phone, country, fullName?) Creates a new Link account for the user. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------------------------- | | `email` | string | Yes | The user’s email address. | | `phone` | string | Yes | The user’s phone number in E.164 format, for example, `+12125551234`. | | `country` | string | Yes | Two-letter ISO country code, for example, `US`. | | `fullName` | string | No | The user’s full name. | Returns: `Promise` | Property | Type | Description | | --------- | ------- | ----------------------------------------- | | `created` | boolean | `true` if a new Link account was created. | #### authenticate(linkAuthIntentId, onCompletion) Presents the OTP consent screen. Calls `onCompletion` when the user completes, abandons, or declines the flow. | Parameter | Type | Required | Description | | ------------------ | ---------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `linkAuthIntentId` | string | Yes | The `id` returned from [Create a LinkAuthIntent](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#create-a-linkauthintent). | | `onCompletion` | `(result: AuthenticationResult) => void` | Yes | Callback invoked when the flow completes. | **`AuthenticationResult`** | Property | Type | Description | | -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------ | | `result` | `'success'` | `'abandoned'` | `'declined'` | Outcome of the authentication flow. | | `crypto_customer_id` | string | Present when `result` is `'success'`. Use for all subsequent onramp API calls. | Returns: `Promise` #### submitKycInfo(params) Submits KYC information for the user. | Parameter | Type | Required | Description | | --------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `params` | `KycInfo` | Yes | KYC data to submit. See [KycInfo](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#kyc-info). | Returns: `Promise` #### registerWalletAddress(walletAddress, network) Registers a wallet address for the user on the given network. | Parameter | Type | Required | Description | | --------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `walletAddress` | string | Yes | The user’s crypto wallet address. | | `network` | `CryptoNetwork` | Yes | The blockchain network for this wallet. See [CryptoNetwork](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#crypto-network). | Returns: `Promise` | Property | Type | Description | | ---------------- | -------------------------- | --------------------------------------------- | | `id` | string | Unique identifier for the wallet. | | `object` | `'crypto.consumer_wallet'` | Object type. Always `crypto.consumer_wallet`. | | `wallet_address` | string | The registered wallet address. | | `network` | string | The blockchain network. | #### deleteWalletAddress(walletId) Removes a previously registered wallet address. | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------- | | `walletId` | string | Yes | The `id` from a `CryptoConsumerWallet` object. | Returns: `Promise` #### collectPaymentMethod(options, onCompletion) Presents the Stripe wallet UI for selecting a payment method. | Parameter | Type | Required | Description | | -------------- | ------------------------------------------------------------ | -------- | -------------------------------------------------------- | | `options` | `CollectPaymentMethodOptions` | Yes | Payment method configuration. | | `onCompletion` | `(request: CollectPaymentMethodOnCompletionRequest) => void` | Yes | Callback invoked when the user selects a payment method. | **`CollectPaymentMethodOptions`** | Property | Type | Required | Description | | ---------------------- | -------------------- | -------- | -------------------------------------------------------------------------- | | `payment_method_types` | string[] | Yes | Allowed payment method types, for example, `['card']`. | | `wallets.applePay` | `'auto'` | `'never'` | Yes | Whether to show Apple Pay. `'auto'` shows it when the device supports it. | | `wallets.googlePay` | `'auto'` | `'never'` | Yes | Whether to show Google Pay. `'auto'` shows it when the device supports it. | **`CollectPaymentMethodOnCompletionRequest`** | Property | Type | Description | | -------------------- | ------ | ----------------------------------------------------------- | | `cryptoPaymentToken` | string | The payment token to pass when creating the onramp session. | Returns: `Promise` #### verifyDocuments() Presents the Stripe-hosted identity document verification flow. Returns: `Promise` | Property | Type | Description | | -------- | --------------------------- | ------------------------------------------ | | `result` | `'success'` | `'abandoned'` | Outcome of the document verification flow. | #### performCheckout(onrampSessionId, checkout) Runs the checkout flow for an onramp session, handling any required payment actions such as 3DS in the browser. | Parameter | Type | Required | Description | | ----------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `onrampSessionId` | string | Yes | The `id` of the `CryptoOnrampSession` to check out. | | `checkout` | `(onrampSessionId: string) => Promise` | Yes | Callback that calls your back end, which calls the onramp session checkout endpoint with the session ID, and returns the resulting `client_secret`. | Returns: `Promise` | Property | Type | Description | | ------------ | ------- | ------------------------------------------------ | | `successful` | boolean | `true` when the purchase completed successfully. | #### destroy() Destroys the `OnrampCoordinator` instance and cleans up all associated resources. Returns: `void` ### Types #### KycInfo | Property | Type | Required | Description | | --------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `given_name` | string | No | The user’s given name. | | `surname` | string | No | The user’s surname. | | `address` | `Address` | No | The user’s address. See [Address](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#address). | | `id_number` | `IdNumber` | No | Government-issued ID number. See [IdNumber](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#id-number). | | `date_of_birth` | `DateOfBirth` | No | The user’s date of birth. See [DateOfBirth](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#date-of-birth). | | `nationalities` | string[] | No | ISO country codes for the user’s nationalities, for example, `['US']`. | | `birth_country` | string | No | ISO country code of the user’s birth country. | | `birth_city` | string | No | The user’s city of birth. | #### Address | Property | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------- | | `country` | string | Yes | Two-letter ISO country code, for example, `US`. | | `city` | string | No | City, district, suburb, town, or village. | | `line1` | string | No | Address line 1 (street address or PO box). | | `line2` | string | No | Address line 2 (apartment, suite, unit, or building). | | `postal_code` | string | No | ZIP or postal code. | | `state` | string | No | State, county, province, or region. | | `town` | string | No | Town. | #### IdNumber | Property | Type | Required | Description | | -------- | ---------- | -------- | ------------------------------------------------------- | | `type` | `'us_ssn'` | Yes | ID number type. Currently only `'us_ssn'` is supported. | | `value` | string | Yes | The ID number value. | #### DateOfBirth | Property | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------ | | `day` | number | Yes | Day of the month (1–31). | | `month` | number | Yes | Month of the year (1–12). | | `year` | number | Yes | Full four-digit year, for example, `1990`. | #### CryptoNetwork | Value | Network | | -------------- | ----------- | | `'bitcoin'` | Bitcoin | | `'ethereum'` | Ethereum | | `'solana'` | Solana | | `'polygon'` | Polygon | | `'stellar'` | Stellar | | `'avalanche'` | Avalanche | | `'base'` | Base | | `'aptos'` | Aptos | | `'optimism'` | Optimism | | `'worldchain'` | World Chain | | `'xrpl'` | XRP Ledger | | `'sui'` | Sui | | `'tempo'` | Tempo | ## 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. |