> ## Documentation Index
> Fetch the complete documentation index at: https://bluvo.mintlify.site/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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.