Disclaimer: Public market data from Bybit is generally accessible under their API Terms of Service, but you are responsible for reviewing and complying with Bybit's Terms of Service and any applicable regional regulations before scraping. This guide covers only public market endpoints — not authenticated private account or trading endpoints.
If you are building a crypto trading bot, a market-data pipeline, or an analytics dashboard, you will eventually hit Bybit's unified V5 Market API. The good news is that the V5 interface consolidates spot, linear derivatives, inverse contracts, and options behind a single set of public endpoints. The bad news is that Bybit enforces strict per-IP rate limits and applies geo-restrictions that can return 403 responses without warning. Learning how to scrape the Bybit V5 Market API with rotating proxies is the difference between a reliable data feed and a banned IP address.
This guide maps every public endpoint under /v5/market/, explains Bybit's rate-limit and geo-block behavior, and provides runnable Node.js and Python examples using both raw proxy URLs and the ProxyHat SDK against gate.proxyhat.com:8080.
Bybit V5 Market API Endpoints Explained
Bybit's V5 API is documented under the official V5 reference. All public market-data endpoints share the base URL https://api.bybit.com and return a consistent JSON envelope:
{
"retCode": 0,
"retMsg": "OK",
"result": { ... },
"time": 1700000000000
}
A non-zero retCode means the request was processed but returned an application-level error (invalid symbol, bad category, etc.). A 403 HTTP status, on the other hand, typically means an IP-level block — either rate-limit or geo-restriction. Your client must handle both layers.
The Four Core Public Market Endpoints
| Endpoint | Path | Key Params | Use Case |
|---|---|---|---|
| Order Book | /v5/market/orderbook | category, symbol, limit | Depth snapshots (bids/asks) |
| Tickers | /v5/market/tickers | category, optional symbol | Latest price, 24h volume |
| Kline (Candles) | /v5/market/kline | category, symbol, interval, start, end | OHLCV historical bars |
| Recent Trades | /v5/market/recent-trade | category, symbol, limit | Latest executed trades |
The category parameter is central to the V5 design. It accepts values like spot, linear, inverse, and option. The same endpoint path serves all product types — you just change the category. For example, requesting api.bybit.com/v5/market/orderbook?category=spot&symbol=BTCUSDT&limit=50 returns the spot order book, while category=linear returns the USDT perpetual order book for the same symbol.
The limit parameter on /v5/market/orderbook controls depth: valid values are 1, 25, 50, 100, 500, 1000. Larger depths cost more in response payload size and are more likely to trip rate limits if you poll aggressively.
Bybit's IP-Based Rate Limits and the ~10-Minute Ban Window
Bybit applies per-endpoint rate limits at the IP level. According to Bybit's official rate-limit documentation, public market endpoints typically allow around 200 requests per second per IP for some endpoints, but stricter caps apply to specific paths. The /v5/market/orderbook endpoint is one of the most aggressively limited because it is heavily polled by high-frequency bots.
When you exceed the per-IP rate limit, Bybit returns an HTTP 403 with a JSON body containing a retCode and retMsg indicating rate-limit violation. Once an IP is flagged, it may be temporarily banned for approximately 10 minutes. During that window, every request from that IP returns 403 — there is no partial throttling or retry-after header in most cases.
This is why bursts on /v5/market/orderbook are dangerous. If your scraper polls order books for 50 symbols in a tight loop without rate budgeting, you can exhaust the per-IP allowance in under a second and earn a 10-minute ban. Rotating residential proxies solve this by distributing requests across many IPs, each with its own rate-limit budget.
Geo-Restrictions: 403s and CloudFront Blocks
Bybit restricts access from certain jurisdictions, including the United States, the United Kingdom, and other regions listed in their terms. When a request originates from a restricted IP range, Bybit (or its CloudFront CDN layer) may return a 403 or a CloudFront error page before the request even reaches the API backend. This is distinct from a rate-limit 403 — the response body will not contain the standard retCode envelope.
Residential proxies solve this by routing your requests through IPs in allowed countries. With ProxyHat, you can geo-target the exit IP using the -country- flag in the username. For example, user-country-DE routes through a German residential IP, which is not on Bybit's restricted list.
This is also relevant for data consistency: if you need market data that is identical to what a trader in Japan sees, you can use user-country-JP to ensure your requests exit from a Japanese IP.
Raw Proxy Usage vs ProxyHat SDK
ProxyHat supports two integration patterns. The first is raw proxy URL usage — you pass the proxy string directly to any HTTP client. The second is the ProxyHat SDK, which handles rotation, retries, and session management for you. Below, every example shows both patterns side by side.
ProxyHat Connection Details
| Protocol | URL Format | Port |
|---|---|---|
| HTTP | http://USERNAME:PASSWORD@gate.proxyhat.com:8080 | 8080 |
| SOCKS5 | socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080 | 1080 |
Geo-targeting and session flags go in the username field, separated by hyphens:
user-country-DE:pass— German residential exit IPuser-country-US-state-california:pass— US state-level targetinguser-session-abc123:pass— sticky session (same IP for the session lifetime)user-country-DE-session-kline01:pass— country + sticky session combined
For Bybit scraping, we recommend residential proxies from an allowed country (e.g., DE, JP, SG) with per-request rotation for order-book polling, and sticky sessions for kline pagination. See our proxy locations page for the full list of supported countries.
Example 1: curl — Fetch Order Book via Raw HTTP Proxy
The simplest way to verify your proxy works with Bybit is a single curl request:
# Raw HTTP proxy with German geo-targeting
curl -x "http://user-country-DE:pass@gate.proxyhat.com:8080" \
"https://api.bybit.com/v5/market/orderbook?category=spot&symbol=BTCUSDT&limit=50"
# SOCKS5 proxy variant
curl -x "socks5://user-country-DE:pass@gate.proxyhat.com:1080" \
"https://api.bybit.com/v5/market/tickers?category=linear&symbol=ETHUSDT"
If the response contains "retCode": 0, your proxy is working and the exit IP is not geo-restricted. If you get an HTML CloudFront error, your exit IP may be in a restricted region — change the country flag.
Example 2: Node.js (axios) — Rotating IPs Across a Symbol List
This example fetches order books for multiple symbols, rotating the proxy IP on each request. It shows both raw proxy URL construction and the ProxyHat SDK pattern. It includes retry logic with exponential backoff and per-endpoint rate budgeting.
import axios from 'axios';
import { ProxyHat } from '@proxyhat/sdk';
const BYBIT_BASE = 'https://api.bybit.com';
const symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT'];
// --- Raw proxy URL builder ---
function buildProxyUrl(country, session) {
let username = `user-country-${country}`;
if (session) username += `-session-${session}`;
return `http://${username}:pass@gate.proxyhat.com:8080`;
}
// --- ProxyHat SDK client ---
const phat = new ProxyHat({
gateway: 'gate.proxyhat.com',
port: 8080,
protocol: 'http',
username: 'user',
password: 'pass',
defaultCountry: 'DE',
rotate: true, // new IP per request
});
async function fetchOrderbookRaw(symbol, retries = 3) {
const url = `${BYBIT_BASE}/v5/market/orderbook?category=spot&symbol=${symbol}&limit=50`;
for (let attempt = 0; attempt < retries; attempt++) {
try {
const proxyUrl = buildProxyUrl('DE');
const res = await axios.get(url, {
proxy: {
host: 'gate.proxyhat.com',
port: 8080,
auth: { username: 'user-country-DE', password: 'pass' },
protocol: 'http',
},
timeout: 10000,
});
if (res.data.retCode === 0) return res.data.result;
throw new Error(`retCode ${res.data.retCode}: ${res.data.retMsg}`);
} catch (err) {
if (err.response && err.response.status === 403) {
console.warn(`[${symbol}] 403 on attempt ${attempt + 1}, backing off`);
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
continue;
}
throw err;
}
}
throw new Error(`Failed after ${retries} retries: ${symbol}`);
}
async function fetchOrderbookSDK(symbol, retries = 3) {
const url = `${BYBIT_BASE}/v5/market/orderbook?category=spot&symbol=${symbol}&limit=50`;
for (let attempt = 0; attempt < retries; attempt++) {
try {
const proxyAgent = await phat.getProxyAgent(); // auto-rotates IP
const res = await axios.get(url, {
httpsAgent: proxyAgent,
timeout: 10000,
});
if (res.data.retCode === 0) return res.data.result;
throw new Error(`retCode ${res.data.retCode}: ${res.data.retMsg}`);
} catch (err) {
console.warn(`[${symbol}] retry ${attempt + 1}: ${err.message}`);
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
throw new Error(`Failed after ${retries} retries: ${symbol}`);
}
// Rate budget: max 5 requests/sec across all symbols
const MIN_INTERVAL_MS = 200;
async function scrapeAll() {
for (const symbol of symbols) {
const t0 = Date.now();
try {
// Use either fetchOrderbookRaw or fetchOrderbookSDK here:
const book = await fetchOrderbookSDK(symbol);
const bestBid = book.bids?.[0]?.[0] ?? 'N/A';
console.log(`${symbol} best bid: ${bestBid}`);
} catch (err) {
console.error(`${symbol} failed: ${err.message}`);
}
const elapsed = Date.now() - t0;
if (elapsed < MIN_INTERVAL_MS) {
await new Promise(r => setTimeout(r, MIN_INTERVAL_MS - elapsed));
}
}
}
scrapeAll();
Example 3: Python (requests) — Order Book with Raw Proxy and SDK
import requests
import time
from proxyhat import ProxyHat # ProxyHat Python SDK
BYBIT_BASE = 'https://api.bybit.com'
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT']
# --- Raw proxy dict for requests ---
def raw_proxy(country='DE', session=None):
username = f'user-country-{country}'
if session:
username += f'-session-{session}'
return {
'http': f'http://{username}:pass@gate.proxyhat.com:8080',
'https': f'http://{username}:pass@gate.proxyhat.com:8080',
}
# --- ProxyHat SDK ---
phat = ProxyHat(
gateway='gate.proxyhat.com',
port=8080,
username='user',
password='pass',
country='DE',
rotate=True,
)
def fetch_orderbook_raw(symbol, retries=3):
url = f'{BYBIT_BASE}/v5/market/orderbook'
params = {'category': 'spot', 'symbol': symbol, 'limit': 50}
for attempt in range(retries):
try:
res = requests.get(url, params=params, proxies=raw_proxy('DE'),
timeout=10)
if res.status_code == 403:
print(f'[{symbol}] 403, retry {attempt+1}')
time.sleep(2 ** attempt)
continue
data = res.json()
if data.get('retCode') == 0:
return data['result']
raise Exception(f"retCode {data.get('retCode')}: {data.get('retMsg')}")
except requests.RequestException as e:
print(f'[{symbol}] {e}, retry {attempt+1}')
time.sleep(2 ** attempt)
raise Exception(f'Failed after {retries} retries: {symbol}')
def fetch_orderbook_sdk(symbol, retries=3):
url = f'{BYBIT_BASE}/v5/market/orderbook'
params = {'category': 'spot', 'symbol': symbol, 'limit': 50}
for attempt in range(retries):
try:
proxy_url = phat.get_proxy_url() # auto-rotates
res = requests.get(url, params=params,
proxies={'https': proxy_url, 'http': proxy_url},
timeout=10)
data = res.json()
if data.get('retCode') == 0:
return data['result']
raise Exception(f"retCode {data.get('retCode')}: {data.get('retMsg')}")
except Exception as e:
print(f'[{symbol}] {e}, retry {attempt+1}')
time.sleep(2 ** attempt)
raise Exception(f'Failed after {retries} retries: {symbol}')
# Rate budget: 5 req/sec
for sym in SYMBOLS:
t0 = time.time()
try:
book = fetch_orderbook_sdk(sym)
print(f"{sym} best bid: {book['bids'][0][0]}")
except Exception as e:
print(f"{sym} failed: {e}")
elapsed = time.time() - t0
if elapsed < 0.2:
time.sleep(0.2 - elapsed)
Example 4: Python — Sticky Sessions for Kline Pagination
Kline (candlestick) data is paginated by time range. If you page through historical data with start and end timestamps, rotating IPs on every request can cause inconsistent gaps. A sticky session keeps the same exit IP for a logical pagination sequence, ensuring consistent rate-limit accounting and avoiding mid-pagination IP bans.
import requests
import time
BYBIT_BASE = 'https://api.bybit.com'
def fetch_klines_paginated(symbol, category='linear', interval='60',
start_ts=None, end_ts=None, max_pages=50):
"""Paginate kline data using a sticky proxy session.
Uses user-session-kline01 to keep the same exit IP across
all pagination requests for this symbol.
"""
session_id = f'kline-{symbol}'
proxy_user = f'user-country-DE-session-{session_id}'
proxies = {
'http': f'http://{proxy_user}:pass@gate.proxyhat.com:8080',
'https': f'http://{proxy_user}:pass@gate.proxyhat.com:8080',
}
all_candles = []
cursor = start_ts
url = f'{BYBIT_BASE}/v5/market/kline'
for page in range(max_pages):
params = {
'category': category,
'symbol': symbol,
'interval': interval,
'start': cursor,
'end': end_ts,
'limit': 1000, # max candles per request
}
for attempt in range(3):
try:
res = requests.get(url, params=params, proxies=proxies,
timeout=15)
if res.status_code == 403:
print(f'403 on page {page}, retry {attempt+1}')
time.sleep(2 ** attempt)
continue
data = res.json()
if data.get('retCode') != 0:
raise Exception(f"retCode {data['retCode']}: {data['retMsg']}")
candles = data['result']['list']
if not candles:
return all_candles
all_candles.extend(candles)
# Move cursor forward: last candle's timestamp + 1ms
cursor = int(candles[-1][0]) + 1
if cursor >= end_ts:
return all_candles
break
except requests.RequestException as e:
print(f'Page {page} error: {e}, retry {attempt+1}')
time.sleep(2 ** attempt)
else:
raise Exception(f'Failed page {page} after 3 retries')
# Rate budget: 2 req/sec for kline endpoint
time.sleep(0.5)
return all_candles
# Usage: fetch 1-hour candles for BTCUSDT over a time range
candles = fetch_klines_paginated(
symbol='BTCUSDT',
category='linear',
interval='60',
start_ts=1700000000000,
end_ts=1700600000000,
)
print(f'Fetched {len(candles)} candles')
Example 5: Node.js — Concurrent Ticker Fetch with Rate Budget and Circuit Breaker
For production scrapers, you need a circuit breaker that stops sending requests when the failure rate exceeds a threshold. This prevents cascading failures when Bybit's API or your proxy network is degraded.
import axios from 'axios';
const BYBIT_BASE = 'https://api.bybit.com';
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT',
'ADAUSDT', 'AVAXUSDT', 'LINKUSDT', 'MATICUSDT', 'DOTUSDT'];
// Simple circuit breaker
class CircuitBreaker {
constructor(threshold = 5, resetMs = 60000) {
this.failures = 0;
this.threshold = threshold;
this.resetMs = resetMs;
this.openUntil = 0;
}
recordFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.openUntil = Date.now() + this.resetMs;
console.warn(`Circuit open until ${new Date(this.openUntil).toISOString()}`);
}
}
recordSuccess() { this.failures = 0; }
isOpen() { return Date.now() < this.openUntil; }
}
const breaker = new CircuitBreaker(5, 60000);
function proxyConfig(country = 'DE') {
return {
host: 'gate.proxyhat.com',
port: 8080,
auth: { username: `user-country-${country}`, password: 'pass' },
protocol: 'http',
};
}
async function fetchTicker(symbol) {
if (breaker.isOpen()) {
throw new Error('Circuit breaker open');
}
const url = `${BYBIT_BASE}/v5/market/tickers?category=spot&symbol=${symbol}`;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await axios.get(url, {
proxy: proxyConfig('DE'),
timeout: 10000,
});
if (res.data.retCode === 0) {
breaker.recordSuccess();
return { symbol, ...res.data.result.list[0] };
}
throw new Error(`retCode ${res.data.retCode}`);
} catch (err) {
if (err.response?.status === 403) breaker.recordFailure();
if (attempt < 2) await new Promise(r => setTimeout(r, 2 ** attempt * 500));
else throw err;
}
}
}
// Process in batches of 3 with 250ms spacing (max ~12 req/sec)
const BATCH_SIZE = 3;
const BATCH_DELAY_MS = 250;
async function scrapeTickers() {
for (let i = 0; i < SYMBOLS.length; i += BATCH_SIZE) {
const batch = SYMBOLS.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(
batch.map(sym => fetchTicker(sym))
);
for (const r of results) {
if (r.status === 'fulfilled') {
console.log(`${r.value.symbol}: $${r.value.lastPrice}`);
} else {
console.error(`Error: ${r.reason.message}`);
}
}
await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
}
}
scrapeTickers();
Example 6: Python — Recent Trades with SOCKS5 Proxy
Some network environments require SOCKS5 instead of HTTP proxying. ProxyHat supports SOCKS5 on port 1080. In Python, you need the requests[socks] package installed (pip install requests[socks]).
import requests
import time
BYBIT_BASE = 'https://api.bybit.com'
def fetch_recent_trades(symbol, category='spot', limit=60, retries=3):
url = f'{BYBIT_BASE}/v5/market/recent-trade'
params = {'category': category, 'symbol': symbol, 'limit': limit}
# SOCKS5 proxy with Japanese geo-targeting
socks_proxy = {
'http': 'socks5://user-country-JP:pass@gate.proxyhat.com:1080',
'https': 'socks5://user-country-JP:pass@gate.proxyhat.com:1080',
}
for attempt in range(retries):
try:
res = requests.get(url, params=params, proxies=socks_proxy,
timeout=15)
if res.status_code == 403:
print(f'[{symbol}] 403, retry {attempt+1}')
time.sleep(2 ** attempt)
continue
data = res.json()
if data.get('retCode') == 0:
return data['result']['list']
raise Exception(f"retCode {data['retCode']}: {data['retMsg']}")
except Exception as e:
print(f'[{symbol}] {e}, retry {attempt+1}')
time.sleep(2 ** attempt)
raise Exception(f'Failed: {symbol}')
trades = fetch_recent_trades('BTCUSDT')
print(f'Got {len(trades)} recent trades')
for t in trades[:5]:
print(f" price={t['price']} size={t['size']} side={t['isBuyerMaker']}")
REST Snapshots vs WebSocket Orderbook.50 Stream
Polling /v5/market/orderbook via REST gives you point-in-time snapshots. For live order-book monitoring — especially for HFT-style strategies or real-time spread analysis — Bybit offers a public WebSocket feed. The orderbook.50.{symbol} topic pushes depth updates at 10ms intervals for the top 50 levels, which is far more efficient than polling REST every 100ms.
| Aspect | REST /v5/market/orderbook | WebSocket orderbook.50 |
|---|---|---|
| Latency | ~50–200ms per poll | ~10ms push interval |
| Rate limit impact | Consumes per-IP budget | Single connection, no polling |
| Data type | Full snapshot | Incremental updates (delta) |
| Best for | Periodic snapshots, backfill | Real-time depth monitoring |
| Proxy requirement | HTTP/SOCKS5 proxy per request | WS proxy (SOCKS5 recommended) |
For WebSocket connections, SOCKS5 on gate.proxyhat.com:1080 is the recommended transport. Most WebSocket client libraries support SOCKS5 proxies natively or via an agent wrapper. Keep in mind that a single WebSocket connection uses one IP for its lifetime — use a sticky session if you need to reconnect without changing IPs.
Common Mistakes and Edge Cases
- Ignoring the retCode envelope: A
200HTTP status withretCode > 0is still an error. Always checkretCode === 0before processingresult. - Polling orderbook in a tight loop: This is the fastest way to get a 10-minute IP ban. Always implement rate budgeting — even with rotating proxies, each individual IP has its own limit.
- Using datacenter proxies for geo-restricted access: Datacenter IP ranges are more likely to be flagged or blocked. Residential proxies provide better access from allowed regions.
- Not handling CloudFront 403s: A CloudFront
403returns HTML, not JSON. Your JSON parser will throw — catch this and treat it as a geo-block, not a rate limit. - Rotating IPs during kline pagination: This can cause duplicate or missing candles if Bybit's internal sharding differs by IP. Use sticky sessions (
-session-) for pagination sequences. - Assuming category defaults: The
categoryparameter is required on most V5 market endpoints. Omitting it returns an error, not a default category.
ProxyHat Setup and Pricing
Getting started with ProxyHat for Bybit scraping is straightforward. Create an account at dashboard.proxyhat.com, choose a residential proxy plan, and use the credentials provided with the gateway details above. See our pricing page for current plans and bandwidth options.
For more scraping use cases and patterns, check out our web scraping use case guide and SERP tracking guide. Full API and SDK documentation is available at docs.proxyhat.com.
Key Takeaways
- Bybit's V5 Market API consolidates spot, linear, inverse, and options behind four public endpoints with a
categoryparameter and aretCode/retMsgenvelope.- Rate limits are per-IP and violations trigger a ~10-minute ban. The
/v5/market/orderbookendpoint is the most aggressively limited.- Geo-restrictions return
403or CloudFront errors. Residential proxies with country targeting (e.g.,user-country-DE) bypass these blocks.- Use per-request rotation for order-book polling and sticky sessions (
-session-) for kline pagination.- Always implement retries with backoff, rate budgeting, and a circuit breaker in production scrapers.
- Prefer WebSocket
orderbook.50for real-time depth; use REST for snapshots and historical backfill.
FAQ
What is the Bybit V5 Market API and why do I need proxies to scrape it?
The Bybit V5 Market API is a set of public REST endpoints (/v5/market/orderbook, /v5/market/tickers, /v5/market/kline, /v5/market/recent-trade) that provide real-time and historical market data for spot, derivatives, and options. You need proxies because Bybit enforces strict per-IP rate limits that can trigger a ~10-minute ban, and geo-restrictions that block requests from certain countries. Rotating residential proxies distribute requests across many IPs and allow geo-targeting to allowed regions.
Which proxy type works best for scraping Bybit's API?
Residential proxies are the best choice for Bybit scraping. They use real ISP-assigned IP addresses, which are less likely to be flagged by Bybit's anti-bot systems or CloudFront CDN. Datacenter proxies are cheaper but more easily detected and blocked. For order-book polling, use per-request rotation; for kline pagination, use sticky sessions to maintain the same IP across a pagination sequence. SOCKS5 on port 1080 is recommended for WebSocket connections.
How do you avoid 403 blocks when scraping the Bybit V5 Market API?
To avoid 403 blocks, implement three strategies: (1) use residential proxies geo-targeted to an allowed country like Germany or Japan, (2) enforce per-endpoint rate budgeting — for example, no more than 5 requests per second per IP for order-book polling, and (3) implement exponential backoff retries with a circuit breaker that stops requests when the failure rate exceeds a threshold. Also distinguish between rate-limit 403s (JSON body with retCode) and geo-block 403s (CloudFront HTML response).
What is the difference between Bybit's REST orderbook endpoint and the WebSocket stream?
The REST endpoint /v5/market/orderbook returns full point-in-time snapshots and consumes your per-IP rate-limit budget on every poll. The WebSocket orderbook.50.{symbol} topic pushes incremental depth updates at approximately 10ms intervals over a single long-lived connection. REST is best for periodic snapshots and historical backfill; WebSocket is best for real-time depth monitoring and HFT-style strategies. For WebSocket, use SOCKS5 proxies on gate.proxyhat.com:1080.
How does the category parameter work in Bybit's V5 Market API?
The category parameter specifies the product type: spot for spot trading, linear for USDT-margined perpetuals and futures, inverse for inverse contracts, and option for options. The same endpoint path serves all categories — you change the parameter value. For example, /v5/market/orderbook?category=spot&symbol=BTCUSDT returns the spot order book, while category=linear returns the USDT perpetual order book. The category is required on most V5 market endpoints.






