◈   ⌘ api · Intermediate

Bybit Unified Account API with Python: Complete Guide

Master Bybit's Unified Account API using Python — from authentication and account setup to live order execution and position management for algo traders.

Uncle Solieditor · voc · 19.05.2026 ·views 7
◈   Contents
  1. → Understanding the Unified Account Structure
  2. → Setting Up Authentication and Your Python Environment
  3. → Querying Account Balances and Positions
  4. → Placing and Managing Orders
  5. → WebSocket Streaming for Real-Time Data
  6. → Error Handling and Rate Limit Management
  7. → Frequently Asked Questions
  8. → Building a Production-Ready Foundation

Bybit's Unified Trading Account (UTA) changed the game for serious traders. Instead of juggling separate spot, derivatives, and options wallets, you get a single margin pool that works across all product types. For Python developers building bots or systematic strategies, this means one API surface to master instead of three. The pybit library is the official Python SDK, and it abstracts most of the complexity — but knowing what's happening underneath is what separates a bot that works from one that blows up at 3am.

Understanding the Unified Account Structure

Before writing a single line of code, understand what 'unified' actually means in practice. On Bybit, the Unified Account consolidates margin across USDT Perpetuals, USDC Perpetuals, Inverse Contracts, Spot, and Options into one account. Your BTC held as collateral can back a USDT-margined ETH short — that cross-margining is the core feature. Contrast this with Binance, where you still manage separate Sub-wallets for different product lines, or OKX's Unified Account which works similarly to Bybit's but has different tier requirements. The practical implication for your Python bot: all account state queries go to one endpoint, and your position risk is portfolio-level, not per-product.

Bybit offers two account modes: Classic and Unified. Most new accounts default to Unified. Verify your account type in the dashboard before testing — Classic accounts use different endpoints and the pybit session classes differ.

Setting Up Authentication and Your Python Environment

Start by installing pybit, the officially maintained Python SDK for Bybit. It handles request signing, rate limiting, and WebSocket reconnection logic so you don't have to reinvent those wheels.

pip install pybit python-dotenv

Store your API credentials in a .env file — never hardcode them in source files, especially if you're pushing to GitHub. Create your API key in Bybit's account settings under API Management, and enable the permissions you actually need (read-only for monitoring bots, trade permissions only for execution bots).

import os
from dotenv import load_dotenv
from pybit.unified_trading import HTTP

load_dotenv()

API_KEY = os.getenv("BYBIT_API_KEY")
API_SECRET = os.getenv("BYBIT_API_SECRET")

# testnet=True for paper trading, testnet=False for live
session = HTTP(
    testnet=False,
    api_key=API_KEY,
    api_secret=API_SECRET
)

# Verify connectivity and check account type
try:
    info = session.get_account_info()
    account_type = info["result"]["unifiedMarginStatus"]
    print(f"Account type: {account_type}")
    # 1 = Classic, 3 = Unified Margin, 4 = UTA Pro
except Exception as e:
    print(f"Auth failed: {e}")

The unifiedMarginStatus field tells you exactly what mode you're in. Status 4 is UTA Pro with portfolio margin — the full unified experience. Status 3 is standard Unified Account. If you get status 1 you're on Classic and need to upgrade through the Bybit UI before the unified endpoints will return meaningful data.

Querying Account Balances and Positions

The wallet balance endpoint is your single source of truth for capital available across the entire unified account. It returns equity, unrealized PnL, available margin, and a breakdown by coin — all in one call.

def get_account_state(session):
    """Fetch full unified account snapshot."""
    try:
        response = session.get_wallet_balance(accountType="UNIFIED")
        
        if response["retCode"] != 0:
            raise Exception(f"API error: {response['retMsg']}")
        
        account = response["result"]["list"][0]
        
        total_equity = float(account["totalEquity"])
        total_unrealized_pnl = float(account["totalPerpUPL"])
        available_to_withdraw = float(account["totalAvailableBalance"])
        
        print(f"Total Equity:        ${total_equity:,.2f}")
        print(f"Unrealized PnL:      ${total_unrealized_pnl:,.2f}")
        print(f"Available Balance:   ${available_to_withdraw:,.2f}")
        
        # Coin-level breakdown
        for coin in account["coin"]:
            if float(coin["walletBalance"]) > 0:
                print(f"  {coin['coin']}: {coin['walletBalance']} "
                      f"(usd val: {coin['usdValue']})")
        
        return account
    
    except KeyError as e:
        print(f"Unexpected response structure: {e}")
        return None

account_data = get_account_state(session)

For open positions, use get_positions with category set to 'linear' for USDT perpetuals or 'inverse' for coin-margined contracts. The unified account returns all positions regardless of which sub-product they belong to, so you can build a complete portfolio view without multiple calls.

def get_open_positions(session, symbol=None):
    """Get all open positions or filter by symbol."""
    params = {
        "category": "linear",
        "settleCoin": "USDT"
    }
    if symbol:
        params["symbol"] = symbol
    
    response = session.get_positions(**params)
    
    if response["retCode"] != 0:
        raise Exception(f"Positions fetch failed: {response['retMsg']}")
    
    positions = response["result"]["list"]
    active = [p for p in positions if float(p["size"]) > 0]
    
    for pos in active:
        side = pos["side"]  # Buy or Sell
        size = pos["size"]
        entry = pos["avgPrice"]
        pnl = pos["unrealisedPnl"]
        liq_price = pos["liqPrice"]
        print(f"{pos['symbol']} {side} {size} @ {entry} | "
              f"uPnL: {pnl} | Liq: {liq_price}")
    
    return active

positions = get_open_positions(session)

Placing and Managing Orders

Order placement on the Unified Account uses the place_order endpoint with a category parameter that routes to the correct product. For most systematic traders, linear USDT perpetuals are the primary instrument — deep liquidity, straightforward funding, and the unified margin means you can hold spot BTC as collateral while running perp strategies simultaneously.

def place_limit_order(session, symbol, side, qty, price, reduce_only=False):
    """
    Place a limit order on Bybit USDT perpetuals.
    side: 'Buy' or 'Sell'
    qty: position size in base currency (e.g. '0.01' for 0.01 BTC)
    price: limit price as string
    """
    try:
        response = session.place_order(
            category="linear",
            symbol=symbol,
            side=side,
            orderType="Limit",
            qty=str(qty),
            price=str(price),
            timeInForce="PostOnly",  # maker-only, avoids taker fees
            reduceOnly=reduce_only,
            closeOnTrigger=False
        )
        
        if response["retCode"] != 0:
            print(f"Order rejected: {response['retMsg']}")
            return None
        
        order_id = response["result"]["orderId"]
        print(f"Order placed: {order_id}")
        return order_id
    
    except Exception as e:
        print(f"Order placement error: {e}")
        return None


def cancel_order(session, symbol, order_id):
    """Cancel an open order by ID."""
    response = session.cancel_order(
        category="linear",
        symbol=symbol,
        orderId=order_id
    )
    if response["retCode"] == 0:
        print(f"Order {order_id} cancelled")
    else:
        print(f"Cancel failed: {response['retMsg']}")


# Example: place a buy limit 0.01 BTC at $65,000
order_id = place_limit_order(
    session,
    symbol="BTCUSDT",
    side="Buy",
    qty=0.01,
    price=65000
)

# Cancel it if needed
if order_id:
    cancel_order(session, "BTCUSDT", order_id)
Use PostOnly for limit orders when you want to guarantee maker fee rates. Bybit's maker fee on VIP0 is 0.01% vs 0.06% taker. On a $1M monthly volume that difference is $500. PostOnly orders get rejected if they would immediately fill — handle that case in your error logic.

WebSocket Streaming for Real-Time Data

REST polling is fine for account queries and order management, but for real-time price data and trade signals you need WebSocket streams. Bybit's public WebSocket feeds orderbook, trades, and kline data with sub-100ms latency. For private streams (your positions and orders updating in real time), you authenticate the WebSocket connection with a time-limited signature.

If you're using VoiceOfChain for trade signals, the workflow typically looks like: VoiceOfChain fires a signal → your Python bot receives it via webhook or its own analysis → WebSocket confirms current price before execution → REST call places the order. This pattern avoids the race condition of placing orders on stale REST data.

from pybit.unified_trading import WebSocket
from time import sleep

def handle_orderbook(message):
    """Process orderbook update."""
    data = message["data"]
    best_bid = float(data["b"][0][0]) if data["b"] else None
    best_ask = float(data["a"][0][0]) if data["a"] else None
    if best_bid and best_ask:
        spread = best_ask - best_bid
        mid = (best_bid + best_ask) / 2
        print(f"BTC | Bid: {best_bid} | Ask: {best_ask} | "
              f"Spread: {spread:.1f} | Mid: {mid:.1f}")

def handle_private_orders(message):
    """Process private order updates."""
    for order in message["data"]:
        print(f"Order update: {order['orderId']} "
              f"status={order['orderStatus']} "
              f"filled={order['cumExecQty']}/{order['qty']}")

# Public WebSocket — no auth needed
ws_public = WebSocket(
    testnet=False,
    channel_type="linear"
)
ws_public.orderbook_stream(
    depth=1,
    symbol="BTCUSDT",
    callback=handle_orderbook
)

# Private WebSocket — needs API credentials
ws_private = WebSocket(
    testnet=False,
    channel_type="private",
    api_key=API_KEY,
    api_secret=API_SECRET
)
ws_private.order_stream(callback=handle_private_orders)

print("Streaming... Ctrl+C to stop")
try:
    while True:
        sleep(1)
except KeyboardInterrupt:
    ws_public.exit()
    ws_private.exit()

Error Handling and Rate Limit Management

Production bots fail at the error handling layer, not the happy path. Bybit returns error codes in retCode — zero means success, anything else is a problem. The most common issues are rate limits (retCode 10006), invalid API key after rotation (retCode 10003), and insufficient margin errors (retCode 110007). Build a retry wrapper that handles transient errors automatically but surfaces permanent errors immediately.

import time
import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Bybit error codes worth handling specifically
ERROR_RATE_LIMIT = 10006
ERROR_INVALID_KEY = 10003
ERROR_INSUFFICIENT_MARGIN = 110007
ERROR_ORDER_NOT_EXISTS = 110001

def bybit_request(max_retries=3, backoff=1.5):
    """Decorator for Bybit API calls with retry + error handling."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(max_retries):
                try:
                    response = func(*args, **kwargs)
                    ret_code = response.get("retCode", -1)
                    
                    if ret_code == 0:
                        return response
                    
                    # Non-retryable errors — fail fast
                    if ret_code in (ERROR_INVALID_KEY, ERROR_INSUFFICIENT_MARGIN):
                        logger.error(f"Fatal API error {ret_code}: "
                                     f"{response.get('retMsg')}")
                        return None
                    
                    # Rate limit — back off and retry
                    if ret_code == ERROR_RATE_LIMIT:
                        wait = backoff ** attempt
                        logger.warning(f"Rate limited, waiting {wait:.1f}s")
                        time.sleep(wait)
                        continue
                    
                    logger.error(f"API error {ret_code}: {response.get('retMsg')}")
                    last_error = response
                    
                except Exception as e:
                    logger.error(f"Request exception (attempt {attempt+1}): {e}")
                    time.sleep(backoff ** attempt)
            
            return last_error
        return wrapper
    return decorator


@bybit_request(max_retries=3)
def safe_get_balance(session):
    return session.get_wallet_balance(accountType="UNIFIED")


@bybit_request(max_retries=2)
def safe_place_order(session, **kwargs):
    return session.place_order(**kwargs)


# Usage
balance = safe_get_balance(session)
if balance:
    print(f"Equity: {balance['result']['list'][0]['totalEquity']}")

Bybit's rate limits for the Unified Account REST API are generous compared to, say, Coinbase Advanced Trade or Gate.io — 10 requests per second for most order endpoints at VIP0. The WebSocket approach eliminates most polling entirely, so you rarely need to worry about rate limits in a well-designed bot. Keep a request counter per second in production and log any 10006 responses as they indicate architectural issues, not transient noise.

Bybit Unified Account API Rate Limits (VIP0)
EndpointLimitWindow
Place/Amend/Cancel Order10 req/sPer second
Get Orders (open)10 req/sPer second
Get Positions10 req/sPer second
Get Wallet Balance10 req/sPer second
WebSocket (private)UnlimitedPer connection

Frequently Asked Questions

Do I need to upgrade my Bybit account to use the Unified Account API?
Yes — Classic accounts and Unified Accounts use different endpoints and pybit session classes. Check your account type in Bybit's dashboard under 'Account & Security'. If you're on Classic, you can upgrade to Unified from the same page, but note that the upgrade is irreversible.
Is pybit the only Python library for the Bybit API?
Pybit is the officially maintained SDK and the best choice for production use. There are community wrappers, but they often lag behind API version updates. Bybit updated to V5 API in late 2023 and pybit 5.x fully supports it — make sure you're not using an older version that targets V3.
How do I test my bot without risking real money?
Bybit has a full testnet at testnet.bybit.com with separate API keys. Set testnet=True in your HTTP or WebSocket session. The testnet environment mirrors the production Unified Account structure, so code that works there will work live. Fund your testnet account from the testnet faucet in the dashboard.
Why does my order get rejected with 'insufficient available balance' even though I have funds?
In the Unified Account, available margin depends on your current position risk and the Initial Margin Rate (IMR) of your open positions. If you're heavily positioned, your 'available' balance shown in the API may be much less than your total equity. Reduce position sizes or add collateral. Also verify you're reading totalAvailableBalance, not totalWalletBalance.
Can I run the same bot logic on multiple exchanges?
Yes, and it's a common pattern. Libraries like ccxt provide a unified interface across Binance, Bybit, OKX, KuCoin, and others, but they sacrifice exchange-specific features like Bybit's portfolio margin. For serious algos, build exchange-specific adapters but share the strategy logic — that way you can route signals from VoiceOfChain to whichever exchange has the best liquidity at execution time.
What happens to open orders if my bot crashes?
Orders placed via the API remain active on Bybit's servers until they fill, are cancelled, or expire. A bot crash does not cancel orders. Build a startup routine that fetches and reviews all open orders before your bot begins its main loop — don't assume a clean slate just because your process restarted.

Building a Production-Ready Foundation

The code patterns above give you a working foundation, but production bots need a few more layers: persistent order state (a local SQLite or Redis cache so you don't lose track of order IDs across restarts), a heartbeat monitor that alerts you if the WebSocket drops and reconnects fail, and position reconciliation that compares your bot's internal state against the actual Bybit API state at startup and periodically during operation. These aren't exotic requirements — they're the difference between a demo bot and one you can run unattended.

Pair your execution bot with quality signal sources. VoiceOfChain aggregates real-time order flow, large trade detection, and momentum signals across major markets — the kind of data that tells you whether the $65,000 BTC limit you just placed is sitting in front of accumulation pressure or about to get run through by a whale offloading. Technical execution on Bybit's Unified Account API handles the 'how to trade' — solid signal sourcing handles the 'when and which direction.' Both halves matter.

◈   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