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

# Withdrawal 2FA (TOTP)

> Optional per-wallet TOTP second factor required to authorise withdrawals.

Withdrawal 2FA adds a time-based one-time-password (TOTP) second factor on
top of the [EIP-712 withdrawal signature](/concepts/withdrawals/eip712). It is
an **opt-in, per-wallet** control: once a wallet activates it, every subsequent
withdrawal must carry a valid code. The same endpoints serve the frontend
(Privy JWT) and API-key integrators, so a partner arms 2FA here and then passes
the code in the withdraw request.

<Note>
  **Opt-in.** A wallet with no active 2FA can withdraw without a code (subject
  to the [address whitelist](/concepts/withdrawals/address-whitelist) and AML
  checks). 2FA and the whitelist are independent — neither implies the other.
</Note>

## When it is enforced

The withdraw-time gate runs only when the platform has withdrawal-security
enforcement enabled (a server-side flag, off by default during rollout) **and**
the caller is not a `partner_sso` integration (SSO partners are exempt — they
carry their own KYB-level controls). Setup/management endpoints below are always
available so a wallet can arm 2FA ahead of enforcement.

## Endpoints

| Method & path                                   | Scope            | Rate limit | Purpose                                          |
| ----------------------------------------------- | ---------------- | ---------- | ------------------------------------------------ |
| `GET /api/me/withdrawal-security/totp/status`   | `portfolio:read` | 60/min     | Is 2FA active?                                   |
| `POST /api/me/withdrawal-security/totp/setup`   | `vault:write`    | 10/min     | Begin setup — returns secret + backup codes once |
| `POST /api/me/withdrawal-security/totp/confirm` | `vault:write`    | 5/min      | Activate with a code                             |
| `POST /api/me/withdrawal-security/totp/disable` | `vault:write`    | 5/min      | Remove 2FA (step-up)                             |

For `multi_wallet` API keys every call needs the
[`X-User-Wallet`](/auth/api-keys) header naming the acting wallet; reads need
`portfolio:read`, all mutations need `vault:write`.

## Setup flow

<Steps>
  <Step title="Begin setup">
    `POST /api/me/withdrawal-security/totp/setup` returns the provisioning URI,
    the base32 secret, and ten one-time backup codes — **shown once, never
    again**. The secret is stored *pending* (not yet active).

    ```json Response theme={null}
    {
      "otpauthUri": "otpauth://totp/PredictStreet:0x1234…?secret=JBSWY3DPEHPK3PXP&issuer=PredictStreet",
      "secret": "JBSWY3DPEHPK3PXP",
      "backupCodes": ["a1b2c3d4e5f60718", "…(10 total)…"]
    }
    ```

    Render `otpauthUri` as a QR code for the user's authenticator app, or show
    `secret` for manual entry. Persist the backup codes somewhere safe — each
    authorises exactly one withdrawal if the authenticator is lost.
  </Step>

  <Step title="Confirm">
    `POST /api/me/withdrawal-security/totp/confirm` with the current 6-digit
    code from the app activates 2FA. Backup codes are **not** accepted here —
    you confirm with the authenticator you just scanned.

    ```json Body theme={null}
    { "code": "123456" }
    ```

    Returns `{ "configured": true }`. A wrong code → `403 totp_invalid`; if
    there is no pending setup → `403 totp_setup_not_pending`.
  </Step>
</Steps>

Re-running `setup` while 2FA is already active is refused with
`409 totp_already_configured` — disable first (a hijacked session must not be
able to silently rotate the secret).

## Using 2FA at withdraw time

When the wallet has 2FA active, include `totpCode` in the withdraw request
([`POST /api/withdrawals/request`](/concepts/withdrawals/eip712#request)):

```json theme={null}
{
  "amount": "100",
  "destination": "0x…",
  "salt": "<decimal-uint256>",
  "expiry": 1729511712,
  "userSig": "0x…",
  "totpCode": "123456"
}
```

`totpCode` is a 6-digit TOTP **or** a 16-hex backup code. It is verified only
after every other compliance check passes, so a single-use code is never burned
on a withdrawal that would have been rejected anyway. Codes are single-use:

* A matched TOTP time-step is recorded, so the same code (or its clock-drift
  twin) cannot be replayed on a second withdrawal — even under a concurrent
  double-submit.
* A backup code is consumed on first use.

### Gate errors (403)

| Code                  | When                                          |
| --------------------- | --------------------------------------------- |
| `totp_required`       | 2FA is active but `totpCode` was omitted      |
| `totp_invalid`        | Code wrong, expired, or already used (replay) |
| `totp_not_configured` | 2FA is not active on this wallet              |

All are `403` (never `401`) — the session is authenticated; only the second
factor failed. A `401` would be read by the frontend as a dead session and log
the user out for a mistyped code.

## Disabling

`POST /api/me/withdrawal-security/totp/disable` deletes 2FA but is a **step-up**:
it requires a valid current TOTP or backup code, so a hijacked session cannot
silently remove the factor. After disabling, withdrawals are blocked until 2FA
is set up again from scratch.

```json Body theme={null}
{ "code": "123456" }
```

## API-key callers

A `multi_wallet` partner provisions 2FA per end-user wallet via `X-User-Wallet`

* `vault:write`. The acting wallet must already exist on the platform — on the
  very first request for a freshly-seen wallet, setup may return
  `409 account_provisioning` while partner-backed onboarding finishes writing the
  user record. This is transient: retry after a moment (it converges in seconds,
  the same as the first order/vault call for a new wallet).

## Next

<CardGroup cols={2}>
  <Card title="Address whitelist" icon="address-book" href="/concepts/withdrawals/address-whitelist">
    Restrict withdrawals to pre-approved destinations.
  </Card>

  <Card title="Withdrawals overview" icon="money-bill-transfer" href="/concepts/withdrawals/overview">
    The full dual-sig withdrawal flow.
  </Card>

  <Card title="Error codes" icon="triangle-exclamation" href="/errors/codes">
    Every withdrawal-security code in one table.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/openapi">
    Withdrawal Security endpoints.
  </Card>
</CardGroup>
