> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Quickstart

> Let users withdraw stablecoins to their bank account from your React app

In this guide, you will add a withdrawal flow to a Next.js app with the `CrossmintEmbeddedOfframp` component and run a withdrawal in the Staging environment. You will:

* Create an offramp session on your server for the user and the wallet they withdraw from
* Render `CrossmintEmbeddedOfframp` with the session and the user's Crossmint wallet
* Handle the component's events
* Test a withdrawal on staging

The component runs the whole withdrawal inside your app: the user picks, adds, renames or removes a bank account, enters an amount, verifies their identity if the order needs it, reviews the quote and approves the deposit from their wallet.

<Snippet file="offramp-embedded-flow.mdx" />

## Prerequisites

* **Server**: one server endpoint that creates the session, such as the Next.js server action in this guide.
* **API keys**: a server-side API key with the `orders.create` and `payment-methods.create` scopes, and a [client-side API key](https://docs.crossmint.com/introduction/platform/api-keys/client-side.md) for the component, both from the [staging console](https://staging.crossmint.com/console).
* **Project setup**: KYC configured for your project and, for production, Offramp and bank account payment methods enabled on it. Crossmint sets these up per project; [contact Crossmint](https://www.crossmint.com/contact/sales) to enable them. You can test the embedded offramp on staging before they are enabled for production.
* **Crossmint wallet**: an EVM or Solana wallet owned by `userId:<USER_ID>`, where the user ID is the `sub` of your JWT. Set up your authentication and the Crossmint providers as in [Use Your Own Auth Provider](https://docs.crossmint.com/wallets/guides/bring-your-own-auth.md), with `base-sepolia` as the chain. The session takes only `userId:` locators, so email and phone locators do not work.
* **USDC**: testnet USDC in the wallet, from the [Circle faucet](https://faucet.circle.com/).

Install the Crossmint client SDK, version 4.13.1 or later:

<Snippet file="client-sdk-react-ui-installation-cmd.mdx" />

Add your API keys to `.env.local`. Pass the client-side key to `CrossmintProvider` as `apiKey={process.env.NEXT_PUBLIC_CROSSMINT_API_KEY}`:

```bash .env.local theme={null}
NEXT_PUBLIC_CROSSMINT_API_KEY="_YOUR_CLIENT_API_KEY_"
CROSSMINT_SERVER_SIDE_API_KEY="_YOUR_SERVER_API_KEY_"
```

### 1. Create the session on your server

A session lets one user make one withdrawal from one wallet. Your server makes one call, [Create Offramp Session](https://docs.crossmint.com/offramp/api-reference/create-offramp-session.md), and the component runs every other step with Crossmint:

<Snippet file="offramp-embedded-where-it-runs.mdx" />

Make the call from a server action. Anyone can read a key that ships to the browser, and with your server API key they could open withdrawal sessions for any of your users:

```typescript app/actions/createOfframpSession.ts theme={null}
"use server";

interface CreateOfframpSessionParams {
    userId: string;
    receiptEmail: string;
    walletAddress: string;
    chain: string;
}

export async function createOfframpSession({ userId, receiptEmail, walletAddress, chain }: CreateOfframpSessionParams) {
    const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
    if (serverApiKey == null) {
        throw new Error("CROSSMINT_SERVER_SIDE_API_KEY is not set");
    }

    const response = await fetch("https://staging.crossmint.com/api/unstable/offramp-sessions", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-API-KEY": serverApiKey,
        },
        body: JSON.stringify({
            userLocator: `userId:${userId}`,
            payer: { address: walletAddress, chain },
            receiptEmail,
        }),
    });

    if (!response.ok) {
        const { code, message } = await response.json().catch(() => ({}));
        throw new Error(`Create Offramp Session failed (${response.status}, ${code ?? "no code"}): ${message ?? response.statusText}`);
    }

    const { sessionId, clientSecret } = await response.json();
    return { sessionId, clientSecret };
}
```

<Snippet file="offramp-embedded-session-fields.mdx" />

> [!WARNING]
>
> This example takes the user from the caller to stay short. In your app, read the signed-in user's ID and email from your own server-side session, so no one can open a session for another user.

### 2. Render the offramp

Create a client component that creates the session and renders `CrossmintEmbeddedOfframp`. `createCrossmintWalletSigner` turns the user's Crossmint wallet into the `payer` that signs and sends the deposit:

```tsx app/components/Withdraw.tsx theme={null}
"use client";

import { useState } from "react";
import {
    CrossmintEmbeddedOfframp,
    createCrossmintWalletSigner,
    useWallet,
    type EmbeddedOfframpCredentials,
} from "@crossmint/client-sdk-react-ui";
import { createOfframpSession } from "../actions/createOfframpSession";

export default function Withdraw({ userId, email }: { userId: string; email: string }) {
    const { wallet } = useWallet();
    const [credentials, setCredentials] = useState<EmbeddedOfframpCredentials | null>(null);
    const [isLoading, setIsLoading] = useState(false);

    if (wallet == null) {
        return <p>Sign in to load your wallet.</p>;
    }

    const startWithdrawal = async () => {
        setIsLoading(true);
        try {
            setCredentials(
                await createOfframpSession({
                    userId,
                    receiptEmail: email,
                    walletAddress: wallet.address,
                    chain: wallet.chain,
                })
            );
        } catch (error) {
            console.error("Failed to create the offramp session:", error);
        } finally {
            setIsLoading(false);
        }
    };

    if (credentials == null) {
        return (
            <button onClick={startWithdrawal} disabled={isLoading}>
                {isLoading ? "Starting..." : "Withdraw to bank"}
            </button>
        );
    }

    const close = () => setCredentials(null);

    return (
        <div className="max-w-[450px] w-full mx-auto">
            <button onClick={close}>Close</button>
            <CrossmintEmbeddedOfframp
                credentials={credentials}
                payer={createCrossmintWalletSigner(wallet)}
                onStatusChange={({ status, orderId }) => console.log("Offramp status:", status, orderId)}
                onComplete={({ orderId, txId }) => console.log("Withdrawal sent:", orderId, txId)}
                onCancel={close}
                onError={({ reason, retriable, message }) => console.error("Offramp error:", reason, retriable, message)}
            />
        </div>
    );
}
```

Render `<Withdraw />` inside the Crossmint providers, with the user ID from your JWT and the user's email:

```tsx app/page.tsx theme={null}
import { Providers } from "./providers";
import Withdraw from "./components/Withdraw";

export default function Home() {
    return (
        <Providers>
            <main className="min-h-screen flex items-center justify-center">
                <Withdraw userId="<USER_ID>" email="alice@example.com" />
            </main>
        </Providers>
    );
}
```

The component takes the width of its container. It is at least 650px tall plus padding, and grows when a screen needs more. To give it a fixed area instead, pass `fill` and give the container a definite height. See [Sizing](https://docs.crossmint.com/offramp/guides/ui-customization.md#sizing).

### 3. Handle the events

<Snippet file="offramp-embedded-handle-events.mdx" />

### 4. Test on staging

1. Fund the wallet with testnet USDC from the [Circle faucet](https://faucet.circle.com/).
2. Click **Withdraw to bank**, add a US bank account, and enter an amount. Use a bank account number that is unique to your integration.
3. If the user is not verified yet, verify their identity when the component asks.
4. Review the quote and confirm with **Hold to confirm**. The wallet signs and sends the deposit, and the status moves through `confirming-deposit` to `completed`, when `onComplete` fires.

On staging every payout is simulated and succeeds. To see a failed payout, add the US test bank account from [Test a Failed Payout](https://docs.crossmint.com/offramp/guides/test-payout-failures.md) in the component. The SEPA one appears only when SEPA is offered (see [SEPA in the Embedded Component](https://docs.crossmint.com/offramp/concepts/how-it-works.md#sepa-in-the-embedded-component)).

## Use an External Wallet

<Snippet file="offramp-embedded-external-wallet-payer.mdx" />

```typescript payer.ts theme={null}
import { OfframpSignatureRejectedError, type EmbeddedOfframpPayer } from "@crossmint/client-sdk-react-ui";

// Your wallet code: sign and broadcast the transaction, and resolve with its transaction ID.
declare function sendWithYourWallet(tx: { serializedTransaction: string; chain: string; payerAddress: string }): Promise<string>;
// Your wallet code: true when the user declined the request in their wallet.
declare function isUserRejection(error: unknown): boolean;

export const payer: EmbeddedOfframpPayer = {
    async signAndSendTransaction(tx) {
        try {
            return await sendWithYourWallet(tx);
        } catch (error) {
            if (isUserRejection(error)) {
                throw new OfframpSignatureRejectedError("The user declined the transaction");
            }
            throw error;
        }
    },
};
```

<Snippet file="offramp-embedded-external-wallet-proof.mdx" />

## Troubleshooting

<Snippet file="offramp-embedded-troubleshooting.mdx" />

## Launching in Production

<Snippet file="offramp-embedded-launch.mdx" />

## Learn More

> [card] [React SDK Reference](https://docs.crossmint.com/sdk-reference/offramp/react/components.md)
>
> Props and types of `CrossmintEmbeddedOfframp`

> [card] [Handle Offramp Events and Errors](https://docs.crossmint.com/offramp/guides/handle-events-and-errors.md)
>
> What each status and error reason means
