@plutarc/bybit
v0.1.0
Published
Fully-typed Bybit V5 REST and WebSocket client for TypeScript/JavaScript
Maintainers
Readme
@plutarc/bybit
A fully-typed Bybit V5 REST and WebSocket client for TypeScript/JavaScript. Zero runtime dependencies — uses only Node.js/Bun built-ins.
Table of Contents
- Features
- Installation
- Quick Start
- REST API Reference
- WebSocket API Reference
- Utilities
- Error Handling
- Rate Limiting
- Testnet
- Custom Logger
- Development
- Resources
- License
Features
- Complete V5 REST API — Market, Order, Position, Account, and Asset endpoints with full TypeScript types
- Real-time WebSocket — Public (per-category) and private WebSocket clients with typed event emitters
- Zero dependencies — Uses only
node:cryptoandnode:eventsbuilt-ins - Fully typed — Every request parameter and response field has TypeScript interfaces
- Unified V5 API — Single client for spot, linear, inverse, and option markets via
categoryparameter - Rate limiting — Built-in rolling window rate limiter that tracks
X-Bapi-Limit-Status - Auto-retry — Exponential backoff with jitter for retryable errors (429, 5xx)
- Auto-reconnect — WebSocket reconnection with exponential backoff
- Ping/pong — Automatic JSON heartbeat to keep WebSocket connections alive
- Delta parser — Generic WebSocket state management with orderbook convenience store
- Authentication — HMAC-SHA256 signing for both REST and WebSocket
- Testnet support — Single flag to switch between production and testnet
- Dual format — Ships ESM and CJS builds with full declaration files
Installation
# bun
bun add @plutarc/bybit
# npm
npm install @plutarc/bybit
# pnpm
pnpm add @plutarc/bybitQuick Start
REST Client
import { BybitClient } from "@plutarc/bybit";
const client = new BybitClient({
apiKey: "your-api-key",
apiSecret: "your-api-secret",
testnet: true, // use testnet for development
});
// Get instrument info
const instruments = await client.market.getInstrumentsInfo({
category: "linear",
});
// Get tickers
const tickers = await client.market.getTickers({
category: "linear",
symbol: "BTCUSDT",
});
// Place a limit order
const order = await client.order.place({
category: "linear",
symbol: "BTCUSDT",
side: "Buy",
orderType: "Limit",
qty: "0.01",
price: "50000",
timeInForce: "GTC",
});
// Get wallet balance
const wallet = await client.account.getWalletBalance({
accountType: "UNIFIED",
});Public WebSocket
Bybit public WebSocket streams are per-category — each BybitPublicWs instance connects to one market type.
import { BybitPublicWs } from "@plutarc/bybit";
const ws = new BybitPublicWs({
category: "linear", // "spot" | "linear" | "inverse" | "option"
testnet: true,
});
// Subscribe to real-time trades — fully typed callback
ws.on("publicTrade", (type, data, topic) => {
for (const trade of data) {
console.log(`${trade.S} ${trade.v} @ ${trade.p}`);
}
});
// Subscribe to orderbook updates
ws.on("orderbook", (type, data, topic) => {
console.log(`Orderbook ${type}: ${data.b.length} bids, ${data.a.length} asks`);
});
// Subscribe to 1-minute klines
ws.on("kline", (type, data, topic) => {
for (const candle of data) {
console.log(`O:${candle.open} H:${candle.high} L:${candle.low} C:${candle.close}`);
}
});
// Start subscriptions and connect
ws.subscribe([
"publicTrade.BTCUSDT",
"orderbook.50.BTCUSDT",
"kline.1.BTCUSDT",
]);
ws.connect();Private WebSocket
import { BybitPrivateWs } from "@plutarc/bybit";
const ws = new BybitPrivateWs({
apiKey: "your-api-key",
apiSecret: "your-api-secret",
testnet: true,
});
// Listen for authentication success
ws.on("authenticated", () => {
console.log("Authenticated — subscribing to private topics");
ws.subscribe(["order", "position", "wallet", "execution"]);
});
// Order updates
ws.on("order", (data) => {
for (const order of data) {
console.log(`Order ${order.orderId}: ${order.orderStatus}`);
}
});
// Position updates
ws.on("position", (data) => {
for (const pos of data) {
console.log(`${pos.symbol}: size=${pos.size} pnl=${pos.unrealisedPnl}`);
}
});
// Execution (fill) updates
ws.on("execution", (data) => {
for (const exec of data) {
console.log(`Fill: ${exec.execQty} @ ${exec.execPrice}`);
}
});
ws.connect();REST API Reference
Client Options
const client = new BybitClient({
apiKey: "...", // Optional — required for authenticated endpoints
apiSecret: "...", // Optional — required for authenticated endpoints
testnet: false, // Use testnet (default: false)
baseUrl: "https://...", // Custom base URL (overrides testnet flag)
recvWindow: 5000, // Receive window in ms (default: 5000)
rateLimitPer5Seconds: 600, // Self-throttle limit (default: 600)
maxRetries: 3, // Max retries for retryable errors (default: 3)
baseRetryDelayMs: 500, // Base delay for exponential backoff (default: 500)
requestTimeoutMs: 15000, // Request timeout (default: 15000)
logger: console, // Custom logger (default: silent)
});Market
All market endpoints are public — no API key required.
// Get instrument info
const instruments = await client.market.getInstrumentsInfo({
category: "linear",
symbol: "BTCUSDT",
});
// Get tickers
const tickers = await client.market.getTickers({
category: "linear",
symbol: "BTCUSDT",
});
// Get orderbook
const book = await client.market.getOrderbook({
category: "linear",
symbol: "BTCUSDT",
limit: 50,
});
// Get klines (candlesticks)
const klines = await client.market.getKline({
category: "linear",
symbol: "BTCUSDT",
interval: "60",
limit: 100,
});
// Get recent trades
const trades = await client.market.getRecentTrade({
category: "linear",
symbol: "BTCUSDT",
limit: 50,
});
// Get funding rate history
const funding = await client.market.getFundingHistory({
category: "linear",
symbol: "BTCUSDT",
});
// Get mark price kline
const markKline = await client.market.getMarkPriceKline({
category: "linear",
symbol: "BTCUSDT",
interval: "60",
});
// Get index price kline
const indexKline = await client.market.getIndexPriceKline({
category: "linear",
symbol: "BTCUSDT",
interval: "60",
});
// Get open interest
const oi = await client.market.getOpenInterest({
category: "linear",
symbol: "BTCUSDT",
intervalTime: "1h",
});
// Get risk limit
const risk = await client.market.getRiskLimit({ category: "linear" });Order
All order endpoints require authentication. Every method takes a category parameter.
// Place an order
const order = await client.order.place({
category: "linear",
symbol: "BTCUSDT",
side: "Buy",
orderType: "Limit",
qty: "0.01",
price: "50000",
timeInForce: "GTC",
});
// Amend an order
const amended = await client.order.amend({
category: "linear",
symbol: "BTCUSDT",
orderId: "order-id",
price: "51000",
});
// Cancel an order
const cancelled = await client.order.cancel({
category: "linear",
symbol: "BTCUSDT",
orderId: "order-id",
});
// or by client order ID
const cancelled = await client.order.cancel({
category: "linear",
symbol: "BTCUSDT",
orderLinkId: "my-order-1",
});
// Get open orders
const open = await client.order.getOpen({
category: "linear",
symbol: "BTCUSDT",
});
// Get order history
const history = await client.order.getHistory({
category: "linear",
symbol: "BTCUSDT",
limit: 50,
});
// Cancel all orders
await client.order.cancelAll({
category: "linear",
symbol: "BTCUSDT",
});
// Batch place orders (spot/option only)
const batch = await client.order.placeBatch({
category: "spot",
request: [
{ category: "spot", symbol: "BTCUSDT", side: "Buy", orderType: "Limit", qty: "0.01", price: "49000" },
{ category: "spot", symbol: "BTCUSDT", side: "Buy", orderType: "Limit", qty: "0.01", price: "48000" },
],
});
// Batch amend / cancel
await client.order.amendBatch({ category: "spot", request: [/* ... */] });
await client.order.cancelBatch({ category: "spot", request: [/* ... */] });Position
// Get position list
const positions = await client.position.getList({
category: "linear",
symbol: "BTCUSDT",
});
// Set leverage
await client.position.setLeverage({
category: "linear",
symbol: "BTCUSDT",
buyLeverage: "10",
sellLeverage: "10",
});
// Switch margin mode (isolated/cross)
await client.position.switchMarginMode({
category: "linear",
symbol: "BTCUSDT",
tradeMode: "ISOLATED",
buyLeverage: "10",
sellLeverage: "10",
});
// Set risk limit
await client.position.setRiskLimit({
category: "linear",
symbol: "BTCUSDT",
riskId: 1,
});
// Set take profit / stop loss
await client.position.setTradingStop({
category: "linear",
symbol: "BTCUSDT",
takeProfit: "60000",
stopLoss: "45000",
positionIdx: 0,
});
// Switch position mode (one-way / hedge)
await client.position.switchPositionMode({
category: "linear",
symbol: "BTCUSDT",
mode: 0, // 0 = merged single, 3 = both side
});
// Get closed PnL
const pnl = await client.position.getClosedPnl({
category: "linear",
symbol: "BTCUSDT",
});Account
// Get wallet balance
const wallet = await client.account.getWalletBalance({
accountType: "UNIFIED",
coin: "USDT",
});
// Get account info
const info = await client.account.getAccountInfo();
// Get fee rate
const fees = await client.account.getFeeRate({
category: "linear",
symbol: "BTCUSDT",
});
// Get transaction log
const log = await client.account.getTransactionLog({
category: "linear",
limit: 50,
});
// Set margin mode
await client.account.setMarginMode({
setMarginMode: "REGULAR_MARGIN",
});Asset
// Get account coins balance
const balance = await client.asset.getAccountCoinsBalance({
accountType: "UNIFIED",
coin: "USDT",
});
// Internal transfer between accounts
const transfer = await client.asset.createInternalTransfer({
transferId: "uuid-here",
coin: "USDT",
amount: "100",
fromAccountType: "UNIFIED",
toAccountType: "FUND",
});
// Get transfer records
const records = await client.asset.getInternalTransferRecords();
// Get deposit records
const deposits = await client.asset.getDepositRecords({ coin: "USDT" });
// Get withdrawal records
const withdrawals = await client.asset.getWithdrawalRecords({ coin: "USDT" });
// Get coin info (chains, fees, limits)
const coins = await client.asset.getCoinInfo({ coin: "USDT" });Spot Leveraged Tokens
// Get leveraged token info
const tokens = await client.spotLeverToken.getInfo({ ltCoin: "BTC3L" });
// Get leveraged token market data
const market = await client.spotLeverToken.getMarket({ ltCoin: "BTC3L" });
// Purchase leveraged tokens
const purchase = await client.spotLeverToken.purchase({
ltCoin: "BTC3L",
ltAmount: "100",
});
// Redeem leveraged tokens
const redeem = await client.spotLeverToken.redeem({
ltCoin: "BTC3L",
ltAmount: "100",
});
// Get purchase/redeem records
const history = await client.spotLeverToken.getRecords({ ltCoin: "BTC3L" });Spot Margin Trade
// Get margin mode status
const status = await client.spotMarginTrade.getStatus();
// Toggle spot margin mode
await client.spotMarginTrade.toggle({ spotMarginMode: "1" });
// Set spot margin leverage
await client.spotMarginTrade.setLeverage({ leverage: "5" });
// Get VIP margin data
const vipData = await client.spotMarginTrade.getVipMarginData();
// Get borrow orders
const borrows = await client.spotMarginTrade.getBorrowOrders();WebSocket API Reference
WebSocket Options
// Public WebSocket — requires a category to select the stream
const pub = new BybitPublicWs({
category: "linear", // Required: "spot" | "linear" | "inverse" | "option"
testnet: false, // Use testnet (default: false)
wsUrl: "wss://...", // Custom WebSocket URL (overrides testnet/category)
pingIntervalMs: 20000, // Heartbeat interval (default: 20000)
maxReconnectAttempts: 10, // Max reconnect attempts, 0 = infinite (default: 10)
baseReconnectDelayMs: 1000, // Base delay for reconnect backoff (default: 1000)
logger: console, // Custom logger (default: silent)
});
// Private WebSocket — authentication required, no category needed
const priv = new BybitPrivateWs({
apiKey: "...", // Required
apiSecret: "...", // Required
testnet: false,
authExpirySec: 5, // Auth signature expiry (default: 5)
// ...same options as public (except category)
});Category-based URLs: Each category connects to a different WebSocket endpoint:
| Category | Mainnet URL | Testnet URL |
|----------|-------------|-------------|
| spot | wss://stream.bybit.com/v5/public/spot | wss://stream-testnet.bybit.com/v5/public/spot |
| linear | wss://stream.bybit.com/v5/public/linear | wss://stream-testnet.bybit.com/v5/public/linear |
| inverse | wss://stream.bybit.com/v5/public/inverse | wss://stream-testnet.bybit.com/v5/public/inverse |
| option | wss://stream.bybit.com/v5/public/option | wss://stream-testnet.bybit.com/v5/public/option |
| (private) | wss://stream.bybit.com/v5/private | wss://stream-testnet.bybit.com/v5/private |
To stream multiple categories, create multiple BybitPublicWs instances.
Public Topics
Subscribe using Bybit topic format: "topic.symbol" or "topic.depth.symbol" for orderbook.
| Event | Data Type | Description |
|-------|-----------|-------------|
| orderbook | WsOrderbook | Orderbook snapshot/delta (depth: 1, 50, 200, 500) |
| publicTrade | WsPublicTrade[] | Real-time trades |
| tickers | WsTicker | Ticker snapshot updates |
| kline | WsKline[] | Candlestick data |
| liquidation | WsLiquidation | Liquidation orders |
All public data events receive (type: WsMessageType, data: T, topic: string) where WsMessageType is "snapshot" | "delta".
snapshot— Full data snapshot (initial load, periodic refresh)delta— Incremental update (changes only)
The topic string is the full subscription topic (e.g., "orderbook.50.BTCUSDT") so you can identify the symbol, depth, or interval.
ws.subscribe([
"publicTrade.BTCUSDT",
"orderbook.50.BTCUSDT",
"tickers.BTCUSDT",
"kline.1.BTCUSDT",
"liquidation.BTCUSDT",
]);Private Topics
Private topics do not require a symbol — they stream all data for your account.
| Event | Data Type | Description |
|-------|-----------|-------------|
| order | WsPrivateOrder[] | Order status updates |
| execution | WsPrivateExecution[] | Trade executions / fills |
| position | WsPrivatePosition[] | Position updates |
| wallet | WsPrivateWallet[] | Wallet balance changes |
| greeks | WsPrivateGreeks[] | Option greeks updates |
Private events receive (data: T[]) directly — there is no type field for private WebSocket messages.
ws.on("authenticated", () => {
ws.subscribe(["order", "position", "execution", "wallet"]);
});Lifecycle Events
Both public and private WebSocket clients emit lifecycle events:
| Event | Args | Description |
|-------|------|-------------|
| open | — | Connection established (and authenticated for private) |
| close | (code, reason) | Connection closed |
| reconnecting | (attempt) | Reconnection attempt |
| error | (error) | Connection or auth error |
| authenticated | — | (Private only) Authentication successful |
ws.on("open", () => console.log("Connected"));
ws.on("close", (code, reason) => console.log(`Disconnected: ${code}`));
ws.on("reconnecting", (attempt) => console.log(`Reconnecting (attempt ${attempt})`));
ws.on("error", (err) => console.error("WS error:", err));Connection Management
ws.connect(); // Establish connection
ws.disconnect(); // Gracefully disconnect (no auto-reconnect)
ws.subscribe(["publicTrade.BTCUSDT"]); // Subscribe to topics
ws.unsubscribe(["publicTrade.BTCUSDT"]); // Unsubscribe from topics
ws.state; // "connecting" | "connected" | "reconnecting" | "closed"
ws.activeSubscriptions; // string[] of current subscriptionsUtilities
Delta Parser
A generic, zero-dependency delta parser that maintains local state from Bybit WebSocket delta messages (snapshot/delta). Works with any WebSocket data type.
import { DeltaParser } from "@plutarc/bybit";
import type { WsPublicTrade } from "@plutarc/bybit";
// Create a parser for trade data — keys identify unique rows
const tradeParser = new DeltaParser<WsPublicTrade>({
keys: ["i"],
maxItems: 1000, // keep only the most recent 1000 trades
});
ws.on("publicTrade", (type, data) => {
const snapshot = tradeParser.update(type, data);
console.log(`${snapshot.length} trades in store`);
});For data where items can be removed (like orderbook levels), use the isRemoved callback:
interface Level {
price: string;
size: string;
}
const parser = new DeltaParser<Level>({
keys: ["price"],
isRemoved: (item) => item.size === "0", // Bybit convention: size "0" = remove
});API:
| Method / Property | Returns | Description |
|---|---|---|
| update(type, data) | readonly T[] | Process a delta message, returns full current state |
| snapshot | readonly T[] | Current state without processing a new message |
| length | number | Number of items in the store |
| clear() | void | Reset the store to empty |
OrderBook Store
A higher-level convenience store for Bybit L2 orderbook data. Uses price-keyed Maps for O(1) updates on Bybit's [price, size] array format. Maintains sorted bids and asks with common orderbook queries.
import { OrderBookStore } from "@plutarc/bybit";
const book = new OrderBookStore();
// Plug directly into a WebSocket event handler
ws.on("orderbook", (type, data) => book.update(type, data));
// Query the book
console.log(book.bestBid()); // 50000
console.log(book.bestAsk()); // 50001
console.log(book.spread()); // 1
console.log(book.midPrice()); // 50000.5
// Get top 5 levels from each side
const { bids, asks } = book.depth(5);
// Volume at top of book
console.log(book.bidVolume(10)); // total bid size in top 10 levels
console.log(book.askVolume()); // total ask size across all levels
// Sorted level arrays
book.bids; // OrderBookLevel[] sorted desc by price (best bid first)
book.asks; // OrderBookLevel[] sorted asc by price (best ask first)Composability — use as much or as little as you need:
import { DeltaParser, OrderBookStore } from "@plutarc/bybit";
// 1. Batteries included — OrderBookStore handles snapshot/delta directly
const book = new OrderBookStore();
ws.on("orderbook", (type, data) => book.update(type, data));
// 2. DeltaParser only — feed into your own store (zustand, redux, etc.)
ws.on("orderbook", (type, data) => {
// handle raw { b: [[price, size], ...], a: [[price, size], ...] }
useMyStore.setState({ orderbook: data });
});
// 3. Neither — handle raw deltas yourself
ws.on("orderbook", (type, data, topic) => { /* your own logic */ });API:
| Method / Property | Returns | Description |
|---|---|---|
| update(type, data) | void | Process a snapshot/delta message (auto-bound arrow function) |
| bids | readonly OrderBookLevel[] | All bids sorted descending by price |
| asks | readonly OrderBookLevel[] | All asks sorted ascending by price |
| bestBid() | number \| undefined | Highest bid price |
| bestAsk() | number \| undefined | Lowest ask price |
| spread() | number \| undefined | Best ask minus best bid |
| midPrice() | number \| undefined | Average of best bid and best ask |
| depth(n) | { bids, asks } | Top N levels from each side |
| bidVolume(n?) | number | Total bid size (optionally top N levels only) |
| askVolume(n?) | number | Total ask size (optionally top N levels only) |
| clear() | void | Reset the book to empty |
Authentication Helpers
For advanced usage:
import { sign, getSignedHeaders, signWs } from "@plutarc/bybit";
// REST API signature
const sig = sign(apiSecret, timestamp, apiKey, recvWindow, payload);
// Full signed headers (auto-generates timestamp)
const headers = getSignedHeaders(apiKey, apiSecret, recvWindow, payload);
// WebSocket auth signature
const wsSig = signWs(apiSecret, expires);Error Handling
All REST errors throw typed error classes. Bybit wraps all responses in { retCode, retMsg, result } — the client automatically unwraps successful responses and throws on non-zero retCode, even when the HTTP status is 200.
import { BybitApiError, BybitRateLimitError } from "@plutarc/bybit";
try {
await client.order.place({ /* ... */ });
} catch (err) {
if (err instanceof BybitRateLimitError) {
console.log(`Rate limited — retry after ${err.retryAfterMs}ms`);
} else if (err instanceof BybitApiError) {
console.log(`API error ${err.status} (retCode=${err.retCode}): ${err.errorMessage}`);
console.log(`Retryable: ${err.retryable}`);
}
}Retryable errors (automatically retried up to maxRetries times):
429Too Many Requests5xxServer errors- Network/timeout errors
Non-retryable errors (thrown immediately):
400Bad Request401Unauthorized403Forbidden404Not Found- Non-zero
retCodewith HTTP 200 (business logic errors)
Rate Limiting
The client self-throttles to stay within Bybit rate limits:
- Default: 600 requests per 5-second window (Bybit IP-based limit)
- Tracks the
X-Bapi-Limit-Statusresponse header from Bybit - Pauses requests when approaching the limit (5-request buffer)
- Configurable via
rateLimitPer5Secondsoption
Note: Bybit also enforces per-UID per-second limits on specific endpoints (e.g., 20 req/s for order creation). These are endpoint-specific and not tracked by the client-side rate limiter.
Testnet
All clients support testnet with a single flag:
// REST
const client = new BybitClient({ testnet: true });
// → https://api-testnet.bybit.com
// Public WebSocket
const ws = new BybitPublicWs({ category: "linear", testnet: true });
// → wss://stream-testnet.bybit.com/v5/public/linear
// Private WebSocket
const priv = new BybitPrivateWs({ apiKey: "...", apiSecret: "...", testnet: true });
// → wss://stream-testnet.bybit.com/v5/privateGet testnet API keys at testnet.bybit.com.
Custom Logger
Pass any object implementing { debug, info, warn, error } methods. By default the client is silent.
// Use console
const client = new BybitClient({ logger: console });
// Use pino
import pino from "pino";
const client = new BybitClient({ logger: pino({ name: "bybit" }) });
// Use winston
import winston from "winston";
const client = new BybitClient({ logger: winston.createLogger({ /* ... */ }) });Development
# Install dependencies
bun install
# Type check
bun run typecheck
# Run tests
bun test
# Build
bun run build
# Lint & format
bun run lint
bun run formatResources
- Bybit V5 API Documentation — API overview and guides
- Bybit API Integration Guide — Authentication, rate limits, and integration guidance
- Bybit WebSocket API — WebSocket documentation
- Bybit Testnet — Testnet for development
- Bybit API Changelog — API changes and updates
