Short answer: To build a Binance API trading bot in Python, start on the Binance Spot Testnet. Read the symbol's filters from exchangeInfo, round prices and quantities to them, sign requests with an Ed25519 key and validate orders with the test endpoint. Then place, query and cancel them while following order events on the user data stream. Moving to production should only mean changing the base URL and keys, after the bot has been tested and reviewed.
Binance's official Python SDK is the binance-sdk-spot package (version 13.x as of October 2026); the older binance-connector package is marked deprecated on PyPI. This guide uses plain requests and websockets calls so every header and signature is visible. Public calls were run against the testnet on 5 November 2026. Signed calls were run with a malformed placeholder key and against a local mock server, since no credentials appear here, so no order has reached the testnet from this code.
This is a technical guide, not a recommendation to use Binance or any strategy. Availability depends on your jurisdiction and the exchange's terms. In Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign-based crypto-asset platforms directed at Turkish residents as unauthorized service provision, and the Capital Markets Board (SPK) has stated that appearing on its list of operating providers does not mean a firm is authorized. Automated trading can lose money.
Binance testnet: Spot and futures API keys and setup
The Spot Testnet is a separate exchange with virtual funds: log in at testnet.binance.vision (with a GitHub account as of October 2026) and generate a key. Binance recommends Ed25519 keys, which you create yourself and register by uploading the public half; the testnet then issues the API key that goes in the X-MBX-APIKEY header. Production keys are covered in creating a Binance API key.
openssl genpkey -algorithm ed25519 -out test-prv-key.pem
openssl pkey -pubout -in test-prv-key.pem -out test-pub-key.pem
Things to know before you rely on it:
- Base URLs: REST
https://testnet.binance.vision/api, WebSocket APIwss://ws-api.testnet.binance.vision/ws-api/v3, streamswss://stream.testnet.binance.vision/ws. - Only
/apiendpoints exist; wallet and other/sapiendpoints do not. - Data is wiped roughly once a month without prior notice (the testnet changelog lists resets on 9 September and 7 October 2026); API keys survive resets.
- Filters are generally the same as production, but on 24 October 2026 the testnet allowed 50 orders per 10 seconds and 160,000 per day versus 100 and 200,000 in production.
- Testnet data does not match production. Binance's testnet page says limits and filters are generally the same, and makes no promise about prices or liquidity. One reading on 4 November 2026 showed the BTCUSDT last price within a cent of production, but 24-hour volume of about 681 BTC against about 21,959 BTC, and different sizes at the best bid and ask. Do not judge fills or slippage here.
Binance testnet futures: USD-M base URLs
USD-M futures are a separate API with their own test environment. Binance's derivatives documentation, read on 4 November 2026, gives https://demo-fapi.binance.com for REST and wss://demo-fstream.binance.com for WebSocket and says most endpoints work there; its Quick Start points to the Futures Demo Trading page for setting up the key. Paths start with /fapi, not /api, so spot code does not carry over unchanged. Public calls need no key:
import requests
FUTURES_TESTNET = "https://demo-fapi.binance.com"
r = requests.get(f"{FUTURES_TESTNET}/fapi/v1/ticker/price",
params={"symbol": "BTCUSDT"}, timeout=10)
r.raise_for_status()
print(r.json()["price"])
This ran on 4 November 2026. The futures test price differed from the live futures endpoint by about $56 in the same minute, another reason to test behavior and not results. The rest of this guide uses the Spot API.
Exchange info and filters: PRICE_FILTER, LOT_SIZE, NOTIONAL
Every order must pass the symbol's filters, so read them from GET /api/v3/exchangeInfo before you build an order. The three that reject most first attempts:
- PRICE_FILTER: price between
minPriceandmaxPriceand a multiple oftickSize. - LOT_SIZE: quantity between
minQtyandmaxQtyand a multiple ofstepSize. - NOTIONAL: price times quantity between
minNotionalandmaxNotional; market orders, which carry no price, are checked against a reference or recent average price.
This snippet calls only public endpoints, so it runs without keys. It checks connectivity, reads the filters for BTCUSDT and rounds a hypothetical limit price 5% below the last price and a hypothetical quantity down to valid increments with Decimal, never floats:
from decimal import Decimal, ROUND_DOWN
import requests
BASE = "https://testnet.binance.vision" # Spot Testnet REST base URL
http = requests.Session()
def get(path, **params):
r = http.get(BASE + path, params=params, timeout=10)
r.raise_for_status()
return r.json()
def floor_to(value, step):
"""Round down to the exchange's tick or step size."""
step = Decimal(step)
return (Decimal(value) / step).to_integral_value(rounding=ROUND_DOWN) * step
print(get("/api/v3/ping")) # {} means reachable
print(get("/api/v3/time")["serverTime"])
symbol = get("/api/v3/exchangeInfo", symbol="BTCUSDT")["symbols"][0]
filters = {f["filterType"]: f for f in symbol["filters"]}
last = Decimal(get("/api/v3/ticker/price", symbol="BTCUSDT")["price"])
price = floor_to(last * Decimal("0.95"), filters["PRICE_FILTER"]["tickSize"])
qty = floor_to("0.0004321", filters["LOT_SIZE"]["stepSize"])
notional = price * qty
print(symbol["status"], format(price.normalize(), "f"), format(qty.normalize(), "f"), round(notional, 2),
notional >= Decimal(filters["NOTIONAL"]["minNotional"]))
Output on 5 November 2026 (testnet prices are independent of the live market):
{}
1793855824433
TRADING 76439.93 0.00043 32.87 True
On that date BTCUSDT had a tick size of 0.01, a step size of 0.00001 and a minimum notional of 5 USDT on both testnet and production. Read them at runtime; they change.
Signing Binance API requests in Python
Signed endpoints need a timestamp, an optional recvWindow (default 5,000 ms, maximum 60,000) and a signature computed over the URL-encoded parameters. With an Ed25519 key the signature is the base64-encoded signature of that payload:
import base64, os, time, urllib.parse
import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
BASE = "https://testnet.binance.vision"
API_KEY = os.environ["BINANCE_TESTNET_API_KEY"] # testnet key only
with open(os.environ["BINANCE_TESTNET_PRIVATE_KEY"], "rb") as fh:
PRIVATE_KEY = load_pem_private_key(fh.read(), password=None)
class BinanceError(Exception):
"""The API refused the request."""
class UnknownStatus(Exception):
"""Timeout, 5XX, -1006 or -1007: the order may exist. Query before anything else."""
def signed(method, path, **params):
params["timestamp"] = int(time.time() * 1000)
params["recvWindow"] = 5000
payload = urllib.parse.urlencode(params)
params["signature"] = base64.b64encode(PRIVATE_KEY.sign(payload.encode())).decode()
try:
r = requests.request(method, BASE + path, params=params,
headers={"X-MBX-APIKEY": API_KEY}, timeout=10)
except requests.RequestException as err:
raise UnknownStatus(type(err).__name__) from err
if r.status_code >= 500:
raise UnknownStatus(r.status_code)
try:
body = r.json()
except ValueError: # a WAF block can answer in HTML
raise BinanceError(r.status_code, None, r.text[:60]) from None
if r.status_code >= 400:
if body.get("code") in (-1006, -1007): # documented: execution status unknown
raise UnknownStatus(body["code"])
raise BinanceError(r.status_code, body.get("code"), body.get("msg"))
return body
The helper separates a refusal (4XX) from an unknown outcome (a timeout, 5XX, -1006 or -1007), because only the first means the order failed. If you prefer the official SDK, install binance-sdk-spot, point its ConfigurationRestAPI at SPOT_REST_API_TESTNET_URL and pass your private key; its README documents Ed25519 and RSA support. How to store keys outside environment variables in production is covered in API key security for trading bots.
Binance API order example: test, place, query, cancel
Continuing from the two snippets above, this places a limit order with your own client order ID, then queries and cancels it by that ID:
cid = f"demo-{int(time.time())}" # our own client order ID
order = dict(symbol="BTCUSDT", side="BUY", type="LIMIT", timeInForce="GTC",
quantity=format(qty, "f"), price=format(price, "f"), newClientOrderId=cid)
try:
print(signed("POST", "/api/v3/order/test", **order)) # validated, not sent
except (BinanceError, UnknownStatus) as err:
raise SystemExit(f"rejected: {err.args}") # nothing was sent yet
try:
signed("POST", "/api/v3/order", **order)
except BinanceError as err:
print("rejected:", err.args)
except UnknownStatus as err:
print("unknown:", err.args, "looking the order up by client ID")
try:
print(signed("GET", "/api/v3/order", symbol="BTCUSDT", origClientOrderId=cid)["status"])
except (BinanceError, UnknownStatus) as err2: # not found is not proof of failure
print("still unknown:", err2.args, "do not resend; check again later")
else:
print(signed("GET", "/api/v3/order", symbol="BTCUSDT", origClientOrderId=cid)["status"])
print(signed("DELETE", "/api/v3/order", symbol="BTCUSDT", origClientOrderId=cid)["status"])
What each call does, per the Spot API documentation as of October 2026:
| Endpoint | Weight | Notes |
|---|---|---|
POST /api/v3/order/test | 1 | Validates the order and signature without sending it to the matching engine; returns {} |
POST /api/v3/order | 1 | LIMIT needs timeInForce, quantity, price; adds 1 to the unfilled order count |
GET /api/v3/order | 4 | Needs orderId or origClientOrderId |
DELETE /api/v3/order | 1 | Cancels an active order by either ID |
Run with a malformed placeholder key on 5 November 2026, the script stopped at the first call with rejected: (401, -2014, 'API-key format invalid.'). No valid key was used, so the other paths ran against a local mock server that answers as the documentation describes: a resting limit order reports NEW and then CANCELED; a 503, an HTML 502, a read timeout or -1007 on the order call prints the unknown line and then the lookup result; and a lookup that finds nothing prints still unknown and sends nothing. Client order IDs must be unique among open orders, so a retry with the same ID is rejected while the first order is still open, but the ID can be reused once that order has filled, which is why the script never resends on its own. The reasoning is in crypto exchange API integration.
Order updates from the user data stream
A bot should learn about fills from events, not by polling. As of October 2026 Binance delivers account events through the WebSocket API: userDataStream.subscribe after an Ed25519 session.logon, or userDataStream.subscribe.signature, which works with any key type. The listenKey method, deprecated in April 2025, stopped working on 20 February 2026, so older tutorials that call POST /api/v3/userDataStream no longer apply. Note that WebSocket API signatures are computed over parameters sorted by name:
import asyncio, json
import websockets
WS_API = "wss://ws-api.testnet.binance.vision/ws-api/v3"
async def user_events():
params = {"apiKey": API_KEY, "timestamp": int(time.time() * 1000)}
payload = urllib.parse.urlencode(sorted(params.items())) # sorted by name
params["signature"] = base64.b64encode(PRIVATE_KEY.sign(payload.encode())).decode()
async with websockets.connect(WS_API) as ws:
await ws.send(json.dumps({"id": "uds-1", "params": params,
"method": "userDataStream.subscribe.signature"}))
reply = json.loads(await ws.recv())
if reply["status"] >= 400:
raise BinanceError(reply["status"], reply["error"]["code"], reply["error"]["msg"])
while True: # events arrive wrapped in "event"
ev = json.loads(await ws.recv()).get("event", {})
if ev.get("e") == "executionReport":
print(ev["c"], ev["X"], ev["z"]) # client ID, status, filled qty
asyncio.run(user_events())
Each executionReport carries the client order ID, the order status and the cumulative filled quantity. Tested with a well-formed placeholder key, the testnet answered with error -2015, confirming the request format. Market data uses separate streams such as wss://stream.testnet.binance.vision/ws/btcusdt@trade; connections close after 24 hours, so reconnect logic is mandatory. Building a full order book from depth streams is covered in the local order book guide. Limits and reconnects are in Binance WebSocket API and streams.
A minimal Binance trading bot loop in Python
A bot is a loop around these calls: for each new closed candle it asks your rule what to do, sizes the order from the filters and sends it once, under a client order ID derived from the bar. Continuing from the snippets above, this is the smallest version that still follows the rules in this guide. It offers no strategy: decide() returns nothing until you write your own rule.
SYMBOL, INTERVAL, MAX_COST = "BTCUSDT", "1h", Decimal("20") # hypothetical cap in USDT
def decide(closes, in_position):
"""Your written rule goes here. Return "BUY", "SELL" or None. This guide suggests none."""
return None
def step(state):
rows = get("/api/v3/klines", symbol=SYMBOL, interval=INTERVAL, limit=100)
bar = rows[-2][0] # the last row is still forming
if bar == state.get("bar"):
return "no new closed bar"
closes = [Decimal(r[4]) for r in rows[:-1]]
held = state.get("qty")
side = decide(closes, held is not None)
if side is None or (side == "BUY" and held is not None):
state["bar"] = bar
return "no order"
if side == "SELL" and held is None:
state["bar"] = bar
return "nothing to sell"
filters = {f["filterType"]: f for f in get("/api/v3/exchangeInfo", symbol=SYMBOL)["symbols"][0]["filters"]}
qty = held if side == "SELL" else floor_to(MAX_COST / closes[-1], filters["LOT_SIZE"]["stepSize"])
if side == "BUY" and qty * closes[-1] < Decimal(filters["NOTIONAL"]["minNotional"]):
state["bar"] = bar
return "below minimum notional, skipped"
cid = f"bot-{bar}"
try:
done = signed("POST", "/api/v3/order", symbol=SYMBOL, side=side, type="MARKET",
quantity=format(qty, "f"), newClientOrderId=cid)
except UnknownStatus: # never resend; find out first
raise SystemExit(f"status unknown for {cid}: reconcile before restarting")
filled = Decimal(done["executedQty"]) # can be less than asked
left = filled if side == "BUY" else held - filled
state.update(bar=bar, qty=left or None)
return f"{side} {format(filled, 'f')} filled as {cid}"
state = {}
for _ in range(2): # a real bot keeps polling and alerts on errors
print(step(state))
On 5 November 2026 against the testnet's public endpoints, with the rule left empty, it printed:
no order
no new closed bar
The second call found no newer candle, the guard against acting twice on one bar. A scripted rule that returned BUY and later SELL ran only on the local mock, which printed BUY 0.10000 filled as bot-356400000 and SELL 0.10000 filled as bot-363600000. There, a SELL with no position returned nothing to sell, a partial fill left the remainder in state, a refusal raised BinanceError and stopped the loop, and a 503, 502, -1007 or read timeout on the order ended with status unknown for bot-385200000: reconcile before restarting, after one request.
This is a starting point, not a finished bot. state lives in memory, so a restart forgets the position and can repeat a bar. There is no risk layer, and the exit does not adjust for a fee paid in the base asset; see trading bot risk management before going further. Run it on the testnet with your own key first, because the order path has not run there.
Common Binance API error codes and what to do
| Code | Meaning | Typical fix |
|---|---|---|
-1021 | Timestamp outside recvWindow or ahead of server time | Sync the clock with NTP; check against GET /api/v3/time |
-1022 | Invalid signature | Sign exactly the string you send, including parameter order |
-1013 | Rejected before the matching engine, e.g. Filter failure: LOT_SIZE | Round to the filters from exchangeInfo |
-2010 | New order rejected, e.g. insufficient balance or duplicate order | Read the message; do not retry blindly |
-2013 | Order does not exist | Check the symbol and ID |
-2014, -2015 | Bad key format; invalid key, IP or permission | Check key, IP allowlist and permissions |
| HTTP 429, 418 | Rate limit hit; IP banned | Back off for Retry-After seconds |
HTTP 5XX responses and -1006 or -1007 answers mean the outcome is unknown, not that the order failed. Query the order by its client ID or wait for its executionReport; a -2013 right after a timeout is not proof of failure either, so check again before deciding to resend. The fuller list, and what to check first, is in the Binance API error code guide.
Hypothetical example. A sizing rule produces 0.0004321 BTC, which fails LOT_SIZE because the step size is 0.00001. Rounded down to 0.00043 at 76,439.93 USDT, the notional is about 32.87 USDT, above the 5 USDT minimum, so the order passes.
From testnet to production: what changes
The code stays the same; the environment and controls change. Before switching the base URL to https://api.binance.com:
- Create a separate production key with only the permissions the bot needs and restrict it to your server's IP address. Binance's support page states that, by default, HMAC keys without an IP restriction can only read, and that withdrawal permission requires one.
- Re-read filters and rate limits from production
exchangeInfo; they can differ from the testnet. Binance API rate limits explains the counters. - Add independent risk limits (maximum order size, daily loss limit, kill switch) as described in trading bot risk management.
- Start small, roll out in stages and compare fills with expectations.
Testnet success shows that the plumbing works, not that a strategy is profitable; backtests and testnet fills do not guarantee future results. The full development process, from specification to monitoring, is outlined in trading bot development.
Frequently asked questions
What is the Binance Spot Testnet?
A separate Binance environment with virtual balances that cannot be withdrawn, for testing the /api endpoints. Data is reset roughly monthly, but API keys are preserved.
What is the Binance futures testnet URL?
As of November 2026, Binance's USD-M futures documentation gives https://demo-fapi.binance.com for REST and wss://demo-fstream.binance.com for WebSocket. They are separate from the Spot Testnet at testnet.binance.vision.
Which Python library should I use for the Binance API?
Binance's official package is binance-sdk-spot; the older binance-connector package is marked deprecated. Plain requests and websockets calls, as in this guide, also work and make every step visible.
Why does Binance reject my order with Filter failure: LOT_SIZE?
The quantity is outside the allowed range or is not a multiple of the symbol's step size. Read LOT_SIZE from exchangeInfo and round the quantity down to the step size with Decimal arithmetic.
How do I get order updates without polling?
Subscribe to the user data stream through the WebSocket API with userDataStream.subscribe.signature, or with userDataStream.subscribe after an Ed25519 session.logon. The listenKey method was retired on 20 February 2026.
Can I use the same code on the live exchange?
Yes, by changing the base URLs and keys, after testing, adding risk limits and creating a restricted production key. Live trading can lose money even with correct code.
Sources
- Binance Spot Testnet: General info (URLs, resets, Ed25519 keys) (opens in a new tab)
- Binance Spot Test Network homepage (GitHub login, API key generation) (opens in a new tab)
- Binance Spot Testnet CHANGELOG (data reset of 2026-10-07) (opens in a new tab)
- Binance Spot API documentation: REST API (security, order endpoints) (opens in a new tab)
- Binance Spot API documentation: Filters (opens in a new tab)
- Binance Spot API documentation: Error codes (opens in a new tab)
- Binance Spot API documentation: WebSocket API (user data stream requests) (opens in a new tab)
- Binance Spot API documentation: WebSocket Streams (opens in a new tab)
- Binance Spot API CHANGELOG (listenKey endpoints retired 2026-02-20) (opens in a new tab)
- Binance Support: How to Create API Keys on Binance (opens in a new tab)
- PyPI: binance-sdk-spot (official Binance Spot SDK) (opens in a new tab)
- PyPI: binance-connector (deprecated) (opens in a new tab)
- Capital Markets Law No. 6362 (consolidated text, Articles 35/B and 99/A) (opens in a new tab)
- SPK: Announcement on crypto-asset service providers, 5 August 2024 (opens in a new tab)
- Binance Derivatives documentation: USD-M Futures General Info (testnet base URLs) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.