Opt-in. A wallet with no active 2FA can withdraw without a code (subject
to the address whitelist and AML
checks). 2FA and the whitelist are independent — neither implies the other.
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 apartner_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) |
multi_wallet API keys every call needs the
X-User-Wallet header naming the acting wallet; reads need
portfolio:read, all mutations need vault:write.
Setup flow
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).Response
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.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.Body
{ "configured": true }. A wrong code → 403 totp_invalid; if
there is no pending setup → 403 totp_setup_not_pending.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, includetotpCode in the withdraw request
(POST /api/withdrawals/request):
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 |
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.
Body
API-key callers
Amulti_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 return409 account_provisioningwhile 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
Address whitelist
Restrict withdrawals to pre-approved destinations.
Withdrawals overview
The full dual-sig withdrawal flow.
Error codes
Every withdrawal-security code in one table.
API reference
Withdrawal Security endpoints.