Binance API Withdraw Permission: Complete Setup Guide
Learn how to safely enable and use Binance API withdraw permission for automated trading, including security best practices and working code examples.
Learn how to safely enable and use Binance API withdraw permission for automated trading, including security best practices and working code examples.
Enabling withdraw permission on a Binance API key is one of the most powerful — and most dangerous — things you can do in crypto automation. Get it right and you unlock fully automated fund management. Get it wrong and you hand an attacker the keys to your wallet. Most traders enable it without understanding exactly what they're authorizing, which is how accounts get drained. This guide walks through every step of setting up Binance API withdraw permission correctly, with real code and real security considerations.
When you create an API key on Binance, you get three core permission toggles: Read, Trading, and Withdraw. Read lets the key fetch account balances and order history. Trading lets it place and cancel orders. Withdraw permission goes further — it authorizes the key to initiate actual fund transfers out of your Binance account to external wallet addresses.
This is fundamentally different from trading permission. A compromised trading key can wreck your positions, but a compromised withdraw key can empty your account permanently. Other exchanges handle this differently — on Bybit and OKX, withdrawal API access requires separate whitelisting steps and additional confirmation layers. Binance also offers IP whitelisting as a critical safeguard, which we'll cover in detail.
NEVER enable withdraw permission on an API key unless your use case specifically requires automated withdrawals. Trading bots, signal followers, and portfolio trackers have zero need for withdraw access. Only enable it for treasury automation, multi-exchange rebalancing bots, or custom withdrawal workflows.
Before touching any code, the API key setup in Binance's dashboard is where most mistakes happen. Follow this sequence exactly.
The withdrawal address whitelist is a separate account-level setting from API permissions. Even with a withdraw-enabled API key, Binance will only allow withdrawals to addresses you've pre-approved in your account's whitelist. This two-layer system means an attacker with your API key still can't send funds to an arbitrary address — they'd need your email and 2FA to add new addresses first.
The Binance API uses HMAC-SHA256 signatures for authenticated endpoints. Withdraw endpoints are SIGNED, meaning every request must include a timestamp and a signature derived from your secret key. Here's how to set up authentication properly:
import hmac
import hashlib
import time
import requests
from urllib.parse import urlencode
API_KEY = 'your_api_key_here'
SECRET_KEY = 'your_secret_key_here'
BASE_URL = 'https://api.binance.com'
def sign_params(params: dict, secret: str) -> str:
"""Generate HMAC-SHA256 signature for Binance API request."""
query_string = urlencode(params)
signature = hmac.new(
secret.encode('utf-8'),
query_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
def get_headers() -> dict:
return {'X-MBX-APIKEY': API_KEY}
def get_account_info() -> dict:
"""Fetch account balances — verify API key works before attempting withdrawals."""
params = {'timestamp': int(time.time() * 1000)}
params['signature'] = sign_params(params, SECRET_KEY)
response = requests.get(
f'{BASE_URL}/api/v3/account',
headers=get_headers(),
params=params
)
response.raise_for_status()
return response.json()
# Test authentication before proceeding
try:
account = get_account_info()
print(f"Account type: {account['accountType']}")
print(f"Can withdraw: {account['canWithdraw']}")
except requests.exceptions.HTTPError as e:
print(f"Auth failed: {e.response.status_code} - {e.response.text}")
Always verify the `canWithdraw` field in the account response before attempting any withdrawal. Binance can restrict withdrawals at the account level independently of API permissions — for example, if KYC is incomplete or a security hold is active.
def submit_withdrawal(
coin: str,
address: str,
amount: float,
network: str,
address_tag: str = None # Required for XRP, EOS, etc.
) -> dict:
"""
Submit a withdrawal via Binance API.
Address must be pre-approved in Binance withdrawal whitelist.
"""
params = {
'coin': coin,
'address': address,
'amount': amount,
'network': network,
'timestamp': int(time.time() * 1000)
}
if address_tag:
params['addressTag'] = address_tag
params['signature'] = sign_params(params, SECRET_KEY)
response = requests.post(
f'{BASE_URL}/sapi/v1/capital/withdraw/apply',
headers=get_headers(),
params=params
)
# Handle specific Binance error codes
if response.status_code != 200:
error = response.json()
error_code = error.get('code', 0)
if error_code == -3005:
raise ValueError("Insufficient balance for withdrawal")
elif error_code == -3006:
raise ValueError("Withdrawal amount below minimum")
elif error_code == 3102:
raise PermissionError("Address not in withdrawal whitelist")
else:
raise RuntimeError(f"Withdrawal failed: {error.get('msg', 'Unknown error')}")
return response.json() # Returns {'id': 'withdrawal_id_string'}
# Example: withdraw USDT on TRC20 network
try:
result = submit_withdrawal(
coin='USDT',
address='TYour_TRC20_Address_Here',
amount=100.0,
network='TRX'
)
print(f"Withdrawal submitted. ID: {result['id']}")
except PermissionError as e:
print(f"Whitelist error: {e}")
except ValueError as e:
print(f"Amount error: {e}")
except RuntimeError as e:
print(f"API error: {e}")
Submitting a withdrawal is half the job. Production systems need to track withdrawal status through to completion, especially when handling cross-exchange rebalancing between Binance, OKX, or KuCoin. Network congestion, incorrect addresses, and chain-specific delays all require proper status polling.
import time
def get_withdrawal_history(coin: str = None, days_back: int = 7) -> list:
"""Fetch withdrawal history. Max lookback is 90 days."""
end_time = int(time.time() * 1000)
start_time = end_time - (days_back * 24 * 60 * 60 * 1000)
params = {
'startTime': start_time,
'endTime': end_time,
'timestamp': int(time.time() * 1000)
}
if coin:
params['coin'] = coin
params['signature'] = sign_params(params, SECRET_KEY)
response = requests.get(
f'{BASE_URL}/sapi/v1/capital/withdraw/history',
headers=get_headers(),
params=params
)
response.raise_for_status()
return response.json()
def poll_withdrawal_status(withdrawal_id: str, max_attempts: int = 20) -> str:
"""
Poll withdrawal status until confirmed or failed.
Status codes: 0=Email sent, 1=Cancelled, 2=Awaiting approval,
3=Rejected, 4=Processing, 5=Failure, 6=Completed
"""
STATUS_MAP = {
0: 'email_sent', 1: 'cancelled', 2: 'awaiting_approval',
3: 'rejected', 4: 'processing', 5: 'failed', 6: 'completed'
}
TERMINAL_STATES = {1, 3, 5, 6}
for attempt in range(max_attempts):
history = get_withdrawal_history(days_back=1)
for record in history:
if record.get('id') == withdrawal_id:
status_code = record['status']
status = STATUS_MAP.get(status_code, 'unknown')
print(f"Attempt {attempt+1}: {status} | TxID: {record.get('txId', 'pending')}")
if status_code in TERMINAL_STATES:
return status
break
time.sleep(30) # Poll every 30 seconds
return 'timeout'
# Usage
withdrawal_id = 'abc123xyz'
final_status = poll_withdrawal_status(withdrawal_id)
print(f"Final withdrawal status: {final_status}")
A withdraw-enabled API key is a high-value target. The security model needs to be layered — no single control is sufficient on its own. Platforms like Bybit and Bitget have similar security architectures, but Binance's implementation has some specific quirks worth knowing.
| Control | How to Enable | Protection Level |
|---|---|---|
| IP Whitelist | API Management → Edit → Restrict access to trusted IPs | Critical |
| Withdrawal Address Whitelist | Security → Withdrawal Whitelist → Enable | Critical |
| API Key Expiry | Set expiration date when creating key | High |
| 2FA on Account | Security → 2-Step Verification | High |
| Email Confirmation | Enabled by default for new withdrawal addresses | Medium |
| Sub-account Isolation | Use sub-accounts for trading vs withdrawal keys | High |
Sub-account isolation is underused and extremely effective. Create a dedicated Binance sub-account for automated withdrawals, keep only the funds needed for current operations in it, and never enable withdraw permission on your main account's API keys. This limits blast radius if a key is ever compromised — the attacker can only access what's in the sub-account.
When building automated trading systems that connect to signal platforms like VoiceOfChain for real-time trade signals, keep the signal-reception layer and the withdrawal-execution layer in completely separate processes with separate API keys. The signal bot needs trading permission only. A separate treasury bot handles fund movements. Never combine them.
Store API keys in environment variables or a secrets manager — never hardcode them. Rotate withdraw-enabled keys every 90 days minimum. Set up Binance account activity alerts via email so any unexpected withdrawal attempt triggers an immediate notification.
Working with the Binance withdrawal API throws a specific set of errors that aren't well-documented. Here are the ones you'll hit in production and what they actually mean:
| Error Code | Message | Fix |
|---|---|---|
| -1022 | Signature for this request is not valid | Check timestamp sync — server clock must be within 1000ms of Binance time |
| -3005 | Insufficient balance | Check available balance minus pending orders |
| -3006 | Amount less than minimum | Check coin-specific minimum withdrawal amounts via /sapi/v1/capital/config/getall |
| 3102 | Address is not in withdrawal address book | Add address to Binance withdrawal whitelist first |
| -1003 | Too many requests | Implement rate limiting — withdrawal endpoint allows 1 req/second |
| -2014 | API key format invalid | Regenerate key — usually caused by whitespace in key string |
The timestamp synchronization error (-1022) is the most common one for developers running in Docker containers or cloud environments. Binance requires your request timestamp to be within 1000ms of their server time. Always fetch the Binance server time at startup via `/api/v3/time` and use it to calculate a time offset for all subsequent requests.
Binance API withdraw permission is powerful infrastructure when used correctly — and a serious liability when it isn't. The pattern that works in production: use withdraw-enabled keys only in isolated sub-accounts, always enforce IP whitelisting, maintain a tight withdrawal address whitelist, and never mix withdrawal logic with trading logic in the same bot. The code examples here give you a working foundation, but adapt them to your specific use case rather than copy-pasting blindly. For the trading and signal side of your automation — where you're acting on real-time market data from platforms like VoiceOfChain — keep those API keys scoped to trading only. Reserve withdraw permission for the treasury layer, keep it small, keep it monitored, and rotate those keys regularly.