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=false only 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:

bash
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 }'
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

bash
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" }'
json
{ "amount_usd": 100.0, "challenge_id": "774b0ee8-8db8-4d76-91a4-d632800e21d8", "code": "123456" }

Response - 202

json
{
  "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:

json
{
  "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:

json
{
  "withdrawals": [
    { "id": "wd_01j9ab...", "status": "confirmed", "amount_usd": 100.0, "tx_hash": "0xabc..." }
  ],
  "next_cursor": null
}

Errors

StatusCode
400idempotency_key_requiredMissing the header
400withdrawal_otp_required / invalid_codeChallenge or six-digit code missing or invalid
403withdrawal_email_login_requiredTerminal session belongs to a wallet-login account; use an email-login account
400insufficient_held_balanceMore than held_usd; stop/deallocate first
409withdrawal_destination_unavailable / withdrawal_challenge_mismatchRegistered destination unavailable or amount changed
409idempotency_key_conflict / withdrawal_state_conflictReuse the key only for the same body; retry a conflicting withdrawal state shortly
410code_expiredRequest a new code
429resend_cooldown / email_limit_reached / attempt_limit_reachedRetry only after the indicated limit resets
503email_delivery_failed / withdrawal_otp_unavailableVerification service temporarily unavailable
503wallet_repository_unavailableWallet storage is temporarily unavailable; retry later with the same key