> ## 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. Your server only creates the session.

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

## Prerequisites

* **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, bank account payment methods and the embedded offramp 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 a user of your project with a `userId:` locator. The session refuses email and phone locators. With [your own authentication](https://docs.crossmint.com/wallets/guides/bring-your-own-auth.md), Crossmint sets the wallet's owner to the user ID in your JWT. Set up the providers as in the [Wallets React quickstart](https://docs.crossmint.com/wallets/quickstarts/react.md), with `base-sepolia` as the chain.
* **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`. The providers from the Wallets quickstart read the client-side 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. Create it with [Create Offramp Session](https://docs.crossmint.com/offramp/api-reference/create-offramp-session.md) from a server action, so your server API key never reaches the browser:

```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 error = await response.json();
        throw new Error(error.message || "Failed to create the offramp session");
    }

    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 signed-in user's ID and 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/embedded-offramp-reference.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 [Supported Markets](https://docs.crossmint.com/offramp/guides/embedded-offramp-reference.md#supported-markets)).

## 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] [Embedded Offramp Reference](https://docs.crossmint.com/offramp/guides/embedded-offramp-reference.md)
>
> Props, events, statuses and errors

> [card] [Manage Orders](https://docs.crossmint.com/offramp/guides/manage-orders.md)
>
> Status, webhooks, and failures
