Withdrawals
Withdraw managed wallet funds with email OTP verification.
Withdrawals currently require an email-login account and a one-time code sent to that account's verified email address. Wallet-login accounts cannot withdraw through Terminal or the Unified API. The destination is resolved server-side from the account's registered withdrawal wallet; clients cannot provide or override it, so a leaked API key cannot redirect funds.
Terminal keeps its browser-facing route at POST /api/withdraw, which authenticates the Terminal session and forwards the request to the Unified API route documented below. External integrations call POST /wallet/withdraw directly with an API key.
Rollout: Email OTP protection is enabled by default. Set
FEATURE_WITHDRAWAL_OTP=falseonly for an emergency rollback to the legacy amount-only contract.
Step 1 — POST /wallet/withdraw/otp
Request a six-digit code bound to the exact amount and server-resolved destination:
curl -X POST https://api.superior.trade/wallet/withdraw/otp \
-H "x-api-key: st_live_..." \
-H "content-type: application/json" \
-d '{ "amount_usd": 100.0 }'
wget -qO- \
--method=POST \
--header='x-api-key: st_live_...' \
--header='content-type: application/json' \
--body-data='{ "amount_usd": 100.0 }' \
https://api.superior.trade/wallet/withdraw/otp$response = Invoke-RestMethod `
-Method POST `
-Uri "https://api.superior.trade/wallet/withdraw/otp" `
-Headers @{ "x-api-key" = "st_live_..."; "content-type" = "application/json" } `
-Body '{ "amount_usd": 100.0 }'
$response | ConvertTo-Json{
"challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8",
"masked_email": "o******r@example.com",
"expires_at": "2026-09-07T09:30:00Z",
"resend_available_at": "2026-09-07T09:21:00Z"
}
The code expires after 10 minutes and permits five attempts. Requesting again after the 60-second cooldown rotates the code; at most five emails may be sent in six hours.
Step 2 — POST /wallet/withdraw
curl -X POST https://api.superior.trade/wallet/withdraw \
-H "x-api-key: st_live_..." \
-H "content-type: application/json" \
-H "Idempotency-Key: wd_20260730_001" \
-d '{ "amount_usd": 100.0, "challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8", "code": "123456" }'
wget -qO- \
--method=POST \
--header='x-api-key: st_live_...' \
--header='content-type: application/json' \
--header='Idempotency-Key: wd_20260730_001' \
--body-data='{ "amount_usd": 100.0, "challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8", "code": "123456" }' \
https://api.superior.trade/wallet/withdraw$response = Invoke-RestMethod `
-Method POST `
-Uri "https://api.superior.trade/wallet/withdraw" `
-Headers @{ "x-api-key" = "st_live_..."; "content-type" = "application/json"; "Idempotency-Key" = "wd_20260730_001" } `
-Body '{ "amount_usd": 100.0, "challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8", "code": "123456" }'
$response | ConvertTo-Json{ "amount_usd": 100.0, "challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8", "code": "123456" }
Response - 202
{
"id": "wd_01j9ab...",
"status": "processing",
"amount_usd": 100.0,
"destination_wallet": "0xVerifiedLoginWallet...",
"created_at": "2026-07-30T09:20:00Z"
}
Retries are safe and required. Send an Idempotency-Key header for every withdrawal request. Keys are scoped to your account and retained for 24 hours; if a client loses the 202 response and retries with the same key and body, the API returns the original withdrawal instead of creating a second payout. Reusing the same key with a different body returns 409 idempotency_key_conflict.
GET/wallet/withdrawals/:id
Read the latest durable withdrawal state. This database-backed route remains available when the live wallet provider is not configured and does not itself trigger venue reconciliation:
{
"id": "wd_01j9ab...",
"status": "broadcast",
"amount_usd": 100.0,
"asset": "USDC",
"chain": "arbitrum",
"destination_wallet": "0xVerifiedLoginWallet...",
"tx_hash": "0xabc...",
"created_at": "2026-07-30T09:20:00Z",
"updated_at": "2026-07-30T09:20:41Z"
}
status: processing -> broadcast -> confirmed, or failed with an error object.
GET/wallet/withdrawals
List recent withdrawals with limit/cursor:
{
"withdrawals": [
{ "id": "wd_01j9ab...", "status": "confirmed", "amount_usd": 100.0, "tx_hash": "0xabc..." }
],
"next_cursor": null
}
Errors
| Status | Code | |
|---|---|---|
| 400 | idempotency_key_required | Missing the header |
| 400 | withdrawal_otp_required / invalid_code | Challenge or six-digit code missing or invalid |
| 403 | withdrawal_email_login_required | Terminal session belongs to a wallet-login account; use an email-login account |
| 400 | insufficient_held_balance | More than held_usd; stop/deallocate first |
| 409 | withdrawal_destination_unavailable / withdrawal_challenge_mismatch | Registered destination unavailable or amount changed |
| 409 | idempotency_key_conflict / withdrawal_state_conflict | Reuse the key only for the same body; retry a conflicting withdrawal state shortly |
| 410 | code_expired | Request a new code |
| 429 | resend_cooldown / email_limit_reached / attempt_limit_reached | Retry only after the indicated limit resets |
| 503 | email_delivery_failed / withdrawal_otp_unavailable | Verification service temporarily unavailable |
| 503 | wallet_repository_unavailable | Wallet storage is temporarily unavailable; retry later with the same key |