◈   ⌘ api · Intermediate

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.

Uncle Solieditor · voc · 05.05.2026 ·views 12
◈   Contents
  1. → What Is the Bybit Unified Margin Account?
  2. → Authentication Setup: Signing Your API Requests
  3. → Placing and Managing Orders via the V5 API
  4. → Querying Positions and Account State
  5. → Real-Time Data with Bybit WebSocket Streams
  6. → Frequently Asked Questions
  7. → Putting It All Together

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.

What Is the Bybit Unified Margin Account?

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.

Authentication Setup: Signing Your API Requests

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.

Placing and Managing Orders via the V5 API

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.

Bybit V5 Order Types by Category
CategoryAvailable Order TypesLeverage
spotLimit, MarketUp to 10x (margin)
linear (USDT perp)Limit, Market, ConditionalUp to 100x
inverseLimit, Market, ConditionalUp to 100x
optionLimit, MarketN/A (premium based)

Querying Positions and Account State

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.

Real-Time Data with Bybit WebSocket Streams

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.

Frequently Asked Questions

Do I need a Unified Margin Account to use the Bybit V5 API?
No, but you lose access to the consolidated balance and cross-product collateral features. Classic accounts still work with V5, but if you're building anything new, migrate to Unified — the API is simpler and the collateral efficiency is better.
How do I handle API rate limit errors in my bot?
Bybit returns retCode 10006 for rate limit hits. Implement exponential backoff starting at 500ms and cap retries at 3-5 attempts. Restructure your polling logic to use WebSocket subscriptions for data that updates frequently — this eliminates most rate limit pressure.
Can the Bybit API place orders for both spot and futures in the same session?
Yes, that's the core advantage of the Unified Margin API. Use the same API key and authentication — just change the category parameter from 'spot' to 'linear' or 'inverse' per order. Your collateral is shared automatically.
What's the difference between Bybit's linear and inverse contracts in the API?
Linear contracts (BTCUSDT, ETHUSDT) are settled and margined in USDT. Inverse contracts (BTCUSD) are margined in the base coin (BTC) and settled in BTC. The API calls are identical structure-wise — only the category field and the denomination of qty and price differ.
Is there a testnet for the Bybit API before trading with real funds?
Yes. Use https://api-testnet.bybit.com for REST and wss://stream-testnet.bybit.com/v5/ for WebSocket. Create separate API keys on testnet.bybit.com. Always develop and test there first — a bug in order sizing logic with real funds is an expensive lesson.
How do I receive VoiceOfChain signals and auto-execute them on Bybit?
Set up a webhook endpoint in your bot that receives the signal payload, parses the symbol, side, and position size, then calls the Bybit order endpoint. Use the WebSocket private stream to confirm fill. Validate signal parameters against your risk rules before every execution.

Putting It All Together

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.

◈   more on this topic
◉ basics Mastering the ccxt library documentation for crypto traders ⌂ exchanges Mastering the Binance CCXT Library for Crypto Traders ⌬ bots Best Crypto Trading Bots 2025: Profitable AI-Powered Strategies