# Integrate the React Native Embedded Components onramp

Step-by-step integration guide for the Embedded Components onramp.

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 app. Alternatively, see the [quickstart](https://docs.stripe.com/crypto/onramp/embedded-components-quickstart.md) for a minimal example that shows the full flow, or explore the [example app](https://github.com/stripe-samples/crypto-embedded-components-onramp) for a complete React Native project that demonstrates the full crypto purchase flow.

## Before you begin

- The Embedded Components onramp is only available in the US (excluding Hawaii).

- The Embedded Components API is in private preview. No API calls can succeed until onboarding is complete, including in a *sandbox* (A sandbox is an isolated test environment that allows you to test Stripe functionality in your account without affecting your live integration. Use sandboxes to safely experiment with new features and changes). To request access:

  1. [Submit your application](https://docs.stripe.com/crypto/onramp.md#submit-your-application).
  1. [Sign up to join the waitlist](https://docs.stripe.com/crypto/onramp.md#sign-up).
  1. Work with your Stripe account executive or solutions architect to complete the onboarding process before starting your integration. This includes, but isn’t limited to:
     - Confirm your account is enrolled in the required feature gates for the Embedded Components onramp APIs and Link OAuth APIs.
     - Confirm your app is registered as a trusted application. We require this before you can use the SDK, including for simulator testing.
     - Enable Link as a payment method in your [Dashboard](https://dashboard.stripe.com/settings/payment_methods).
     - Obtain your OAuth client ID and client secret, which are provisioned by Stripe and required for the [authentication flow](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#authentication).

- After onboarding is complete, obtain your secret and [publishable API keys](https://docs.stripe.com/keys.md#obtain-api-keys) from the [API keys page](https://dashboard.stripe.com/apikeys).

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

```shell
npx expo install @stripe/stripe-react-native
```

For bare React Native projects, follow the installation instructions in the [README](https://github.com/stripe/stripe-react-native?tab=readme-ov-file). See the [requirements section](https://github.com/stripe/stripe-react-native?tab=readme-ov-file#requirements) for the minimum compatible Expo SDK, React Native, iOS, and Android versions.

### Step 2: Add the onramp dependency (Client-side)

By default, the onramp dependency isn’t included in the Stripe React Native SDK to reduce bundle size. Include it as follows, depending on your platform.

#### Bare React Native App

```shell
# android/gradle.properties
StripeSdk_includeOnramp=true

# ios/Podfile – add pod
pod 'stripe-react-native/Onramp', path: '../node_modules/@stripe/stripe-react-native'
```

#### Expo

```javascript
// [Expo] Add `"includeOnramp": true` (default false)
{
  "expo": {
    ...
    "plugins": [
      [
        "@stripe/stripe-react-native",
        {
          "merchantIdentifier": string | string [],
          "enableGooglePay": boolean,
          "includeOnramp": boolean
        }
      ]
    ]
  }
}
```

```shell
# Add Expo BuildProperties (https://docs.expo.dev/versions/latest/sdk/build-properties/)
npx expo install expo-build-properties
```

If you’re testing on a physical device, install [expo-dev-client](https://docs.expo.dev/versions/latest/sdk/dev-client/) to avoid Metro bundler connection issues:

```shell
npx expo install expo-dev-client
```

### Step 3: Use StripeProvider (Client-side)

Wrap your app with [StripeProvider](https://stripe.dev/stripe-react-native/api-reference/functions/StripeProvider.html) 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 initialize the Stripe SDK in your React Native application before using payment-related features.

```javascript
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 onramp SDK (Client-side)

Before you can successfully call any onramp APIs, you need to configure the SDK using the `configure` method. It’s provided by the [useOnramp()](https://stripe.dev/stripe-react-native/api-reference/functions/useOnramp.html) hook. The `configure` method takes an instance of [Onramp.Configuration](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.Configuration.html) to customize your business display name and lightly customize elements in Stripe-provided interfaces, such as the user’s wallet, one-time passcode authorization, and identity verification UI.

```javascript
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](https://link.com) account to use the onramp APIs. Use `hasLinkAccount` to determine if the customer’s email is associated with an existing Link account. See the [HasLinkAccountResult](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.HasLinkAccountResult.html) for the return type and the [OnrampError](https://stripe.dev/stripe-react-native/api-reference/enums/OnrampError.html) for the error type.

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

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

```javascript
function AuthComponent() {
  const { hasLinkAccount, registerLinkUser } = useOnramp();

  const handleAuth = async () => {
    const linkResult = await hasLinkAccount('user@example.com');

    if (linkResult.error) {
      // Lookup failed. Show linkResult.error.message and stop.
      return;
    }

    if (linkResult.hasLinkAccount) {
      // User has Link account. Proceed to authorization.
    } else {
      const userInfo = {
        email: 'user@example.com',
        phone: '+12125551234',
        country: 'US',
        fullName: 'John Smith',
      };

      const registerResult = await registerLinkUser(userInfo);

      if (registerResult.error) {
        // Registration failed. Show registerResult.error.message and let the user fix the data.
      } else if (registerResult.customerId) {
        // User registered. Proceed to authorization.
      }
    }
  };

  return <Button title="Authenticate" onPress={handleAuth} />;
}
```

### 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-3-authorize). See the [RegisterLinkUserResult](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.RegisterLinkUserResult.html) for the return type and the [OnrampError](https://stripe.dev/stripe-react-native/api-reference/enums/OnrampError.html) for the error type.

```jsx
const userInfo = {
  email: 'user@example.com',
  phone: '+12125551234',
  country: 'US',
  fullName: 'John Smith',
};
const registerResult = await registerLinkUser(userInfo);
if (registerResult.error) return;
if (registerResult.customerId) {
  // Proceed to authorization.
}
```

### Step 3: Authorize (Client-side) (Server-side) 

The primary method of authentication is through two-factor authorization.

#### 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](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 will return a `authIntentId`, which your back end can share with your client application.

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

#### Client-side

```jsx
// 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;
```

#### Server-side

```shell
curl -X POST  https://login.link.com/v1/link_auth_intent \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -d email=user@example.com \
  -d oauth_client_id=$OAUTH_CLIENT_ID \
  -d oauth_scopes=kyc.status:read,crypto:ramp

# Response
{
  "id": "lai_xxxx",
  "expires_at": 1756238966
}
```

#### 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 onramp API calls. See the [AuthorizeResult](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.AuthorizeResult.html) for the return type and the [OnrampError](https://stripe.dev/stripe-react-native/api-reference/enums/OnrampError.html) for the error type.

```jsx
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. Call your backend to exchange for 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 backend 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).

```shell
# Request
curl -X POST https://login.link.com/v1/link_auth_intent/{authIntentId}/tokens \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY"

# Response
{
  "access_token": "liwltoken_xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh": {
    "refresh_token": "liwlrefresh_xxx",
    "expires_in": 7776000
  }
}
```

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

### Step 1: Check if KYC collection is needed (Server-side)

Your backend calls the [Retrieve a CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#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](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#step-2-collect-kyc-if-needed).

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId} \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

### View example

```javascript
async function getCryptoCustomer(req, res) {
  const { id } = req.params;
  const oauthToken = req.headers['stripe-oauth-token'];

  const response = await fetch(
    `https://api.stripe.com/v1/crypto/customers/${id}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
        'Stripe-OAuth-Token': oauthToken ?? '',
      },
    }
  );

  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 `attachKycInfo` to collect and submit user KYC data. Present your own interface to the user to collect this KYC information. See [KycInfo](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.KycInfo.html) for the full parameter type and the [OnrampError](https://stripe.dev/stripe-react-native/api-reference/enums/OnrampError.html) for the error type.

```jsx
import { useOnramp } from '@stripe/stripe-react-native';

function AttachKYCComponent() {
  const { attachKycInfo } = useOnramp();

  const handleAttachKycInfo = async () => {
    const kycInfo = {
      firstName: "FirstName",
      lastName: "LastName",
      idNumber: '000000000',   // Full ID number — for US, only SSN is currently supported.
      dateOfBirth: {           // 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',
        postalCode: '94111',
        country: 'US',
      },
    };

    const result = await attachKycInfo(kycInfo);

    if (result?.error) {
      // KYC failed to attach. Show result.error.message and let the user fix the data or retry.
    } else {
      // KYC attached. Proceed to identity verification (if needed) or payment flow.
    }
  };

  return <Button title="Attach KYC" onPress={handleAttachKycInfo} />;
}
```

### Step 3: Verify KYC (if needed) (Client-side)

When a user already has KYC information, use `presentKycInfoVerification` so the SDK can present a screen with the user’s existing KYC information for verification. Currently, we only support users updating their address. See the [VerifyKycResult](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.VerifyKycResult.html) for the return type and the [OnrampError](https://stripe.dev/stripe-react-native/api-reference/enums/OnrampError.html) for the error type.

```jsx
import { useOnramp } from '@stripe/stripe-react-native';

function VerifyKYCComponent() {
  const { presentKycInfoVerification } = useOnramp();

  const handlePresentKycVerification = async (updatedAddress: Address | null) => {
    const result = await presentKycInfoVerification(updatedAddress);

    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 if (result?.status === 'Confirmed') {
      // KYC verified. Proceed to identity verification (if needed) or payment flow.
    } else if (result?.status === 'UpdateAddress') {
      // User needs to update address. Show your address form, then call again with updatedAddress.
    } else {
      // User canceled. Dismiss and let them try again.
    }
  };

  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.

Verification is asynchronous. After the user completes the flow, your backend can call the [Retrieve a CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#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.

```jsx
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](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#consumerwallet) must be registered before you can create a [PaymentToken](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#paymenttoken). This validates that the address is valid for the given network. Your backend can call the [List ConsumerWallets](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#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. A previously registered wallet can be reused in future sessions. For all valid network values, see [Network](https://docs.stripe.com/crypto/onramp/embedded-components-react-native-integration-guide.md#network).

#### List ConsumerWallets

```shell
curl "https://api.stripe.com/v1/crypto/customers/{customerId}/crypto_consumer_wallets" \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

#### Register a wallet

```jsx
import { useOnramp } from '@stripe/stripe-react-native';
function RegisterWalletComponent() {
  const { registerWalletAddress } = useOnramp();

  const handleRegisterWallet = async () => {
    const result = await registerWalletAddress("0x000…", Onramp.CryptoNetwork.ethereum);

    if (result?.error) {
      // Registration failed. Show result.error.message and let the user retry with a different address.
    } else {
      // Wallet registered. Proceed to collect payment method.
    }
  };

  return <Button title="Register Wallet" onPress={handleRegisterWallet} />;
}
```

### Step 2: Collect a payment method (Client-side) (Server-side)

A payment method must first be collected before a transaction can occur. Your backend can call the [List PaymentTokens](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#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`.

Card, Bank Account, Apple Pay, and Google Pay are supported. 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](https://stripe.dev/stripe-react-native/api-reference/types/Onramp.CollectPaymentMethodResult.html), which includes a `displayData` property (icon, label, sublabel) that you can use in your UI to show the selected payment method.

#### List PaymentTokens

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId}/payment_tokens \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

#### Collect a payment method

```jsx
import { useOnramp } from '@stripe/stripe-react-native';

function CollectPaymentMethodComponent() {
  const { collectPaymentMethod } = useOnramp();

  const handleCollectPaymentMethod = async () => {
    const result = await collectPaymentMethod('Card');

    if (result?.error) {
      // Collection failed. Show result.error.message and let the user retry.
    } else if (result?.displayData) {
      // Payment method selected. Use result.displayData in your UI, then call createCryptoPaymentToken.
    } else {
      // User canceled. Dismiss and let them try again.
    }
  };

  return <Button title="Collect Payment Method" onPress={handleCollectPaymentMethod} />;
}
```

#### Collect Apple Pay

To collect Apple Pay, first check [isPlatformPaySupported](https://stripe.dev/stripe-react-native/api-reference/functions/isPlatformPaySupported.html) in [useStripe()](https://stripe.dev/stripe-react-native/api-reference/functions/useStripe.html). See [Apple Pay on React Native](https://docs.stripe.com/apple-pay.md?platform=react-native#check-if-apple-pay-supported). If the user chooses Apple Pay, pass an instance of [PlatformPay.PaymentMethodParams](https://stripe.dev/stripe-react-native/api-reference/types/PlatformPay.PaymentMethodParams.html) into `collectPaymentMethod`.

#### Collect Google Pay

To collect Google Pay, first check [isPlatformPaySupported](https://stripe.dev/stripe-react-native/api-reference/functions/isPlatformPaySupported.html) in [useStripe()](https://stripe.dev/stripe-react-native/api-reference/functions/useStripe.html). See [Google Pay on React Native](https://docs.stripe.com/google-pay.md?platform=react-native#react-native-create-enable-google-pay). If the user chooses Google Pay, pass an instance of [PlatformPay.PaymentMethodParams](https://stripe.dev/stripe-react-native/api-reference/types/PlatformPay.PaymentMethodParams.html) into `collectPaymentMethod`.

### Step 3: Create a payment token (Client-side)

Create a [PaymentToken](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#paymenttoken) by calling `createCryptoPaymentToken`. Use the returned token when creating the [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md).

```jsx
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 onramp session (Server-side)

From your UI, determine the amount, source currency (for example, `usd`), destination currency (for example, `usdc`), and network. Your backend 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 Stripe React Native SDK doesn’t provide APIs for creating a crypto onramp session. It happens on your backend. The example below shows how a client application might call your backend. Adapt it to your use case.

#### Client-side

```jsx
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} />;
}
```

#### Server-side

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN" \
  -d "ui_mode=headless" \
  -d "crypto_customer_id=crc_xxx" \
  -d "payment_token=cpt_xxx" \
  -d "source_amount=100" \              # Pass source_amount OR destination_amount, not both.
  -d "source_currency=usd" \
  -d "destination_currency=usdc" \
  -d "destination_network=base" \       # Singular: pins the transaction to this network.
  -d "destination_networks[]=base" \    # Array: required when wallet_address is set. Must match destination_network.
  -d "wallet_address=0x1234567890abcdef1234567890abcdef12345678" \
  -d "customer_ip_address=203.0.113.1"
```

### Step 5: Perform checkout (Client-side) (Server-side)

Call `performCheckout` to run the checkout flow for a crypto onramp session. It presents a UI for any required actions such as 3DS.

You must implement the client-side callback `fetchClientSecretFromBackend`, which the SDK invokes to retrieve the checkout client secret. Have it call your back end, which calls the [onramp session checkout endpoint](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#checkout) with the session ID. The response includes the `client_secret`, which your callback can then return to the SDK.

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

#### Client-side

```jsx
import { useOnramp } from '@stripe/stripe-react-native';

function CheckoutComponent() {
  const { performCheckout } = useOnramp();

  const handleCheckout = async () => {
    const result = await performCheckout(sessionId, async () => {
      // fetchClientSecretFromBackend is a client-side function you must implement.
      // It calls your back end, which calls the onramp session checkout endpoint with the session ID
      // and returns the client_secret from the response.
      // Return the client secret on success, or throw an Error on failure.
      const clientSecret = await fetchClientSecretFromBackend(sessionId);
      return clientSecret;
    });

    if (result.error?.code === 'Canceled') {
      // User canceled. Dismiss and let them try again.
    } else if (result.error) {
      // Checkout failed. Show result.error.message and let the user retry.
    } else {
      // Purchase complete. Show success and that crypto was sent to their wallet.
    }
  };

  return <Button title="Complete Purchase" onPress={handleCheckout} />;
}
```

#### Server-side

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions/{sessionId}/checkout \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

When the API returns 200 or 202 but the purchase isn’t done, the response body includes the [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions.md) object with `transaction_details.last_error` set. Use that value to decide the next step:

| **last\_error**                 | **Description**                                             | **How to handle**                                                                                                                                     |
| ------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_required`               | User must complete a payment step (for example, 3D Secure). | Run the SDK’s 3DS handling. After the user completes the step, call checkout again.                                                                   |
| `missing_kyc`                   | KYC verification is required.                               | Have the user complete KYC in the SDK (for example, `attachKycInfo`). Then call checkout again.                                                       |
| `missing_document_verification` | Identity document verification is required.                 | Have the user complete verification in the SDK (for example, `verifyIdentity`). Then call checkout again.                                             |
| `charged_with_expired_quote`    | Quote expired.                                              | Call [Refresh a Quote](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#refresh-a-quote) API, then call checkout again. |
| `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:

```jsx
import { useOnramp } from '@stripe/stripe-react-native';

function CheckoutWithRetryComponent() {
  const { attachKycInfo, verifyIdentity, registerWalletAddress, performCheckout } = useOnramp();

  const handleCheckout = async () => {
    // Step 1: Create a new session on your backend before starting checkout.
    let sessionId = await createSessionOnBackend();

    for (let attempt = 0; attempt < 5; attempt++) {
      const result = await performCheckout(sessionId, async () => {
        // Your backend calls POST /v1/crypto/onramp_sessions/{sessionId}/checkout.
        // Return { clientSecret, lastError } from your backend response.
        return await fetchClientSecretFromBackend(sessionId);
      });

      if (!result.error) {
        // Purchase complete.
        return;
      }

      if (result.error.code === 'Canceled') {
        // User canceled. Stop retrying.
        return;
      }

      // 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 attachKycInfo({ firstName, lastName, idNumber, dateOfBirth, address });
      } else if (lastError === 'missing_document_verification') {
        // Prompt user to complete identity verification, then retry on the same session.
        await verifyIdentity();
      } else if (lastError === 'missing_consumer_wallet') {
        // Prompt user to register a wallet, then retry on the same session.
        await 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 {
        // Terminal errors: transaction_limit_reached, location_not_supported, transaction_failed.
        // Do not retry. Show an appropriate error message.
        showError(lastError);
        return;
      }
    }
  };

  return <Button title="Complete Purchase" onPress={handleCheckout} />;
}
```

## Testing

> You can test your integration in two ways: *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, and *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. The OAuth client ID and client secret are the same for both sandboxes and live mode.

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

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            | SSN (`idNumber`)         | `000000000`                                           |
| KYC            | Address line 1 (`line1`) | `address_full_match`                                  |
| KYC            | State (`state`)          | Two-letter code (for example, `WA`, not `Washington`) |
| Payment        | Credit card number       | `4242 4242 4242 4242`                                 |

#### Identity verification

The identity verification step (`verifyIdentity`) 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.

#### Currency and network support

Bridge-backed coins aren’t available 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). For example, you can onramp USDC on Solana but not Bridge-backed coins such as USDC on Base.

### Live mode testing

Live mode testing validates the full production experience 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.
- **Physical device**: The SDK requires a physical iOS or Android device. Simulators and emulators are not supported for live mode. 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.
- **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.

## Main objects

### CryptoCustomer

A Crypto Customer represents a Link user in the context of your platform’s crypto onramp. It ties together user’s KYC and verification state, their consumer wallets, and their payment tokens. You use `customerId` in all onramp API calls to check what’s missing (for example, KYC or identity verification), to list wallets and payment tokens, and to create crypto onramp sessions.

| Attribute         | Type                  | Description                                                                                                                                               |
| ----------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | string                | Unique identifier for the object (for example, `crc_xxx`).                                                                                                |
| `provided_fields` | array of strings      | The set of KYC fields. See [KycField](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#kycfield).                           |
| `verifications`   | array of Verification | List of verifications and their outcome. See [Verification](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#verification). |

#### KycField

| Value                 | Description                                                       |
| --------------------- | ----------------------------------------------------------------- |
| `first_name`          | Customer’s first name.                                            |
| `last_name`           | Customer’s last name.                                             |
| `id_type`             | Type of ID document (for example, passport or driver’s license).  |
| `id_number`           | ID document number.                                               |
| `dob`                 | Date of birth.                                                    |
| `address_line_1`      | Address line 1.                                                   |
| `address_line_2`      | Address line 2.                                                   |
| `address_city`        | City.                                                             |
| `address_state`       | State or region.                                                  |
| `address_postal_code` | Postal or ZIP code.                                               |
| `address_country`     | Country of address.                                               |
| `birth_country`       | Country of birth.                                                 |
| `birth_city`          | City of birth.                                                    |
| `nationalities`       | Nationality or nationalities.                                     |
| `id_document`         | Identity document (for example, image or verification reference). |
| `selfie`              | Selfie image for verification.                                    |

#### Verification

| Attribute | Type                    | Description                                                                                                             |
| --------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `name`    | string (enum)           | [VerificationType](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#verificationtype)     |
| `status`  | string (enum)           | [VerificationStatus](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#verificationstatus) |
| `errors`  | array of strings (enum) | [VerificationError](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#verificationerror)   |

#### VerificationType

| Value                  | Description                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| `phone_verified`       | Verification that the customer submitted minimum identity passed Stripe Identity phone verification. |
| `kyc_verified`         | Verification that the customer submitted identity information passed Stripe Policy verifications.    |
| `id_document_verified` | Verification that the customer has submitted and verified their identity documents.                  |

#### VerificationStatus

| Value         | Description                                                                                                                                                 |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `not_started` | Customer hasn’t provided the information.                                                                                                                   |
| `pending`     | Verification has been submitted and is processing.                                                                                                          |
| `rejected`    | Verification failed. See [VerificationError](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#verificationerror) for details. |
| `verified`    | Verification complete. The customer may continue.                                                                                                           |

#### VerificationError

| Value                                       | Description                                                                                                            |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `id_document_verification_failed`           | Identity document verification didn’t succeed (for example, document was rejected or couldn’t be verified).            |
| `user_has_reached_max_verification_attempt` | The user has reached the maximum number of verification attempts and must wait or contact support before trying again. |

### CryptoOnrampSession

See [Stripe API docs](https://docs.stripe.com/api/crypto/onramp_sessions/object.md) for the object’s attributes.

### PaymentToken

A Payment Token is a read-only representation of a payment method that belongs to a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer) and applies only to crypto onramp.

| Attribute         | Type              | Description                                                                                                           |
| ----------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `id`              | string            | Unique identifier for the payment token (for example, `cpt_xxx`).                                                     |
| `type`            | string (enum)     | [PaymentMethodType](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#paymentmethodtype) |
| `card`            | object (optional) | [Card](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#card)                           |
| `us_bank_account` | object (optional) | [UsBankAccount](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#usbankaccount)         |

#### PaymentMethodType

| Value             | Description                     |
| ----------------- | ------------------------------- |
| `card`            | Card payment method.            |
| `us_bank_account` | US bank account payment method. |

#### Card

| Attribute   | Type               | Description                                                                                     |
| ----------- | ------------------ | ----------------------------------------------------------------------------------------------- |
| `brand`     | string (optional)  | Card brand (for example, amex, mastercard, visa).                                               |
| `last4`     | string (optional)  | Last four digits of the card.                                                                   |
| `exp_month` | integer (optional) | Two-digit expiration month (1–12).                                                              |
| `exp_year`  | integer (optional) | Four-digit expiration year.                                                                     |
| `funding`   | string             | Card funding type (for example, credit, debit, prepaid, unknown).                               |
| `wallet`    | object (optional)  | [Wallet](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#wallet) |

#### Wallet

| Attribute | Type          | Description                                                                                             |
| --------- | ------------- | ------------------------------------------------------------------------------------------------------- |
| `type`    | string (enum) | [WalletType](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#wallettype) |

#### WalletType

| Value        | Description        |
| ------------ | ------------------ |
| `apple_pay`  | Apple Pay wallet.  |
| `google_pay` | Google Pay wallet. |

#### UsBankAccount

| Attribute      | Type                    | Description                                                                                               |
| -------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `account_type` | string (enum, optional) | [AccountType](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#accounttype) |
| `last4`        | string (optional)       | Last four digits of the bank account number.                                                              |
| `bank_name`    | string (optional)       | Name of the bank.                                                                                         |

#### AccountType

| Value      | Description       |
| ---------- | ----------------- |
| `checking` | Checking account. |
| `savings`  | Savings account.  |

### ConsumerWallet

A Consumer Wallet is a cryptocurrency wallet address linked to a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer). It’s the destination where purchased crypto is sent. The user registers wallet addresses during account setup.

| Attribute        | Type          | Description                                                                                       |
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------- |
| `id`             | string        | Unique identifier for the consumer wallet.                                                        |
| `livemode`       | boolean       | Whether this wallet is in live mode (true) or test mode (false).                                  |
| `network`        | string (enum) | [Network](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#network) |
| `wallet_address` | string        | The blockchain address (for example, the destination address for crypto delivery).                |

#### Network

| Value        | Description         |
| ------------ | ------------------- |
| `bitcoin`    | Bitcoin network.    |
| `ethereum`   | Ethereum network.   |
| `solana`     | Solana network.     |
| `polygon`    | Polygon network.    |
| `base`       | Base network.       |
| `avalanche`  | Avalanche network.  |
| `stellar`    | Stellar network.    |
| `aptos`      | Aptos network.      |
| `optimism`   | Optimism network.   |
| `worldchain` | Worldchain network. |
| `xrpl`       | XRP Ledger network. |

## API specs

### Retrieve a CryptoCustomer

Retrieves the details of a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer). Use the response to check the customer’s KYC and verification status so you can prompt the user to complete setup (for example, KYC or identity verification) before creating an onramp session.

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId} \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

```json
// Response
{
  "id": "crc_XXXX",
  "object": "object.public_crypto_customer",
  "provided_fields": ["first_name", "last_name"],
  "verifications": [
    {
      "name": "phone_verified",
      "status": "verified",
      "errors": []
    },
    {
      "name": "kyc_verified",
      "status": "verified",
      "errors": []
    },
    {
      "name": "id_document_verified",
      "status": "rejected",
      "errors": ["id_document_verification_failed"]
    }
  ]
}
```

**Parameters**

| Parameter | Type              | Description                                      |
| --------- | ----------------- | ------------------------------------------------ |
| `id`      | string (required) | The crypto customer ID (for example, `crc_xxx`). |

**Returns**

Returns a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer) object.

**Errors**

| HTTP status | Cause                                                                                                                                                    |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Missing OAuth token. The request didn’t include the `Stripe-OAuth-Token` header.                                                                         |
| 403         | Invalid or insufficient OAuth token. The token is missing, invalid, expired, has wrong scopes (for example, missing kyc.status:read), or wrong livemode. |
| 403         | Customer belongs to another user. The CryptoCustomer exists but is tied to a different Link consumer than the one identified by the OAuth token.         |
| 404         | Customer not found. The :id isn’t a valid crypto customer ID.                                                                                            |
| 404         | Embedded Components onramp not enabled. The request is from a business that doesn’t have access to the Embedded Components onramp.                       |

### List ConsumerWallets

Returns a list of [ConsumerWallet](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#consumerwallet) for a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer). Use this to see which wallet addresses are on file and whether the user needs to register a new one. Each wallet has an id, network, and address.

```shell
curl "https://api.stripe.com/v1/crypto/customers/{customerId}/crypto_consumer_wallets" \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

```json
// Response
{
  "data": [
    {
      "id": "ccw_xxx",
      "object": "crypto.consumer_wallet",
      "livemode": false,
      "network": "ethereum",
      "wallet_address": "0x1234567890abcdef1234567890abcdef12345678"
    }
  ],
  "has_more": false
}
```

**Parameters**

| Parameter        | Type               | Description                                                           |
| ---------------- | ------------------ | --------------------------------------------------------------------- |
| `id`             | string (required)  | The crypto customer id (for example, `crc_xxx`).                      |
| `limit`          | integer (optional) | Maximum number of wallets to return. Default is 10; maximum is 20.    |
| `starting_after` | string (optional)  | A wallet id. Return wallets after this id (for forward pagination).   |
| `ending_before`  | string (optional)  | A wallet id. Return wallets before this id (for backward pagination). |

**Returns**

| Field      | Type    | Description                                     |
| ---------- | ------- | ----------------------------------------------- |
| `data`     | array   | List of ConsumerWallet objects.                 |
| `has_more` | boolean | Whether there are more wallets after this page. |
| `url`      | string  | The URL of this list.                           |

**Errors**

| HTTP status | Cause                                                                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Missing required param.                                                                                                               |
| 403         | Invalid request: invalid or expired OAuth token, wrong scopes, livemode mismatch, or crypto customer belongs to a different consumer. |
| 404         | No such cryptocustomer: customer id invalid or not found.                                                                             |
| 404         | Embedded Components onramp not enabled for this business.                                                                             |

### List PaymentTokens

Returns a list of [PaymentToken](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#paymenttoken) for a [CryptoCustomer](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md#cryptocustomer). Use this to see which saved payment methods the user has before asking them to add one.

```shell
curl https://api.stripe.com/v1/crypto/customers/{customerId}/payment_tokens \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

```json
// Response
{
  "url": "/v1/crypto/customers/crc_xxx/payment_tokens",
  "data": [
    {
      "id": "cpt_xxx",
      "object": "crypto.payment_token",
      "type": "card",
      "card": {
        "brand": "visa",
        "last4": "4242",
        "exp_month": 12,
        "exp_year": 2028,
        "funding": "credit"
      }
    }
  ],
  "has_more": false
}
```

**Parameters**

| Parameter        | Type               | Description                                                                                                 |
| ---------------- | ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `id`             | string (required)  | The crypto customer id (for example, `crc_xxx`). Must belong to the consumer identified by the OAuth token. |
| `limit`          | integer (optional) | Maximum number of payment tokens to return. Default is 10.                                                  |
| `starting_after` | string (optional)  | A payment token id. Return tokens after this id (for forward pagination).                                   |
| `ending_before`  | string (optional)  | A payment token id. Return tokens before this id (for backward pagination).                                 |

**Returns**

| Field      | Type    | Description                                            |
| ---------- | ------- | ------------------------------------------------------ |
| `data`     | array   | List of PaymentToken objects.                          |
| `has_more` | boolean | Whether there are more payment tokens after this page. |
| `url`      | string  | The URL of this list.                                  |

**Errors**

| HTTP status | Cause                                                                                                                                              |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | Missing required param: HTTP_HEADER[Stripe-OAuth-Token]. Send the Stripe-OAuth-Token header with a valid OAuth access token.                       |
| 403         | Invalid request: invalid or expired OAuth token, missing crypto:ramp scope, livemode mismatch, or crypto customer belongs to a different consumer. |
| 404         | No such crypto customer: customer id invalid or not found.                                                                                         |
| 404         | Embedded Components onramp not enabled for this business.                                                                                          |

### Checkout

Completes a [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md) by confirming the payment and executing the quote so crypto is delivered to the customer’s wallet. Call this after creating an onramp session. If the response indicates more steps (for example, 3DS), have the user complete them in the SDK and call checkout again.

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions/{sessionId}/checkout \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

```json
// Response
{
  "id": "cos_xxx",
  "object": "crypto.onramp_session",
  "status": "fulfillment_complete",
  "transaction_details": {
    "wallet_address": "0xx",
    "source_amount": "100.00",
    "source_currency": "usd",
    "destination_amount": "0.05",
    "destination_currency": "eth",
    "destination_network": "ethereum",
    "quote_expiration": 1756238966,
    "transaction_id": "0xx"
  }
}
```

**Parameters**

| Parameter      | Type              | Description                                                                                                                                                    |
| -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | string (required) | The onramp session id (for example, `cos_xxx`) to checkout. The session must have been created with `ui_mode=headless` and be in a state that allows checkout. |
| `mandate_data` | object (optional) | Details for creating a mandate when required (for example, for certain payment method types or regions). Include when the API or payment method requires it.   |

**Returns**

Returns the updated [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md) object.

**Errors**

| HTTP status | Cause                                                                                                               |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| 400         | Invalid request: missing or invalid parameters, unsupported currency or network, or invalid or missing OAuth token. |
| 400         | MissingKYC: user has not completed KYC.                                                                             |
| 400         | InvalidWallet: wallet not attached to this user.                                                                    |
| 400         | InvalidPaymentMethod: payment method not attached to this user.                                                     |
| 400         | Unauthorized: OAuth token invalid or missing.                                                                       |
| 404         | Session not found or not owned by this business.                                                                    |

### Refresh a Quote

Refreshes the quote for a [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md). Use this when checkout returns `charged_with_expired_quote` so you can get a new quote and call checkout again on the same session instead of creating a new one.

```shell
curl -X POST https://api.stripe.com/v1/crypto/onramp_sessions/{sessionId}/quote \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -H "Stripe-OAuth-Token: $ACCESS_TOKEN"
```

```json
// Response
{
  "id": "cos_XXXX",
  "object": "crypto.onramp_session",
  "status": "requires_payment",
  "transaction_details": {
    "wallet_address": "0x1234567890abcdef1234567890abcdef12345678",
    "source_amount": "100.00",
    "source_currency": "usd",
    "destination_amount": "0.05",
    "destination_currency": "eth",
    "destination_network": "ethereum",
    "quote_expiration": 1756238966
  }
}
```

**Parameters**

| Parameter | Type              | Description                                                                      |
| --------- | ----------------- | -------------------------------------------------------------------------------- |
| `id`      | string (required) | The crypto onramp session ID (for example, `cos_xxx`). Provided in the URL path. |

**Returns**

Returns a [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions/object.md) object with an updated quote.

**Errors**

| HTTP status | Cause                                                                                                                              |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 403         | Quote locked. The session quote is in a locked state. Create a new session and call checkout on it instead.                        |
| 403         | Internal error. The session has no consumer account, or the liquidity provider account couldn’t be found.                          |
| 404         | Session not found. The id isn’t a valid crypto onramp session ID or the session doesn’t exist.                                     |
| 404         | Embedded Components onramp not enabled. The request is from a business that doesn’t have access to the Embedded Components onramp. |
| 499         | Partner exchange error. The quote provider returned an error or timeout.                                                           |

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

This API is still in private preview. [Sign up](https://docs.stripe.com/crypto/onramp.md#sign-up) to get access to additional API info.

```shell
curl -X POST https://login.link.com/v1/link_auth_intent \
  -H "Authorization: Bearer $STRIPE_SECRET_KEY" \
  -d email=user@example.com \
  -d oauth_client_id=$OAUTH_CLIENT_ID \
  -d oauth_scopes=kyc.status:read,crypto:ramp
```

```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 consumer. 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         | CreateLinkAuthIntent not enabled for business or invalid or missing API key.               |
| 404         | OAuth client not found for authIntentId or no active Link consumer for the provided email. |
| 409         | Link consumer previously revoked connection with this partner.                             |

### Retrieve Access Tokens

Exchanges a consented `LinkAuthIntent` for an OAuth access token. Call this after the user has completed authorization. Use the access token (for example, in the `Stripe-OAuth-Token` header) on subsequent onramp API requests for that user.

This API is still in private preview. [Sign up](https://docs.stripe.com/crypto/onramp.md#sign-up) to get access to additional API info.

```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` has not been consented.                                       |
| 403         | Invalid or missing API key.                                                    |
| 404         | `LinkAuthIntent` not found (invalid id or 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) 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.

This API is still in private preview. [Sign up](https://docs.stripe.com/crypto/onramp.md#sign-up) to get access to additional API info.

```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 Retrieve Access Tokens. |
| `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            | 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.                                                                                                                         |

## React Native SDK

| SDK method                                  | Presents a UI | What the user sees                                               |
| ------------------------------------------- | ------------- | ---------------------------------------------------------------- |
| `authorize(authIntentId)`                   | Yes           | Link consent screen (or consent inline on OTP screen).           |
| `attachKycInfo`                             | Optional      | You can collect KYC in your own UI and pass data in.             |
| `presentKycInfoVerification`                | Yes           | A Stripe screen showing existing KYC for the customer to verify. |
| `verifyIdentity`                            | Yes           | The Stripe-hosted flow (document + selfie).                      |
| `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 onramp 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* (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) 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-environment` entitlement.

- Confirm you’re running on a supported device and using a supported distribution method. We support simulators in *sandboxes* (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), 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](https://developer.apple.com/testflight/), or publish to the App Store. When running from Xcode, set `com.apple.developer.devicecheck.appattest-environment` to `production` in your entitlements file, and delete and reinstall the app if you previously used the `development` environment.

### Unrecognized request URL

If your API calls return a `404` with an `invalid_request_error` and the message `Unrecognized 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 onramp.
