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

# Error Handling

> Handle errors, exchange-specific 2FA behavior, and multi-step MFA verification in the Bluvo SDK state machine

## Overview

In the Bluvo state machine, errors are **states**, not exceptions. When something goes wrong, the flow transitions to an error state (e.g., `withdraw:error2FA`), and the hook exposes it as a boolean (e.g., `flow.requires2FA`). Your UI reacts to these booleans the same way it reacts to any other state.

Every error state has a defined recovery action, submit a code, retry, adjust parameters, or cancel. The hook also provides detection helpers like `hasAmountError` and `hasAddressError` that inspect error messages to help you show targeted UI.

<Tip>
  For the full list of states, booleans, and transitions, see the [State Machine Reference](/learn/sdk/state-machine). This page focuses on **what to do** when things go wrong.
</Tip>

***

## Error Quick Reference

### Connection Errors

| Hook Boolean                       | State                            | Severity    | Recovery Action                                            |
| ---------------------------------- | -------------------------------- | ----------- | ---------------------------------------------------------- |
| `isOAuthError`                     | `oauth:error` or `oauth:fatal`   | Varies      | Retry `startWithdrawalFlow()` (if not fatal) or `cancel()` |
| `isOAuthFatal`                     | `oauth:fatal`                    | Fatal       | `cancel()`, exchange rejected the connection               |
| `isOAuthWindowBeenClosedByTheUser` | `oauth:window_closed_by_user`    | User action | Retry `startWithdrawalFlow()` or `cancel()`                |
| `isWalletConnectionInvalid`        | `oauth:fatal` or `qrcode:fatal`  | Fatal       | `cancel()` and start over                                  |
| `isQRCodeError`                    | `qrcode:error` or `qrcode:fatal` | Varies      | `refreshQRCode()` (if not fatal) or `cancel()`             |
| `isQRCodeTimeout`                  | `qrcode:timeout`                 | Recoverable | `refreshQRCode()`                                          |

### Wallet Errors

| Hook Boolean                 | State                                 | Severity | Recovery Action                                          |
| ---------------------------- | ------------------------------------- | -------- | -------------------------------------------------------- |
| `isWalletError`              | `wallet:error`                        | Error    | `cancel()` and retry flow                                |
| `hasWalletNotFoundError`     | `wallet:error` + "not found"          | Fatal    | Wallet deleted, create new [wallet ID](/learn/wallet-id) |
| `hasInvalidCredentialsError` | `wallet:error` + "invalid credential" | Fatal    | Tokens revoked, user must re-authenticate                |

### Quote Errors

| Hook Boolean      | State                             | Severity   | Recovery Action                                           |
| ----------------- | --------------------------------- | ---------- | --------------------------------------------------------- |
| `isQuoteError`    | `quote:error`                     | Error      | Retry `requestQuote()` with corrected params              |
| `isQuoteExpired`  | `quote:expired`                   | Expired    | Call `requestQuote()` again                               |
| `hasAmountError`  | `quote:error` or `withdraw:fatal` | Validation | Show min/max from `quote.additionalInfo`, let user adjust |
| `hasAddressError` | `quote:error` or `withdraw:fatal` | Validation | Prompt user to check destination address                  |
| `hasNetworkError` | `quote:error` or `withdraw:fatal` | Validation | Show supported networks, let user re-select               |

### Withdrawal Errors

| Hook Boolean             | State                            | Severity       | Recovery Action                                                 |
| ------------------------ | -------------------------------- | -------------- | --------------------------------------------------------------- |
| `requires2FA`            | `withdraw:error2FA`              | Requires input | `submit2FA(code)`                                               |
| `requires2FAMultiStep`   | `withdraw:error2FAMultiStep`     | Requires input | `submit2FAMultiStep(stepType, code)` / `pollFaceVerification()` |
| `requiresSMS`            | `withdraw:errorSMS`              | Requires input | `submit2FA(code)` (SMS code)                                    |
| `requiresKYC`            | `withdraw:errorKYC`              | Blocking       | Show "Complete KYC on exchange" message                         |
| `hasInsufficientBalance` | `withdraw:errorBalance`          | Blocking       | Show balance, let user adjust amount                            |
| `canRetry`               | `withdraw:retrying`              | Auto-retry     | Show spinner, retry is automatic                                |
| `isWithdrawBlocked`      | `withdraw:blocked`               | Fatal          | Show reason from `flow.error?.message`                          |
| `hasFatalError`          | `withdraw:fatal`                 | Fatal          | Check `requiresValid2FAMethod`; show `flow.error?.message`      |
| `requiresValid2FAMethod` | `withdraw:fatal` + valid methods | Fatal          | Show supported 2FA methods from `flow.valid2FAMethods`          |

***

## Exchange-Specific 2FA

<Warning>
  2FA behavior is **completely different** between exchanges. Your UI must account for this, do not assume all exchanges use the same 2FA pattern. The hook booleans tell you exactly which path to render.
</Warning>

| Auth Method | 2FA Type       | Hook Booleans That Fire                          | Notes                                                                      |
| ----------- | -------------- | ------------------------------------------------ | -------------------------------------------------------------------------- |
| OAuth       | None           | Neither `requires2FA` nor `requires2FAMultiStep` | No 2FA for withdrawals. Hide all 2FA UI.                                   |
| OAuth       | Single-step    | `requires2FA` only                               | One TOTP code → `submit2FA(code)`. `requires2FAMultiStep` never fires.     |
| QR Code     | Multi-step MFA | `requires2FAMultiStep` only                      | Step-by-step verification. `requires2FA` never fires. See deep dive below. |

### Handling All Three in Your UI

```typescript theme={null}
// This single block handles all exchanges correctly
if (flow.requires2FA) {
  // single 2FA input
  return <SingleCodeInput onSubmit={(code) => flow.submit2FA(code)} />;
}

if (flow.requires2FAMultiStep) {
  // multi-step MFA
  return <MultiStepMFA flow={flow} />;
}

if (flow.isWithdrawProcessing) {
  // no 2FA, just processing
  return <Spinner label="Processing withdrawal..." />;
}
```

***

## Multi-Step 2FA Deep Dive

When `requires2FAMultiStep` is `true`, the exchange requires multiple verification steps before confirming the withdrawal. This is currently used by **Binance**.

### Step Types

| Step Type      | Description                        | Verification Method                                      |
| -------------- | ---------------------------------- | -------------------------------------------------------- |
| `GOOGLE`       | Google Authenticator TOTP          | `submit2FAMultiStep('GOOGLE', code)`                     |
| `EMAIL`        | Email verification code            | `submit2FAMultiStep('EMAIL', code)`                      |
| `SMS`          | SMS verification code              | `submit2FAMultiStep('SMS', code)`                        |
| `FACE`         | Biometric face verification via QR | `pollFaceVerification()`, user scans QR on mobile        |
| `ROAMING_FIDO` | Passkey / security key             | `pollRoamingFidoVerification()`, user verifies on device |

### Relation Types

The `multiStep2FARelation` field determines how steps combine:

* **`AND`**, All required steps must be verified. Show all step inputs simultaneously.
* **`OR`**, Any one required step is sufficient. Show options and let user pick.

### Verification Source of Truth

<Warning>
  Use `mfa.verified` (available as `flow.mfaVerified`) as the **primary source of truth** for whether a step is verified, not `step.status`. The `mfa.verified` object is updated by the server and reflects the actual verification state. The step `status` field may lag behind.
</Warning>

The hook provides convenience booleans that check `mfa.verified` first, falling back to `step.status`:

```typescript theme={null}
flow.isGoogleStepVerified  // mfa.verified.GOOGLE === true || step.status === 'success'
flow.isEmailStepVerified   // mfa.verified.EMAIL === true  || step.status === 'success'
flow.isFaceStepVerified    // mfa.verified.FACE === true   || step.status === 'success'
flow.isSmsStepVerified     // mfa.verified.SMS === true    || step.status === 'success'
```

### Complete Flow

1. Withdrawal execution triggers `requires2FAMultiStep`
2. Check `flow.multiStep2FASteps` for required step types
3. For each step, render the appropriate input:
   * `GOOGLE` / `EMAIL` / `SMS`, code input → `submit2FAMultiStep(type, code)`
   * `FACE`, show QR code from `flow.faceQrCodeUrl` → user scans → `pollFaceVerification()`
   * `ROAMING_FIDO`, prompt user → `pollRoamingFidoVerification()`
4. After each submission, the server returns updated step statuses
5. When `flow.allMultiStep2FAStepsVerified` becomes `true`, `isReadyToConfirm` fires
6. Call `flow.confirmWithdrawal()` to finalize

### FACE Verification

The FACE step requires the user to scan a QR code with their exchange mobile app and complete biometric verification:

```typescript theme={null}
if (flow.hasFaceStep && !flow.isFaceStepVerified) {
  return (
    <div>
      <p>Scan this QR code with your Binance app to verify your identity</p>
      <QRCode value={flow.faceQrCodeUrl} />
      {flow.faceQrCodeExpiresAt && (
        <p>Expires: {new Date(flow.faceQrCodeExpiresAt).toLocaleTimeString()}</p>
      )}
      <button onClick={() => flow.pollFaceVerification()}>
        I've completed verification
      </button>
    </div>
  );
}
```

### Code Example

<CodeGroup>
  ```typescript Multi-Step MFA Component theme={null}
  function MultiStepMFA({ flow }: { flow: UseBluvoFlowHook }) {
    const [googleCode, setGoogleCode] = React.useState("");
    const [emailCode, setEmailCode] = React.useState("");
    const [smsCode, setSmsCode] = React.useState("");

    // All steps verified, show confirm button
    if (flow.isReadyToConfirm) {
      return (
        <div>
          <p>All verification steps complete.</p>
          <button onClick={() => flow.confirmWithdrawal()}>
            Confirm Withdrawal
          </button>
        </div>
      );
    }

    const relation = flow.multiStep2FARelation; // 'AND' or 'OR'

    return (
      <div>
        <h3>Verify your identity {relation === 'AND' ? '(all required)' : '(any one)'}</h3>

        {flow.hasGoogleStep && !flow.isGoogleStepVerified && (
          <div>
            <label>Google Authenticator</label>
            <input value={googleCode} onChange={(e) => setGoogleCode(e.target.value)} />
            <button onClick={() => flow.submit2FAMultiStep('GOOGLE', googleCode)}>
              Verify
            </button>
          </div>
        )}
        {flow.isGoogleStepVerified && <p>Google Authenticator: Verified</p>}

        {flow.hasEmailStep && !flow.isEmailStepVerified && (
          <div>
            <label>Email Code</label>
            <input value={emailCode} onChange={(e) => setEmailCode(e.target.value)} />
            <button onClick={() => flow.submit2FAMultiStep('EMAIL', emailCode)}>
              Verify
            </button>
          </div>
        )}
        {flow.isEmailStepVerified && <p>Email: Verified</p>}

        {flow.hasSmsStep && !flow.isSmsStepVerified && (
          <div>
            <label>SMS Code</label>
            <input value={smsCode} onChange={(e) => setSmsCode(e.target.value)} />
            <button onClick={() => flow.submit2FAMultiStep('SMS', smsCode)}>
              Verify
            </button>
          </div>
        )}
        {flow.isSmsStepVerified && <p>SMS: Verified</p>}

        {flow.hasFaceStep && !flow.isFaceStepVerified && (
          <div>
            <p>Scan with your exchange app:</p>
            <img src={flow.faceQrCodeUrl} alt="Face verification QR" />
            <button onClick={() => flow.pollFaceVerification()}>
              I've completed face verification
            </button>
          </div>
        )}
        {flow.isFaceStepVerified && <p>Face Verification: Verified</p>}
      </div>
    );
  }
  ```

  ```typescript Usage in Withdrawal Flow theme={null}
  function WithdrawalFlow() {
    const flow = useBluvoFlow({ /* config */ });

    if (flow.requires2FAMultiStep) {
      return <MultiStepMFA flow={flow} />;
    }

    if (flow.requires2FA) {
      return <SingleCodeInput onSubmit={(code) => flow.submit2FA(code)} />;
    }

    // ... rest of withdrawal UI
  }
  ```
</CodeGroup>

***

## Recovery Actions by Error State

<AccordionGroup>
  <Accordion title="withdraw:error2FA, Single-Step 2FA Required">
    The exchange (typically Coinbase) requires a TOTP code.

    **Recovery:** Call `flow.submit2FA(code)` with the user's authenticator code.

    **Track:** `flow.invalid2FAAttempts` increments on each invalid submission. Consider showing a warning after 2-3 attempts.

    ```typescript theme={null}
    if (flow.requires2FA) {
      return (
        <div>
          <input placeholder="Enter 2FA code" onChange={(e) => setCode(e.target.value)} />
          {flow.invalid2FAAttempts > 0 && (
            <p>Invalid code. Attempts: {flow.invalid2FAAttempts}</p>
          )}
          <button onClick={() => flow.submit2FA(code)}>Submit</button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:error2FAMultiStep, Multi-Step MFA Required">
    The exchange requires multiple verification steps.

    **Recovery:** Use `flow.submit2FAMultiStep(stepType, code)` for code-based steps and `flow.pollFaceVerification()` for biometric steps. Once `flow.allMultiStep2FAStepsVerified` is `true`, call `flow.confirmWithdrawal()`.

    See the [Multi-Step 2FA Deep Dive](#multi-step-2fa-deep-dive) above for the complete implementation.
  </Accordion>

  <Accordion title="withdraw:errorSMS, SMS Verification Required">
    The exchange sent an SMS code to the user's registered phone.

    **Recovery:** Call `flow.submit2FA(code)` with the SMS code the user received.

    ```typescript theme={null}
    if (flow.requiresSMS) {
      return (
        <div>
          <p>Enter the SMS code sent to your phone</p>
          <input onChange={(e) => setCode(e.target.value)} />
          <button onClick={() => flow.submit2FA(code)}>Verify</button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:errorKYC, KYC Completion Required">
    The exchange requires the user to complete identity verification before withdrawals are allowed.

    **Recovery:** This cannot be resolved via the SDK. Direct the user to complete KYC on the exchange's website or app, then retry the withdrawal later.

    ```typescript theme={null}
    if (flow.requiresKYC) {
      return (
        <div>
          <p>Your exchange account requires identity verification before withdrawals.</p>
          <p>Please complete KYC on the exchange, then try again.</p>
          <button onClick={() => flow.cancel()}>Close</button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:errorBalance, Insufficient Balance">
    The withdrawal amount exceeds the available balance.

    **Recovery:** Show the user their current balance (from `flow.walletBalances`), let them adjust the amount, and request a new quote.

    ```typescript theme={null}
    if (flow.hasInsufficientBalance) {
      return (
        <div>
          <p>Insufficient balance for this withdrawal.</p>
          <p>Available: {flow.walletBalances.find(b => b.asset === selectedAsset)?.balance}</p>
          <button onClick={() => flow.requestQuote({ asset, amount: newAmount, destinationAddress })}>
            Try Different Amount
          </button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:blocked, Withdrawal Blocked">
    The exchange has blocked this withdrawal. This is non-recoverable via the SDK.

    **Recovery:** Display `flow.error?.message` which contains the reason from the exchange. The user may need to resolve account-level restrictions.

    ```typescript theme={null}
    if (flow.isWithdrawBlocked) {
      return (
        <div>
          <p>Withdrawal blocked: {flow.error?.message}</p>
          <button onClick={() => flow.cancel()}>Close</button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:fatal, Fatal Withdrawal Error">
    An unrecoverable error occurred. Check `requiresValid2FAMethod` for a special case where the user's 2FA method isn't supported.

    **Recovery:**

    * If `flow.requiresValid2FAMethod` is `true`: show `flow.valid2FAMethods` and instruct the user to enable a supported method on the exchange.
    * Otherwise: display `flow.error?.message` and offer `flow.cancel()`.

    ```typescript theme={null}
    if (flow.hasFatalError) {
      if (flow.requiresValid2FAMethod) {
        return (
          <div>
            <p>{flow.error?.message}</p>
            <p>Supported methods: {flow.valid2FAMethods?.join(', ')}</p>
          </div>
        );
      }
      return (
        <div>
          <p>Withdrawal failed: {flow.error?.message}</p>
          <button onClick={() => flow.cancel()}>Start Over</button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="withdraw:retrying, Auto-Retry in Progress">
    The SDK is automatically retrying the withdrawal after a transient failure.

    **Recovery:** No action needed. Show a spinner. Track progress with `flow.retryAttempts` / `flow.maxRetryAttempts`.

    ```typescript theme={null}
    if (flow.canRetry) {
      return (
        <div>
          <Spinner />
          <p>Retrying... ({flow.retryAttempts} / {flow.maxRetryAttempts})</p>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="quote:expired, Quote Expired">
    The quote's TTL has elapsed and it can no longer be used for withdrawal.

    **Recovery:** Call `flow.requestQuote()` with the same parameters to get a fresh quote.

    ```typescript theme={null}
    if (flow.isQuoteExpired) {
      return (
        <div>
          <p>Quote expired. Fetching a new one...</p>
          <button onClick={() => flow.requestQuote({ asset, amount, destinationAddress })}>
            Get New Quote
          </button>
        </div>
      );
    }
    ```

    <Tip>
      Enable `autoRefreshQuotation: true` in hook options to automatically refresh quotes before they expire.
    </Tip>
  </Accordion>

  <Accordion title="oauth:error, Recoverable OAuth Error">
    A transient failure occurred during the OAuth flow (network timeout, temporary exchange issue).

    **Recovery:** Call `startWithdrawalFlow()` again with the same exchange and wallet ID.

    ```typescript theme={null}
    if (flow.isOAuthError && !flow.isOAuthFatal) {
      return (
        <div>
          <p>Connection failed: {flow.error?.message}</p>
          <button onClick={() => flow.startWithdrawalFlow({ exchange, walletId })}>
            Try Again
          </button>
        </div>
      );
    }
    ```
  </Accordion>

  <Accordion title="oauth:fatal / qrcode:fatal, Fatal Connection Error">
    The exchange permanently rejected the connection. This cannot be retried.

    **Recovery:** Call `flow.cancel()` and let the user start over, potentially with a different exchange.

    ```typescript theme={null}
    if (flow.isWalletConnectionInvalid) {
      return (
        <div>
          <p>Connection rejected: {flow.error?.message}</p>
          <button onClick={() => flow.cancel()}>Start Over</button>
        </div>
      );
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="State Machine Reference" icon="diagram-project" href="/learn/sdk/state-machine">
    Full list of 35 states, transitions, hook booleans, and context data
  </Card>

  <Card title="OAuth2 Integration" icon="right-left" href="/learn/oauth2-integration">
    Step-by-step implementation guide with React and Next.js
  </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>
</CardGroup>
