# Binance API Python trading bot: a testnet-first guide

URL: https://fincodelabs.com/en/blog/binance-api-python-trading-bot · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-10-24 · Updated: 2026-11-05

Binance API Python guide on the Spot Testnet: exchange filters, Ed25519 signed requests, test and live orders, order status, user data stream, error codes.

**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](https://fincodelabs.com/en/blog/exchange-api-key-how-to).

```bash
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 API `wss://ws-api.testnet.binance.vision/ws-api/v3`, streams `wss://stream.testnet.binance.vision/ws`.
- Only `/api` endpoints exist; wallet and other `/sapi` endpoints 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:

```python
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 `minPrice` and `maxPrice` and a multiple of `tickSize`.
- **LOT_SIZE**: quantity between `minQty` and `maxQty` and a multiple of `stepSize`.
- **NOTIONAL**: price times quantity between `minNotional` and `maxNotional`; 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:

```python
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:

```python
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](https://fincodelabs.com/en/blog/api-key-security-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:

**Order endpoints used above**

| 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](https://fincodelabs.com/en/blog/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:

```python
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](https://fincodelabs.com/en/blog/websocket-order-book). Limits and reconnects are in [Binance WebSocket API and streams](https://fincodelabs.com/en/blog/binance-websocket-api).

## 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.

```python
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](https://fincodelabs.com/en/blog/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

**Frequent Spot API errors, as documented in October 2026**

| 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](https://fincodelabs.com/en/blog/binance-api-error-codes).

> **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](https://fincodelabs.com/en/blog/binance-api-rate-limit) explains the counters.
- Add independent risk limits (maximum order size, daily loss limit, kill switch) as described in [trading bot risk management](https://fincodelabs.com/en/blog/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](https://fincodelabs.com/en/blog/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)](https://github.com/binance/binance-spot-api-docs/blob/master/testnet/general-info.md)
- [Binance Spot Test Network homepage (GitHub login, API key generation)](https://testnet.binance.vision/)
- [Binance Spot Testnet CHANGELOG (data reset of 2026-10-07)](https://github.com/binance/binance-spot-api-docs/blob/master/testnet/CHANGELOG.md)
- [Binance Spot API documentation: REST API (security, order endpoints)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot API documentation: Filters](https://github.com/binance/binance-spot-api-docs/blob/master/filters.md)
- [Binance Spot API documentation: Error codes](https://github.com/binance/binance-spot-api-docs/blob/master/errors.md)
- [Binance Spot API documentation: WebSocket API (user data stream requests)](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-api.md)
- [Binance Spot API documentation: WebSocket Streams](https://github.com/binance/binance-spot-api-docs/blob/master/web-socket-streams.md)
- [Binance Spot API CHANGELOG (listenKey endpoints retired 2026-02-20)](https://github.com/binance/binance-spot-api-docs/blob/master/CHANGELOG.md)
- [Binance Support: How to Create API Keys on Binance](https://www.binance.com/en/support/faq/detail/360002502072)
- [PyPI: binance-sdk-spot (official Binance Spot SDK)](https://pypi.org/project/binance-sdk-spot/)
- [PyPI: binance-connector (deprecated)](https://pypi.org/project/binance-connector/)
- [Capital Markets Law No. 6362 (consolidated text, Articles 35/B and 99/A)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)
- [SPK: Announcement on crypto-asset service providers, 5 August 2024](https://spk.gov.tr/duyurular/basin-duyurulari/2024/kripto-varlik-hizmet-saglayicilara-iliskin-duyuru_05082024)
- [Binance Derivatives documentation: USD-M Futures General Info (testnet base URLs)](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/general-info)

> 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](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/binance-api-python-trading-bot

Cite as: Fincode, "Binance API Python trading bot: a testnet-first guide", updated November 5, 2026, https://fincodelabs.com/en/blog/binance-api-python-trading-bot
