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.
Debug signing with a sample or testnet key and budget your requests: HMAC signature generator and API rate limit calculator.
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.
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.
| 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; storing them, in API key security for 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.
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).
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.
pybit: Bybit's official Python SDK
pybit handles signing and raises exceptions on error codes, and Bybit lists it as its official Python SDK.
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.
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.
| 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:
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.
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
uequal 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.
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) (opens in a new tab)
- Bybit V5: Introduction (opens in a new tab)
- Bybit V5: Rate Limit Rules (opens in a new tab)
- Bybit V5: Error Codes (opens in a new tab)
- Bybit V5: Demo Trading Service (opens in a new tab)
- Bybit V5: WebSocket Connect (hosts, args limits, heartbeat) (opens in a new tab)
- Bybit V5: Orderbook WebSocket topic (opens in a new tab)
- Bybit V5: Place Order (opens in a new tab)
- Bybit V5: Get Instruments Info (opens in a new tab)
- GitHub: pybit (official Python SDK) (opens in a new tab)
- PyPI: pybit (release 5.17.0) (opens in a new tab)
- SPK: Announcement on crypto-asset service providers, 5 August 2024 (Turkish) (opens in a new tab)
- SPK: Provisional list of operating crypto-asset service providers (Turkish) (opens in a new tab)
- Capital Markets Law No. 6362 (consolidated text, Article 99/A, Turkish) (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.