Bybit Unified Margin API: Complete Guide for Traders
Learn how to integrate Bybit's Unified Margin API for automated trading — from authentication setup to order execution, position tracking, and WebSocket streams.
Learn how to integrate Bybit's Unified Margin API for automated trading — from authentication setup to order execution, position tracking, and WebSocket streams.
Bybit's Unified Margin Account (UMA) changed how traders interact with the exchange programmatically. Instead of juggling separate wallets for spot, futures, and options, the unified account pools your collateral across all positions — and the API reflects that unified structure. If you're building a trading bot, running automated strategies, or integrating signal feeds from platforms like VoiceOfChain, understanding this API is non-negotiable.
Launched as part of Bybit's V5 API overhaul, the Unified Margin Account lets a single wallet serve as collateral for spot margin, USDT perpetuals, USDC perpetuals, inverse contracts, and options simultaneously. Compare this to how Binance handles isolated wallets for each product line — on Bybit's unified system, a single API call can query your entire financial picture across every market type.
From a developer's perspective, this simplifies your architecture significantly. You're not managing multiple authentication contexts or reconciling balances across sub-accounts. The V5 API endpoint structure is consistent: category parameter (spot, linear, inverse, option) determines which market you're operating in, while the account data stays consolidated. Exchanges like OKX have a similar unified account concept, but Bybit's V5 implementation is particularly clean to work with programmatically.
Bybit V5 API replaces the older V3 endpoints. If your bot was built on V3, migrate now — Bybit has officially deprecated those endpoints and the Unified Margin features are V5-only.
Bybit uses HMAC-SHA256 request signing. Every private endpoint requires four headers: your API key, a millisecond timestamp, a receive window (how long the request stays valid), and the computed signature. The signature is built from a concatenation of timestamp + api_key + recv_window + query_string_or_body.
import hashlib
import hmac
import time
import requests
import json
API_KEY = "your_api_key_here"
API_SECRET = "your_api_secret_here"
BASE_URL = "https://api.bybit.com"
RECV_WINDOW = "5000"
def generate_signature(params: str, timestamp: str) -> str:
"""Sign the request payload using HMAC-SHA256."""
param_str = f"{timestamp}{API_KEY}{RECV_WINDOW}{params}"
return hmac.new(
API_SECRET.encode("utf-8"),
param_str.encode("utf-8"),
hashlib.sha256
).hexdigest()
def get_auth_headers(query_string: str = "") -> dict:
timestamp = str(int(time.time() * 1000))
signature = generate_signature(query_string, timestamp)
return {
"X-BAPI-API-KEY": API_KEY,
"X-BAPI-SIGN": signature,
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": RECV_WINDOW,
"Content-Type": "application/json"
}
# Test: fetch unified wallet balance
def get_wallet_balance(account_type: str = "UNIFIED") -> dict:
endpoint = "/v5/account/wallet-balance"
params = f"accountType={account_type}"
headers = get_auth_headers(params)
response = requests.get(
f"{BASE_URL}{endpoint}?{params}",
headers=headers
)
data = response.json()
if data["retCode"] != 0:
raise Exception(f"API error {data['retCode']}: {data['retMsg']}")
return data["result"]
balance = get_wallet_balance()
print(json.dumps(balance, indent=2))
One thing that trips up developers coming from Binance's API: Bybit's signature includes the recv_window in the signed string, not just as a header. Miss that detail and every authenticated request returns a signature mismatch error. The receive window defaults to 5000ms — tighten it for production systems to reduce replay attack exposure.
All order operations go through a single endpoint regardless of market type: /v5/order/create for new orders, /v5/order/cancel to cancel, and /v5/order/realtime to query open orders. The category field routes the order to the right engine. This unified design is a genuine improvement over older exchange APIs where spot and derivatives had entirely separate codebases.
def place_limit_order(
symbol: str,
side: str, # "Buy" or "Sell"
qty: str,
price: str,
category: str = "linear" # spot | linear | inverse | option
) -> dict:
endpoint = "/v5/order/create"
payload = {
"category": category,
"symbol": symbol,
"side": side,
"orderType": "Limit",
"qty": qty,
"price": price,
"timeInForce": "GTC",
"reduceOnly": False,
"closeOnTrigger": False
}
body_str = json.dumps(payload)
headers = get_auth_headers(body_str)
response = requests.post(
f"{BASE_URL}{endpoint}",
headers=headers,
data=body_str
)
data = response.json()
if data["retCode"] != 0:
raise Exception(f"Order failed {data['retCode']}: {data['retMsg']}")
return data["result"]
# Example: Buy 0.01 BTC perpetual at $65,000
try:
order = place_limit_order(
symbol="BTCUSDT",
side="Buy",
qty="0.01",
price="65000",
category="linear"
)
print(f"Order placed: {order['orderId']}")
except Exception as e:
print(f"Order error: {e}")
For market orders, set orderType to "Market" and omit the price field. If you're integrating automated signals — for example, acting on alerts from VoiceOfChain — market orders give you immediate fill at the cost of potential slippage during volatile periods. Limit orders give you price control but require active management if the market moves away. Most production bots use limit orders with a fallback to market if the position isn't filled within a configurable timeout.
| Category | Available Order Types | Leverage |
|---|---|---|
| spot | Limit, Market | Up to 10x (margin) |
| linear (USDT perp) | Limit, Market, Conditional | Up to 100x |
| inverse | Limit, Market, Conditional | Up to 100x |
| option | Limit, Market | N/A (premium based) |
The unified account's real power shows when you query positions. A single call to /v5/position/list with no symbol filter returns all open positions across linear and inverse contracts. Pair this with /v5/account/wallet-balance to get your complete financial state. Platforms like OKX and Gate.io also offer position aggregation, but having it all under one Bybit unified account is operationally cleaner for multi-strategy bots.
def get_open_positions(category: str = "linear", symbol: str = None) -> list:
endpoint = "/v5/position/list"
params = f"category={category}"
if symbol:
params += f"&symbol={symbol}"
headers = get_auth_headers(params)
response = requests.get(
f"{BASE_URL}{endpoint}?{params}",
headers=headers
)
data = response.json()
if data["retCode"] != 0:
raise Exception(f"Position query failed: {data['retMsg']}")
positions = data["result"]["list"]
# Filter to only positions with non-zero size
active = [p for p in positions if float(p.get("size", 0)) > 0]
return active
def print_position_summary(positions: list):
for pos in positions:
symbol = pos["symbol"]
side = pos["side"]
size = pos["size"]
entry = pos["avgPrice"]
pnl = pos["unrealisedPnl"]
liq_price = pos["liqPrice"]
print(f"{symbol} | {side} {size} @ {entry} | uPnL: {pnl} | Liq: {liq_price}")
positions = get_open_positions(category="linear")
print_position_summary(positions)
Always monitor liqPrice in your position data. If your unrealised loss pushes you close to the liquidation price, your bot should either add margin or reduce position size automatically — not wait for a human to notice.
REST polling is fine for order management, but for real-time price data and position updates you want WebSocket connections. Bybit's V5 WebSocket has two main endpoints: wss://stream.bybit.com/v5/public/{category} for market data and wss://stream.bybit.com/v5/private for account updates. The private stream pushes order fills and position changes as they happen — critical if your strategy reacts to fills in real-time.
If you use a signal platform like VoiceOfChain that generates entry/exit alerts, the workflow typically looks like: receive signal via webhook → validate against current positions via REST → execute order → monitor fill via WebSocket private stream → update local state. This is more reliable than polling /v5/order/realtime every second, which both hammers rate limits and introduces unnecessary latency.
import asyncio
import websockets
import json
import hmac
import hashlib
import time
WS_PRIVATE_URL = "wss://stream.bybit.com/v5/private"
def ws_auth_payload(api_key: str, api_secret: str) -> str:
expires = int(time.time() * 1000) + 10000 # valid for 10 seconds
sig_str = f"GET/realtime{expires}"
signature = hmac.new(
api_secret.encode(),
sig_str.encode(),
hashlib.sha256
).hexdigest()
return json.dumps({
"op": "auth",
"args": [api_key, expires, signature]
})
async def stream_order_updates():
async with websockets.connect(WS_PRIVATE_URL) as ws:
# Authenticate
await ws.send(ws_auth_payload(API_KEY, API_SECRET))
auth_response = json.loads(await ws.recv())
if not auth_response.get("success"):
raise Exception(f"WS auth failed: {auth_response}")
print("WebSocket authenticated")
# Subscribe to order updates
await ws.send(json.dumps({
"op": "subscribe",
"args": ["order", "position"]
}))
async for message in ws:
data = json.loads(message)
if data.get("topic") == "order":
for order in data.get("data", []):
print(f"Order update: {order['symbol']} {order['side']} "
f"status={order['orderStatus']} "
f"filled={order['cumExecQty']}/{order['qty']}")
elif data.get("topic") == "position":
for pos in data.get("data", []):
print(f"Position update: {pos['symbol']} size={pos['size']} "
f"uPnL={pos['unrealisedPnl']}")
asyncio.run(stream_order_updates())
Rate limits on the V5 REST API are per-endpoint per IP: 10 requests per second for order creation on linear contracts, 50 r/s for position queries. WebSocket subscriptions don't count against REST limits. For high-frequency bots, structure your architecture so order management goes through REST while all monitoring happens via WebSocket — this keeps you well within limits even on busy market sessions.
Bybit's Unified Margin API with V5 endpoints is one of the cleaner exchange APIs to build on right now. The unified account model removes the collateral fragmentation problem that makes multi-strategy bots complicated on exchanges like Binance, where you're constantly moving funds between sub-accounts. The consistent endpoint structure across spot, perps, and options means your core authentication and order logic is reusable everywhere.
A production-ready bot architecture built on this API looks like: REST for order submission and account queries, WebSocket private stream for fill confirmation and position monitoring, and a dedicated reconnection handler for the WebSocket (these connections drop; plan for it). If you're layering in external signal sources like VoiceOfChain or custom technical analysis, use the WebSocket public stream for low-latency price data rather than polling REST. Keep your API keys in environment variables, scope them to the minimum permissions needed, and whitelist your server IP in the Bybit key settings. The infrastructure is solid — what you build on top of it is up to you.