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

# OAuth2 Integration

> Implement secure exchange authentication with Bluvo's OAuth2 popup flow, from exchange selection through wallet connection

## Overview

Bluvo's OAuth2 integration lets your users connect their exchange accounts through a secure popup window. No API keys are exposed to your frontend, users authenticate directly with the exchange, and your app receives a [**wallet ID**](/learn/wallet-id) that represents the connection.

From that point forward, the [wallet ID](/learn/wallet-id) is all you need to fetch balances, request withdrawal quotes, and execute transactions. Bluvo handles the entire OAuth lifecycle behind the scenes: authorization URL generation, token exchange, token refresh, AES-256-CBC encryption, and tenant-isolated storage.

This page walks through the OAuth2 popup flow using `@bluvo/react` and Next.js server actions, the fastest path to a working integration.

<Tip>
  If you pass a [`walletId`](/learn/wallet-id) that already exists, `startWithdrawalFlow()` automatically detects the existing wallet and skips OAuth, no separate resume call needed.
</Tip>

***

## Integration Levels

Bluvo supports multiple integration depths. Pick the level that matches your team's needs:

| Level                          | Description                                               | Effort      | Package                                                        |
| ------------------------------ | --------------------------------------------------------- | ----------- | -------------------------------------------------------------- |
| REST API                       | Full control, implement the entire flow yourself          | \~3-4 weeks | [API Reference](/api-reference)                                |
| Server SDK                     | Language-specific wrapper around the REST API             | \~3 weeks   | [`@bluvo/sdk-ts`](https://www.npmjs.com/package/@bluvo/sdk-ts) |
| State Machine SDK + Server SDK | You build the UI, the state machine handles orchestration | \~5 days    | [`@bluvo/sdk-ts`](https://www.npmjs.com/package/@bluvo/sdk-ts) |
| React State Machine            | React hooks with built-in state management                | \~24 hours  | [`@bluvo/react`](https://www.npmjs.com/package/@bluvo/react)   |

This page focuses on the **React State Machine** approach with `@bluvo/react`. The same concepts (states, transitions, error handling) apply to all levels.

***

## How OAuth2 Works

When a user connects an exchange account, this is what happens under the hood:

1. Your app calls `startWithdrawalFlow({ exchange, walletId })` with the user's selected exchange and a [wallet ID](/learn/wallet-id) (new or previously stored)
2. Bluvo requests an OAuth2 authorization URL from the exchange and opens a popup window
3. The user authenticates directly with the exchange inside the popup
4. The exchange redirects back to Bluvo's callback with an authorization code
5. Bluvo exchanges the code for access/refresh tokens server-side, then encrypts and stores them
6. A wallet record is created and the [wallet ID](/learn/wallet-id) is returned to your app via WebSocket
7. The popup closes automatically and the flow transitions to `wallet:loading`

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Client
    participant S as Bluvo SDK
    participant B as Bluvo Server
    C->>S: oauth2.signIn(exchangeId)
    S-->>C: HTTPS URL
    C->>C: Open popup with URL
    C->>B: User enters credentials
    B->>B: Generate accessToken
    B-->>C: Callback with accessToken
```

### What Bluvo Handles vs. What You Handle

**Bluvo manages:**

* OAuth URL generation and popup lifecycle
* Token exchange, refresh, and revocation
* AES-256-CBC encryption of all credentials
* WebSocket notifications for real-time state updates
* [Wallet ID](/learn/wallet-id) creation and tenant-isolated storage

**You are responsible for:**

* Calling `startWithdrawalFlow()` when the user selects an exchange
* Rendering UI based on the current state
* Persisting [wallet IDs](/learn/wallet-id) via the `onWalletConnectedFn` callback (so returning users skip OAuth automatically)
* Handling popup-closed and error states gracefully

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Client
    participant W as WebSocket (Bluvo)
    participant B as Bluvo Server
    participant DB as Bluvo Tenant DB
    C->>W: Connect (topicName, topicSecretKey)
    W-->>C: WebSocket established
    B->>B: Create API key for wallet
    B->>DB: Store encrypted wallet with idem key
    DB-->>B: walletId
    B->>W: Send {success, walletId?, msg}
    W-->>C: Stream {success, walletId?, msg}
```

***

## State Machine

The `useBluvoFlow` hook exposes the current state of the OAuth flow as boolean properties. Your UI simply reacts to these states.

### OAuth State Flow

```mermaid theme={null}
flowchart LR
    idle["idle"]
    oauth_waiting["oauth:waiting"]
    oauth_processing["oauth:processing"]
    oauth_completed["oauth:completed"]
    wallet_loading["wallet:loading"]
    wallet_ready["wallet:ready"]:::success
    oauth_closed["oauth:window_closed_by_user"]:::error
    oauth_error["oauth:error"]:::error
    oauth_fatal["oauth:fatal"]:::error

    idle --> oauth_waiting --> oauth_processing --> oauth_completed
    oauth_completed --> wallet_loading --> wallet_ready
    oauth_processing --> oauth_closed
    oauth_processing --> oauth_error
    oauth_processing --> oauth_fatal

    classDef error fill:#fca5a5,stroke:#ef4444,color:#7f1d1d
    classDef success fill:#86efac,stroke:#22c55e,color:#14532d
```

### State Reference

| State                         | Meaning                             | Suggested UI                                            |
| ----------------------------- | ----------------------------------- | ------------------------------------------------------- |
| `idle`                        | Flow not started                    | Exchange selector                                       |
| `oauth:waiting`               | Popup open, user authenticating     | Spinner + "Complete authentication in the popup window" |
| `oauth:processing`            | Token exchange in progress          | Spinner + "Connecting your account..."                  |
| `oauth:completed`             | OAuth succeeded, auto-transitions   | Brief "Connected!" message                              |
| `oauth:error`                 | Recoverable error                   | Error message + "Try Again" button                      |
| `oauth:fatal`                 | Non-recoverable (exchange rejected) | Error message + "Start Over" button                     |
| `oauth:window_closed_by_user` | User closed the popup               | "Authentication cancelled" + retry option               |
| `wallet:loading`              | Fetching wallet balances            | Loading skeleton                                        |
| `wallet:ready`                | Wallet connected and loaded         | Balance list, ready for withdrawals                     |

### Hook Booleans

Each state maps to a boolean on the `flow` object returned by `useBluvoFlow`:

```typescript theme={null}
// OAuth lifecycle
flow.isOAuthPending         // true during oauth:waiting OR oauth:processing
flow.isOAuthWaiting         // true when popup is open (oauth:waiting)
flow.isOAuthProcessing      // true during token exchange (oauth:processing)
flow.isOAuthComplete        // true on success (oauth:completed)

// OAuth errors
flow.isOAuthError           // true for any OAuth error (recoverable or fatal)
flow.isOAuthFatal           // true for non-recoverable errors only
flow.isOAuthWindowBeenClosedByTheUser  // true when user closed popup

// Wallet
flow.isWalletLoading        // true while fetching balances
flow.isWalletReady          // true when wallet is connected and balances loaded
flow.isWalletError          // true on wallet loading error
```

***

## React Implementation

### Prerequisites

Install the React SDK:

```bash theme={null}
pnpm add @bluvo/react @bluvo/sdk-ts
```

Set up environment variables:

| Variable                       | Side   | Description                                                   |
| ------------------------------ | ------ | ------------------------------------------------------------- |
| `BLUVO_ORG_ID`                 | Server | Organization ID from [Bluvo Portal](https://portal.bluvo.dev) |
| `BLUVO_PROJECT_ID`             | Server | Project ID                                                    |
| `BLUVO_API_KEY`                | Server | Secret API key (never expose to the client)                   |
| `NEXT_PUBLIC_BLUVO_ORG_ID`     | Client | Organization ID for hooks                                     |
| `NEXT_PUBLIC_BLUVO_PROJECT_ID` | Client | Project ID for hooks                                          |
| `NEXT_PUBLIC_BLUVO_ENV`        | Client | Optional: `production`, `staging`, or `development`           |

### Server Actions

Create server actions that proxy Bluvo SDK calls.

```typescript theme={null}
// app/actions/flowActions.ts
'use server'

import { createClient, createSandboxClient, createDevClient } from "@bluvo/sdk-ts";

function loadBluvoClient() {
    const env = process.env.NEXT_PUBLIC_BLUVO_ENV;
    if (env === 'production') {
        return createClient({
            orgId: process.env.BLUVO_ORG_ID!,
            projectId: process.env.BLUVO_PROJECT_ID!,
            apiKey: process.env.BLUVO_API_KEY!,
        });
    } else if (env === 'staging') {
        return createSandboxClient({
            orgId: process.env.BLUVO_ORG_ID!,
            projectId: process.env.BLUVO_PROJECT_ID!,
            apiKey: process.env.BLUVO_API_KEY!,
        });
    } else {
        return createDevClient({
            orgId: process.env.BLUVO_ORG_ID!,
            projectId: process.env.BLUVO_PROJECT_ID!,
            apiKey: process.env.BLUVO_API_KEY!,
        });
    }
}

export async function listExchanges(status?: string) {
    return await loadBluvoClient().oauth2.listExchanges(status as any);
}

export async function fetchWithdrawableBalances(walletId: string) {
    return await loadBluvoClient().wallet.withdrawals.getWithdrawableBalance(walletId);
}

export async function requestQuotation(walletId: string, params: {
    asset: string; amount: string; address: string;
    network?: string; tag?: string; includeFee?: boolean;
}) {
    return await loadBluvoClient().wallet.withdrawals.requestQuotation(walletId, params);
}

export async function executeWithdrawal(
    walletId: string, idem: string, quoteId: string,
    params?: {
        twofa?: string | null; emailCode?: string | null;
        smsCode?: string | null; bizNo?: string | null;
        tag?: string | null; params?: { dryRun?: boolean } | null;
    }
) {
    return await loadBluvoClient().wallet.withdrawals.executeWithdrawal(
        walletId, idem, quoteId, params ?? {}
    );
}

export async function getWalletById(walletId: string) {
    return await loadBluvoClient().wallet.get(walletId);
}

export async function pingWalletById(walletId: string) {
    return await loadBluvoClient().wallet.ping(walletId);
}
```

### Initialize the Hook

Wire up `useBluvoFlow` in a client component. No provider or context wrapper is needed, the hook manages its own state.

```typescript theme={null}
// app/connect/page.tsx
"use client";

import React from "react";
import { useBluvoFlow } from "@bluvo/react";
import {
    listExchanges, fetchWithdrawableBalances, requestQuotation,
    executeWithdrawal, getWalletById, pingWalletById
} from "../actions/flowActions";

export default function ConnectExchange() {
    const flow = useBluvoFlow({
        orgId: process.env.NEXT_PUBLIC_BLUVO_ORG_ID!,
        projectId: process.env.NEXT_PUBLIC_BLUVO_PROJECT_ID!,
        listExchangesFn: listExchanges,
        fetchWithdrawableBalanceFn: fetchWithdrawableBalances,
        requestQuotationFn: requestQuotation,
        executeWithdrawalFn: executeWithdrawal,
        getWalletByIdFn: getWalletById,
        pingWalletByIdFn: pingWalletById,
        onWalletConnectedFn: (walletId, exchange) => {
            // Persist so returning users skip OAuth automatically
            localStorage.setItem("walletId", walletId);
            localStorage.setItem("exchange", exchange);
        },
        options: {
            sandbox: process.env.NEXT_PUBLIC_BLUVO_ENV === 'staging',
            dev: process.env.NEXT_PUBLIC_BLUVO_ENV === 'development',
            // autoRefreshQuotation: true,  // optional: auto-refresh quotes
            // cache: true,                 // optional: cache exchange list
        },
    });

    // Render UI based on flow state, see next section
}
```

<Tip>
  `useBluvoFlow` captures its configuration at mount time. Changing props after mount has no effect, remount the component to reinitialize.
</Tip>

### Render OAuth States

Use the hook's boolean properties to conditionally render each phase of the flow:

<Tip>
  Each `walletId` must be unique per user **and** per exchange. See [Wallet Id](/learn/wallet-id) for the recommended pattern.
</Tip>

```typescript theme={null}
export default function ConnectExchange() {
    const flow = useBluvoFlow({ /* ... config from above */ });

    const [selectedExchange, setSelectedExchange] = React.useState<string>("");

    // Load available exchanges on mount
    React.useEffect(() => {
        flow.listExchanges("live");
    }, []);

    // Generate a new wallet ID, or use one from a previous session
    const getWalletId = () => {
        const stored = localStorage.getItem("walletId");
        if (stored) return stored;
        return crypto.randomUUID();
    };

    // Step 1: Exchange selector
    if (flow.isIdle || flow.isExchangesReady) {
        return (
            <div>
                <h2>Connect your exchange</h2>

                {flow.isExchangesLoading && <p>Loading exchanges...</p>}
                {flow.exchangesError && <p>Failed to load exchanges</p>}

                {flow.exchanges.length > 0 && (
                    <>
                        <select
                            value={selectedExchange}
                            onChange={(e) => setSelectedExchange(e.target.value)}
                        >
                            <option value="">Select an exchange</option>
                            {flow.exchanges.map((exchange) => (
                                <option key={exchange.id} value={exchange.id}>
                                    {exchange.name}
                                </option>
                            ))}
                        </select>
                        <button
                            disabled={!selectedExchange}
                            onClick={() =>
                                flow.startWithdrawalFlow({
                                    exchange: selectedExchange,
                                    walletId: getWalletId(),
                                })
                            }
                        >
                            Connect
                        </button>
                    </>
                )}
            </div>
        );
    }

    // Step 2: OAuth in progress, popup is open
    if (flow.isOAuthPending) {
        return (
            <div>
                <p>Complete authentication in the popup window...</p>
                <button onClick={() => flow.cancel()}>Cancel</button>
            </div>
        );
    }

    // Step 3: User closed the popup
    if (flow.isOAuthWindowBeenClosedByTheUser) {
        return (
            <div>
                <p>Authentication was cancelled.</p>
                <button onClick={() => flow.startWithdrawalFlow({
                    exchange: selectedExchange,
                    walletId: getWalletId(),
                })}>
                    Try Again
                </button>
                <button onClick={() => flow.cancel()}>Start Over</button>
            </div>
        );
    }

    // Step 4: OAuth error
    if (flow.isOAuthError) {
        return (
            <div>
                <p>Connection failed: {flow.error?.message}</p>
                {!flow.isOAuthFatal ? (
                    <button onClick={() => flow.startWithdrawalFlow({
                        exchange: selectedExchange,
                        walletId: getWalletId(),
                    })}>
                        Try Again
                    </button>
                ) : null}
                <button onClick={() => flow.cancel()}>Start Over</button>
            </div>
        );
    }

    // Step 5: Loading wallet balances
    if (flow.isWalletLoading) {
        return <p>Loading your balances...</p>;
    }

    // Step 6: Wallet ready, OAuth flow complete
    if (flow.isWalletReady) {
        return (
            <div>
                <h2>Connected</h2>
                {flow.walletBalances.map((balance) => (
                    <div key={balance.asset}>
                        {balance.asset}: {balance.free}
                    </div>
                ))}
            </div>
        );
    }

    return null;
}
```

***

## Error Handling

The OAuth flow has three distinct error scenarios, each requiring different UI treatment:

**Recoverable OAuth error** (`flow.isOAuthError && !flow.isOAuthFatal`), a transient failure like a network timeout or temporary exchange issue. The user can retry by calling `startWithdrawalFlow()` again with the same exchange.

**Fatal OAuth error** (`flow.isOAuthFatal`), the exchange permanently rejected the connection (e.g., account restrictions, unsupported region). Display `flow.error?.message` and offer `flow.cancel()` to reset the flow.

**User closed popup** (`flow.isOAuthWindowBeenClosedByTheUser`), the user manually closed the authentication window. This is not an error, offer a retry button or let them pick a different exchange.

<Tip>
  After `wallet:ready`, errors shift to wallet-specific states. Use `flow.isWalletError` for general wallet errors, `flow.hasWalletNotFoundError` when a [wallet ID](/learn/wallet-id) is no longer valid, and `flow.hasInvalidCredentialsError` when stored tokens have been revoked by the exchange.
</Tip>

***

## What Comes After OAuth

Once `flow.isWalletReady` is `true`, the OAuth flow is complete. From here, the typical withdrawal flow continues:

1. User selects an asset, amount, and destination address
2. Your app calls `flow.requestQuote(...)` to get a real-time quote with fees
3. User confirms, and `flow.executeWithdrawal(quoteId)` submits the transaction
4. If the exchange requires 2FA, the hook surfaces `flow.requires2FA` or `flow.requires2FAMultiStep`
5. Transaction completes at `flow.isWithdrawalComplete`

The state machine handles all of these transitions automatically. For the full withdrawal implementation, see the [code samples](https://github.com/bluvoinc/awesome/).

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Get API Keys" icon="key" href="/api-keys">
    Create your organization and project in the Bluvo Portal
  </Card>

  <Card title="Code Samples" icon="code" href="https://github.com/bluvoinc/awesome/">
    Full working examples for Next.js, React, and more
  </Card>

  <Card title="Encryption & Security" icon="shield-halved" href="/learn/security">
    How Bluvo encrypts and isolates exchange credentials
  </Card>

  <Card title="Supported Exchanges" icon="building-columns" href="/exchanges">
    See which exchanges support OAuth2 connections
  </Card>
</CardGroup>
