# Bybit API in Python: V5 signing, testnet and rate limits

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

Bybit API Python guide for V5: testnet and demo hosts, HMAC signing, order calls, rate limits, the too many visits error and a tested WebSocket order book.

**Short answer:** To use the **Bybit API in Python**, start on the V5 testnet at `api-testnet.bybit.com` with pybit, Bybit's official SDK, or plain requests. Sign timestamp + API key + recv_window + the query string or JSON body with HMAC-SHA256 and send the result in X-BAPI headers. Bybit's documentation allows 600 requests per 5 seconds per IP and sets per-UID limits for each endpoint; retCode 10006, Too many visits, means an endpoint's per-UID limit was hit.

V5 puts spot, derivatives and options behind one API, and the `category` parameter selects the market. This guide follows Bybit's official V5 documentation as read on 4 November 2026; general concepts are in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

**What was run and what was not.** Public REST and WebSocket samples ran on the testnet on 4 November 2026 on Python 3.11.3. Signed calls reached the testnet with a placeholder key and stopped at the key check, so server acceptance of the signature, a real order, a fill and live `X-Bapi-Limit` headers are untested here.

This is a technical guide, not a recommendation to use Bybit or any strategy, and availability depends on your jurisdiction and Bybit's terms. For readers 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, so foreign platforms may not target them. Automated trading can lose money.

## Bybit API Python documentation: hosts and who can connect

Bybit documents three separate environments, and a key from one does not work on another.

**Bybit V5 environments, from the integration guide, WebSocket and demo pages, 4 November 2026**

| Environment | REST | WebSocket |
| --- | --- | --- |
| Testnet | `api-testnet.bybit.com` | `stream-testnet.bybit.com` |
| Demo trading | `api-demo.bybit.com` | `stream-demo.bybit.com`, private streams only |
| Mainnet | `api.bybit.com` or `api.bytick.com` | `stream.bybit.com` |

The guide says IP addresses in the US or mainland China are restricted and receive 403 Forbidden; check the terms and do not try to route around it. It also lists regional mainnet hosts for accounts registered on local sites, among them `api.bybit.tr` for Turkey. A company named Bybit Kripto Varlık Alım Satım Platformu AŞ is on the provisional list of Turkey's Capital Markets Board (SPK) as checked on 4 November 2026, and SPK states that being listed does not mean a firm is authorized.

Keys are either system-generated (HMAC) or self-generated (RSA-SHA256, base64), in which case Bybit holds only your public key. Creating them is covered in the [exchange API key guide](https://fincodelabs.com/en/blog/exchange-api-key-how-to); storing them, in [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## Bybit testnet API vs demo trading

The testnet is a separate site with its own accounts and prices, while demo trading is an isolated account inside your mainnet login that sees mainnet market data.

- **Prices.** In one check on 4 November 2026, spot BTCUSDT last traded at about 78,075 on the testnet and about 80,230 on both the demo and mainnet hosts. Testnet fills say nothing about real liquidity.
- **Demo limits.** The demo page says not every API is available, orders are kept for 7 days and rate limits are the default, with no upgrade. Batch orders cover linear and option only.
- **Demo keys.** Create them after switching to Demo Trading on mainnet. The page warns against creating a key from demo mode on the testnet site.
- **Mismatches.** Bybit's error page explains retCode 10003, API key is invalid, as possibly a key and domain that do not match across its four environments: mainnet, testnet and a demo mode on each.

Neither environment shows that a strategy works; they test software.

## Bybit API Python example: market data and filters

Market endpoints need no key, so this runs against the testnet as it is, and it reads the filters every order must pass.

```python
from decimal import Decimal, ROUND_DOWN
import requests

BASE = "https://api-testnet.bybit.com"

def get(path, **params):
    body = requests.get(BASE + path, params=params, timeout=10).json()
    if body["retCode"]:                      # errors can arrive with HTTP 200
        raise RuntimeError(body["retCode"], body["retMsg"])
    return body["result"]

def floor_to(value, step):
    step = Decimal(step)
    return (Decimal(value) / step).to_integral_value(rounding=ROUND_DOWN) * step

print(get("/v5/market/time")["timeSecond"])
info = get("/v5/market/instruments-info", category="spot", symbol="BTCUSDT")["list"][0]
lot, tick = info["lotSizeFilter"], info["priceFilter"]["tickSize"]
last = get("/v5/market/tickers", category="spot", symbol="BTCUSDT")["list"][0]["lastPrice"]

price = floor_to(Decimal(last) * Decimal("0.95"), tick)    # hypothetical limit price
qty = floor_to("0.0012345", lot["basePrecision"])          # hypothetical quantity
print(info["status"], last, tick, lot["basePrecision"], lot["minOrderAmt"])
print(price, qty, round(price * qty, 2), price * qty >= Decimal(lot["minOrderAmt"]))
```

Output on 4 November 2026:

```
1793821813
Trading 78067.2 0.1 0.000001 5
74163.8 0.001234 91.52 True
```

Failures can arrive with HTTP 200, so check `retCode` on every response.

> **Hypothetical example.** Using the testnet values above, a limit 5% below 78,067.2 is 74,163.84, which floors to 74,163.8 at a 0.1 tick. A quantity of 0.0012345 floors to 0.001234 at a 0.000001 step, for an order value of 91.52 USDT, above the 5 USDT minimum. This shows the rounding only.

## How to sign Bybit V5 requests in Python

Bybit's V5 signature is an HMAC-SHA256, in lowercase hex, of timestamp + API key + recv_window + the query string (GET) or the JSON body (POST).

```python
import hashlib, hmac, json, os, time
import requests

BASE = "https://api-testnet.bybit.com"
KEY = os.environ.get("BYBIT_TESTNET_KEY", "placeholder-key")       # testnet key only
SECRET = os.environ.get("BYBIT_TESTNET_SECRET", "placeholder-secret")
RECV_WINDOW = "5000"
http = requests.Session()

def headers(payload):
    ts = str(int(time.time() * 1000))
    text = ts + KEY + RECV_WINDOW + payload                        # the V5 string to sign
    sign = hmac.new(SECRET.encode(), text.encode(), hashlib.sha256).hexdigest()
    return {"X-BAPI-API-KEY": KEY, "X-BAPI-TIMESTAMP": ts, "X-BAPI-SIGN": sign,
            "X-BAPI-RECV-WINDOW": RECV_WINDOW, "Content-Type": "application/json"}

def post(path, body):
    payload = json.dumps(body, separators=(",", ":"))              # send what you signed
    return http.post(BASE + path, data=payload, headers=headers(payload), timeout=10).json()

def get(path, query):
    return http.get(f"{BASE}{path}?{query}", headers=headers(query), timeout=10).json()

link_id = f"demo-{int(time.time())}"                               # at most 36 characters
placed = post("/v5/order/create", {"category": "spot", "symbol": "BTCUSDT", "side": "Buy",
                                   "orderType": "Limit", "qty": "0.001", "price": "20000",
                                   "timeInForce": "PostOnly", "orderLinkId": link_id})
print(placed["retCode"], placed["retMsg"])
print(get("/v5/order/realtime", f"category=spot&symbol=BTCUSDT&orderLinkId={link_id}")["retCode"])
print(post("/v5/order/cancel", {"category": "spot", "symbol": "BTCUSDT",
                                "orderLinkId": link_id})["retCode"])
```

Output on 4 November 2026 with the placeholder key:

```
10003 API key is invalid.
10003
10003
```

The testnet rejected the key, which confirms the request reaches the key check, not that Bybit accepts the signature. Bybit's published examples omit the secret, so they cannot serve as a test, but offline, on the same inputs, this code matched the signer in pybit 5.17.0. With your own testnet key, the script places, reads and cancels an order priced far from the market.

- **Timestamp.** It must satisfy server_time - recv_window <= timestamp < server_time + 1000, and the default window is 5,000 ms. Failures return retCode 10002. Bybit recommends local time kept synchronized by NTP.
- **Exact string.** Sign the same bytes you send; a mismatch gives retCode 10004, Error sign.
- **`orderLinkId`.** Your own order ID, at most 36 characters and unique; use it to look an order up after a timeout.
- **Acknowledgment.** The create-order page says the response only means the request was accepted, and that processing is asynchronous, so confirm the state on the order stream. Add your own limits as in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## pybit: Bybit's official Python SDK

`pybit` handles signing and raises exceptions on error codes, and Bybit lists it as its official Python SDK.

```python
from pybit.unified_trading import HTTP             # pip install pybit
from pybit.exceptions import InvalidRequestError

public = HTTP(testnet=True)
ticker = public.get_tickers(category="spot", symbol="BTCUSDT")["result"]["list"][0]
print(ticker["symbol"], ticker["lastPrice"])

session = HTTP(testnet=True, api_key="placeholder-key", api_secret="placeholder-secret")
try:
    session.place_order(category="spot", symbol="BTCUSDT", side="Buy", orderType="Limit",
                        qty="0.001", price="20000", timeInForce="PostOnly")
except InvalidRequestError as err:
    print(err.status_code, err.message)
```

Output on 4 November 2026, with pybit 5.17.0 (released 14 July 2026, Python 3.10 or later):

```
BTCUSDT 78065.8
10003 API key is invalid.
```

`HTTP` also takes `demo=True` for the demo host. For several exchanges behind one interface, see the [CCXT Python guide](https://fincodelabs.com/en/blog/ccxt-python-trading-bot).

## Bybit API rate limit and the too many visits error

Bybit has two main REST layers, an IP limit and a per-UID limit for each endpoint, and each fails differently.

**Bybit limits and errors, from the rate limit and error code pages, 4 November 2026**

| Limit | Value | Signal | What the docs ask |
| --- | --- | --- | --- |
| HTTP per IP | 600 requests per 5-second window, by default | 403, access too frequent; retCode 10018 is also listed for the IP limit | End all HTTP sessions and wait at least 10 minutes; the ban lifts by itself |
| Per UID, per endpoint | Rolling one-second window, see below | retCode 10006: Too many visits. Exceeded the API Rate Limit. | Watch the limit headers |
| WebSocket | 500 connections per 5 minutes per domain; 1,000 market data connections per IP, per market | Not stated | Avoid frequent reconnects |

A 403 has three documented causes: the IP limit above, a restricted region, or a GET with an empty JSON body, and waiting only helps with the first. UTA2.0 Pro per-UID values on 4 November 2026 for `/v5/order/create` and `/v5/order/cancel` were 10 per second for inverse, linear and option and 20 for spot; `/v5/order/amend` was 10 across the board; `/v5/order/realtime` was 50. Create, amend and cancel are marked upgradable.

Bybit's rate limit page says API responses carry `X-Bapi-Limit`, `X-Bapi-Limit-Status` and `X-Bapi-Limit-Reset-Timestamp`, and this helper turns them into a pause:

```python
import time

def pause_seconds(headers, body, now_ms=None):
    """How long to wait before calling this endpoint again."""
    now_ms = time.time() * 1000 if now_ms is None else now_ms
    remaining = int(headers.get("X-Bapi-Limit-Status", 1))
    reset_ms = int(headers.get("X-Bapi-Limit-Reset-Timestamp", now_ms))
    if body.get("retCode") == 10006 or remaining == 0:
        return max(0.0, (reset_ms - now_ms) / 1000)
    return 0.0

now = 1_793_800_000_000                            # synthetic responses, not live traffic
print(pause_seconds({"X-Bapi-Limit-Status": "9",
                     "X-Bapi-Limit-Reset-Timestamp": str(now)}, {"retCode": 0}, now))
print(pause_seconds({"X-Bapi-Limit-Status": "0",
                     "X-Bapi-Limit-Reset-Timestamp": str(now + 350)},
                    {"retCode": 10006}, now))
```

It prints 0.0, then 0.35. The inputs are synthetic, because public and rejected testnet calls returned none of these headers; test it with your own testnet key.

## Bybit WebSocket orderbook in Python

Subscribe to `orderbook.{depth}.{symbol}`, reset your book on each snapshot, then apply deltas, removing any level whose size is 0.

```python
import asyncio, json, time
import websockets                                  # pip install websockets

URL = "wss://stream-testnet.bybit.com/v5/public/spot"
TOPIC = "orderbook.50.BTCUSDT"

def apply(side, levels):
    for price, size in levels:
        if float(size) == 0:
            side.pop(price, None)                  # size 0 removes the level
        else:
            side[price] = size                     # insert or update

async def main(updates=3):
    bids, asks, seen = {}, {}, 0
    async with websockets.connect(URL, ping_interval=None) as ws:
        await ws.send(json.dumps({"op": "subscribe", "args": [TOPIC]}))
        next_ping = time.monotonic() + 20
        while seen < updates:
            if time.monotonic() >= next_ping:      # Bybit recommends every 20 seconds
                await ws.send(json.dumps({"op": "ping"}))
                next_ping += 20
            msg = json.loads(await asyncio.wait_for(ws.recv(), 25))
            if "topic" not in msg:
                print(msg.get("op"), msg.get("success"))
                continue
            data = msg["data"]
            if msg["type"] == "snapshot" or data["u"] == 1:   # a snapshot resets the book
                bids, asks = {}, {}
            apply(bids, data["b"])
            apply(asks, data["a"])
            seen += 1
            print(msg["type"], data["u"], max(bids, key=float), min(asks, key=float))

asyncio.run(main())
```

Output on the testnet on 4 November 2026:

```
subscribe True
snapshot 1124895 78067.1 78067.2
delta 1124896 78067.1 78067.2
delta 1124897 78067.1 78067.2
```

- **Depths.** Spot and linear offer levels 1, 50, 200 and 1,000, pushed every 10, 20, 100 and 200 ms.
- **Resets.** The docs say a new snapshot, or `u` equal to 1 after a service restart, means overwrite your local book.
- **Subscription limits.** Spot takes up to 10 args per request. As of 5 November 2026 the connect page also lists a limit of 100 args per connection for spot and futures, with the start date marked TBD, so split large symbol lists.

Sequence checks and recovery are covered in the [local order book guide](https://fincodelabs.com/en/blog/websocket-order-book).

## Frequently asked questions

### Why does the Bybit API return 403?

Bybit's docs give three reasons: an IP in the US or mainland China; too many requests from one IP, reported as access too frequent; or a GET with an empty JSON body. For the IP limit, end all sessions and wait at least 10 minutes.

### What does Too many visits, exceeded the API rate limit, mean on Bybit?

It is retCode 10006: your UID passed the per-second limit for that endpoint. Read the X-Bapi-Limit headers and wait for the reset time instead of retrying at once.

### Does Bybit have a testnet API?

Yes. The host is api-testnet.bybit.com, with streams at stream-testnet.bybit.com and separate keys. Its prices differed from mainnet when checked on 4 November 2026.

### What is the difference between the Bybit testnet and demo trading?

The testnet is a separate site with its own market. Demo trading is an isolated account under your mainnet login, on api-demo.bybit.com, with mainnet market data and fewer available endpoints.

## Sources

- [Bybit V5: Integration Guidance (hosts, authentication, 403 restriction, SDKs)](https://bybit-exchange.github.io/docs/v5/guide)
- [Bybit V5: Introduction](https://bybit-exchange.github.io/docs/v5/intro)
- [Bybit V5: Rate Limit Rules](https://bybit-exchange.github.io/docs/v5/rate-limit)
- [Bybit V5: Error Codes](https://bybit-exchange.github.io/docs/v5/error)
- [Bybit V5: Demo Trading Service](https://bybit-exchange.github.io/docs/v5/demo)
- [Bybit V5: WebSocket Connect (hosts, args limits, heartbeat)](https://bybit-exchange.github.io/docs/v5/ws/connect)
- [Bybit V5: Orderbook WebSocket topic](https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook)
- [Bybit V5: Place Order](https://bybit-exchange.github.io/docs/v5/order/create-order)
- [Bybit V5: Get Instruments Info](https://bybit-exchange.github.io/docs/v5/market/instrument)
- [GitHub: pybit (official Python SDK)](https://github.com/bybit-exchange/pybit)
- [PyPI: pybit (release 5.17.0)](https://pypi.org/project/pybit/)
- [SPK: Announcement on crypto-asset service providers, 5 August 2024 (Turkish)](https://spk.gov.tr/duyurular/basin-duyurulari/2024/kripto-varlik-hizmet-saglayicilara-iliskin-duyuru_05082024)
- [SPK: Provisional list of operating crypto-asset service providers (Turkish)](https://spk.gov.tr/kurumlar/kripto-varlik-hizmet-saglayicilar/faaliyette-bulunanlar-listesi)
- [Capital Markets Law No. 6362 (consolidated text, Article 99/A, Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> 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/bybit-api-python

Cite as: Fincode, "Bybit API in Python: V5 signing, testnet and rate limits", updated November 5, 2026, https://fincodelabs.com/en/blog/bybit-api-python
