Configuration
Everything is configured through the constructor — see API Keys Setup for credentials specifically.
Constructor
| Argument | Default | Effect |
|---|---|---|
api_key |
None |
Falls back to BYBIT_API_KEY |
api_secret |
None |
Falls back to BYBIT_API_SECRET |
public |
False |
Build a credential-free client, restricted to client.http.market and the public WebSocket streams |
region |
'global' |
Target a documented regional host — see Regional Endpoints |
testnet |
False |
Target that region's testnet host instead of mainnet |
base_url |
None |
Fully-qualified REST base URL override; WebSocket URLs are still resolved from region/testnet |
validate |
True |
Validate responses against the generated schemas |
http |
None |
Supply your own HttpClient instead of a fresh one |
from typed_bybit import Bybit
client = Bybit.new() # authenticated, reads BYBIT_API_KEY/SECRET
client = Bybit.new(public=True) # credential-free, market data only
client = Bybit.new(testnet=True) # api-testnet.bybit.com
client = Bybit.new(region='eu') # api.bybit.eu
client = Bybit.new(validate=False) # skip response validation
client = Bybit.new(base_url='http://localhost:8000') # mock serverBybit.new() is authenticated by default and raises AuthError if neither the environment
nor the constructor supplies api_key/api_secret; pass public=True for the
credential-free client instead. region and testnet are independent and freely
combinable — region picks the host's domain, testnet picks the api/api-testnet
subdomain on it.
Regional Endpoints
Bybit operates one REST host per legal entity. region selects between them:
region |
Mainnet host | Entity |
|---|---|---|
'global' |
https://api.bybit.com |
Global (default) |
'bytick' |
https://api.bytick.com |
Global, alternate domain |
'eu' |
https://api.bybit.eu |
European Economic Area |
'nl' |
https://api.bybit.nl |
Netherlands |
'tr' |
https://api.bybit.tr |
Turkey |
'kz' |
https://api.bybit.kz |
Kazakhstan |
'ge' |
https://api.bybitgeorgia.ge |
Georgia |
'ae' |
https://api.bybit.ae |
United Arab Emirates |
'id' |
https://api.bybit.id |
Indonesia |
'jp' |
https://api.manepa.jp |
Japan |
testnet=True swaps api for api-testnet on the same domain. That host is live for
global/bytick (api-testnet.bybit.com) and jp (api-testnet.manepa.jp); the other
six regions document no testnet host, so testnet=True still builds the same-shaped URL for
them, and it may not resolve to anything.
from typed_bybit import BYBIT_DOMAINS, resolve_rest_base_url
print(resolve_rest_base_url('eu')) # https://api.bybit.eu
print(BYBIT_DOMAINS['jp']) # manepa.jp
print(len(BYBIT_DOMAINS))The v5 protocol is identical on every host: same paths, same {retCode, retMsg, result}
envelope, same response shapes. Only the product universe differs — see
Regions Do Not Share A Product Universe.
Bybit restricts IP addresses located in the US or Mainland China on all of these hosts.
Regions Do Not Share A Product Universe
!!! warning "A region is not a mirror"
Each host lists only the products its entity is licensed to offer. Switching region
silently changes which symbols exist, and region='eu' lists no derivatives at
all — no linear, no inverse, no options.
Check the product universe for yourself rather than assuming it matches the default host:
from typed_bybit import Bybit
for region in ('global', 'eu'):
async with Bybit.new(public=True, region=region) as client:
spot = await client.http.market.instruments(category='spot')
print(region, 'spot instruments:', len(spot['list']))Even inside spot the symbol sets differ, so a symbol that resolves globally may not be listed regionally:
from typed_bybit import Bybit
for region in ('global', 'eu'):
async with Bybit.new(public=True, region=region) as client:
info = await client.http.market.instruments(category='spot', symbol='BTCUSDT')
print(region, 'BTCUSDT listed:', bool(info['list']))The Instrument Catalogue Is Not The Queryable Set
market.instruments is not a reliable inventory of what a regional host will answer for.
On region='eu' BTCUSDT may be absent from the spot catalogue while market.tickers and
market.kline still return live BTCUSDT data:
from typed_bybit import Bybit
async with Bybit.new(public=True, region='eu') as client:
listed = await client.http.market.instruments(category='spot', symbol='BTCUSDT')
print('BTCUSDT in eu catalogue:', bool(listed['list']))
ticker = await client.http.market.tickers(category='spot', symbol='BTCUSDT')
print('BTCUSDT ticker on eu:', ticker['list'][0]['lastPrice'])So a symbol's absence from instruments does not mean market data is unavailable, and its
presence in tickers does not mean the entity lists it for trading. If you need the tradable
set, use market.instruments; if you need a price, do not gate the call on it.
How A Missing Product Fails
There is no single failure mode, and none of them is an obvious "unsupported region"
error. Against region='eu', asking for derivatives can produce any of:
| Outcome | Endpoints | What you see |
|---|---|---|
BadRequest on retCode |
tickers, kline, orderbook, mark_price_kline, index_price_kline, premium_index_price_kline, open_interest, funding_history, risk_limit, long_short_ratio, adl_alert |
retCode 10001, params error: symbol invalid — the symbol is unknown, not the category |
ApiError on retCode |
price_limit |
retCode 12814, symbol is invalid |
ValidationError |
instruments for a derivatives category, full_orderbook |
HTTP 200, retCode 0, but a payload that does not match the schema |
BadRequest(404) |
insurance, historical_volatility |
The route does not exist on this host at all |
| Empty list | delivery_price, instruments for an unlisted spot symbol |
HTTP 200, retCode 0, list: [] — indistinguishable from "nothing to report" |
The ValidationError cases are the ones worth knowing about. instruments on a derivatives
category returns {"category": "", "list": [], "nextPageCursor": ""} — an empty
category string, which is not a member of the Literal the schema declares, so validation
rejects it. full_orderbook returns result: {}, with the s, b, a, ts and u keys
missing entirely. Neither is an error on the wire; both surface as a schema mismatch.
from typed_bybit import Bybit, ApiError, ValidationError
async with Bybit.new(public=True, region='eu') as client:
try:
await client.http.market.tickers(category='linear', symbol='BTCUSDT')
except ApiError as e:
print('tickers(linear):', type(e).__name__, e.args[0])
try:
await client.http.market.instruments(category='linear')
except ValidationError:
print('instruments(linear): ValidationError — empty category, not an empty list')Note the last row of the table: with validate=False the instruments mismatch stops being
an error and becomes an empty list, so turning validation off turns a loud failure into a
silent one. That is the opposite of the usual trade-off, and it is the reason to leave
validation on when pointing at a non-global region.
Base URLs
from typed_bybit import BYBIT_API_URL, resolve_rest_base_url
print(BYBIT_API_URL) # https://api.bybit.com
print(resolve_rest_base_url('global', testnet=True)) # https://api-testnet.bybit.combase_url takes precedence over both region and testnet for HTTP, so pointing at a mock
server does not require touching either. There is no matching override for the WebSocket
connections — they always resolve from region/testnet.
Connection Pooling
client.http owns one HttpClient, shared by client.http.market and client.http.account
alike, since Http is a router that instantiates both from the same base URL, credentials and
HttpClient. Enter the top-level client as an async context manager so that one connection
pool is reused across every call:
from typed_bybit import Bybit
async with Bybit.new(public=True) as client:
book = await client.http.market.orderbook(category='spot', symbol='BTCUSDT', limit=1)
trades = await client.http.market.recent_trades(category='spot', symbol='BTCUSDT', limit=1)
print(book['s'], trades['list'][0]['price'])Calls made outside the context manager still work — the transport opens a connection per request — but the pool is not reused. See Async Usage for the WebSocket side of the same picture.
Validation
validate=True is the default. Every endpoint method also accepts a per-call validate
override:
from typed_bybit import Bybit
async with Bybit.new(public=True) as client:
tickers = await client.http.market.tickers(category='linear', validate=False)
print(len(tickers['list']))validate=None(the default on each method) follows the client-level settingvalidate=Truevalidates this call regardless of the client defaultvalidate=Falseskips validation for this call
Validation is a pydantic pass over the unwrapped result. Turning it off is worth considering
for large responses — market.tickers(category='linear') returns several hundred records, and
market.full_orderbook ten thousand levels per side — but you lose the early warning when the
upstream schema changes.
Turning validation off does not disable envelope handling. retCode is still inspected and
still raises. See Error Handling.
Response types tolerate undocumented extra fields, so a new Bybit field does not break validation; a changed or removed one does.
Raw Requests
For anything not yet covered by a typed method, or to inspect the untouched envelope:
from typed_bybit import Bybit
async with Bybit.new(public=True) as client:
r = await client.http.request('GET', '/v5/market/tickers', params={'category': 'spot', 'symbol': 'BTCUSDT'})
print(r.status_code)
print(client.http.result(r)['list'][0]['lastPrice'])request returns the raw httpx.Response. result(response, adapter=None, validate=None)
unwraps the envelope and raises on a non-zero retCode, exactly as the generated methods do.