bybit-connector-ruby
Official lightweight Ruby connector for the Bybit V5 REST API.
bybit-connector-ruby wraps the Bybit V5 HTTP endpoints as a set of typed Ruby methods with keyword-arg signatures.
Prerequisites
Before you write any code you need a Bybit API key. Two accounts to know about:
- Testnet — testnet.bybit.com → sign up → API Management → Create New Key. This is where you should point every new integration until you're confident about behavior. Testnet balances are virtual; nothing you do here touches real funds.
- Mainnet — bybit.com → API Management. Real money. Enable only the permissions you actually need (spot / derivatives / withdrawals) and prefer IP-restricted keys.
Testnet and mainnet keys are separate — an API key issued on one won't
work against the other. The SDK selects the environment via
testnet: true|false (or an explicit base_url: override).
Installation
Ruby ≥ 3.0 is required (Ruby 3.3.x recommended).
gem install bybit-connector-ruby
Or in your Gemfile:
gem 'bybit-connector-ruby'
Quick Start
require 'bybit'
# All calls below run against testnet — swap testnet: false for mainnet
# once you're happy with the behavior.
Bybit.configure do |c|
c.api_key = ENV['BYBIT_TESTNET_KEY']
c.api_secret = ENV['BYBIT_TESTNET_SECRET']
c.testnet = true
end
client = Bybit::Client.new
# 1. Public endpoint — no auth needed.
puts client.market.get_server_time
# 2. Signed endpoint — api_key + api_secret required.
wallet = client.account.get_wallet_balance(account_type: 'UNIFIED')
puts wallet['result']['list']
# 3. Place a LIMIT order well below market so it sits on the book and does
# NOT fill (safe to run repeatedly). Adjust `price:` if BTC ever trades
# at $10k again — otherwise this stays a resting order you can cancel.
order = client.trade.create_order(
category: 'linear', symbol: 'BTCUSDT',
side: 'Buy', order_type: 'Limit', qty: '0.01',
price: '10000', time_in_force: 'GTC'
)
order_id = order['result']['orderId']
puts "orderId: #{order_id}"
# 4. Cancel it before moving on.
client.trade.cancel_order(category: 'linear', symbol: 'BTCUSDT', order_id: order_id)
⚠️ Before switching
testnet: false: verify the price / qty increate_orderwon't cross the top of the book — a Limit Buy at $10k on mainnet becomes a market fill instantly (if BTC ever drops that low), and a Limit Sell at $1M does the reverse.
See examples/quickstart.rb for a runnable script.
Configuration
All options live on Bybit::Configuration. Either configure globally with the block:
Bybit.configure do |c|
c.api_key = ENV['BYBIT_KEY']
c.api_secret = ENV['BYBIT_SECRET']
c.testnet = false # default false
c.recv_window = '5000' # milliseconds — Bybit rejects requests
# whose signed timestamp is older than
# this window. Bump to 10000+ if your
# clock drifts or the network is noisy.
c.timeout = 10 # Faraday timeout, seconds
end
Or pass overrides to Client.new — per-client overrides win over
Bybit.configure, so you can share defaults globally and still spin up a
dedicated client with a different key or environment:
client = Bybit::Client.new(api_key: '...', api_secret: '...', testnet: true)
Bring your own Faraday connection to inject retries / logging / adapters:
conn = Faraday.new('https://api-testnet.bybit.com') do |f|
f.request :retry, max: 3
f.response :logger
end
Bybit.configure { |c| c.faraday_connection = conn }
Base URLs (exported constants):
Bybit::BASE_URL_MAINNET—https://api.bybit.comBybit::BASE_URL_TESTNET—https://api-testnet.bybit.com
Services
Each API group is a property on Bybit::Client:
client.market— public market data (kline, tickers, orderbook, instruments-info, ...)client.trade— orders (create / amend / cancel / batch / history)client.position— positions, leverage, TP/SL, move-positionclient.account— wallet, margin, collateral, fee-rate, transaction logclient.asset— coin balance, coin greeks, funding historyclient.user— sub-accounts, API-key managementclient.affiliate— sub-affiliate listsclient.broker— broker earnings, distributionsclient.crypto_loan— flexible / fixed crypto loansclient.institutional_loan— /v5/ins-loan/* (OTC institutional loans)client.rfq— request-for-quote (block trades)client.spot_margin— UTA spot marginclient.earn— earn, liquidity mining, RWA, PWM, hold-to-earnclient.p2p— P2P advertise / order / chatclient.bot— DCA / grid / futures-combo / futures-grid / martingaleclient.pre_upgrade— /v5/pre-upgrade/* historical queries for Classic → UTA upgradesclient.misc— /v5/announcements/index, /v5/system/status
Error Handling
Every failure is a subclass of Bybit::Error:
begin
client.trade.create_order(category: 'linear', symbol: 'BTCUSDT', side: 'Buy', ...)
rescue Bybit::AuthError => e # retCode 10003/10004/10005/... or HTTP 401/403
# bad key / bad sign / permission
rescue Bybit::RateLimitError => e # retCode 10006 / 10018 or HTTP 429
sleep 1 && retry
rescue Bybit::TimeoutError => e # Faraday::TimeoutError
rescue Bybit::NetworkError => e # Faraday::ConnectionFailed / SSLError
rescue Bybit::ServerError => e # HTTP 5xx w/ non-JSON body
rescue Bybit::ClientError => e # HTTP 4xx w/ non-JSON body (WAF, CDN, etc.)
rescue Bybit::ParseError => e # unrecognized body shape; e.body holds raw payload
rescue Bybit::ApiError => e # any other retCode != 0 — catch-all API error
end
Full hierarchy:
-
Bybit::Error(StandardError)Bybit::ConfigurationError— missing api_key / conflicting options-
Bybit::TransportErrorBybit::TimeoutErrorBybit::NetworkErrorBybit::ServerError(5xx w/o body)Bybit::ClientError(non-auth 4xx w/o body)Bybit::ParseError— body did not parse or shape mismatch (has#body,#http_status)
-
Bybit::ApiError— Bybit V5 body with retCode != 0Bybit::AuthErrorBybit::RateLimitError
Every ApiError exposes #ret_code, #ret_msg, #result, #time, #http_status. See the Bybit V5 error-code list for meanings.
The
sleep 1 && retryshown above is fine for exploration, not for production — Bybit will escalate throttling on tight retry loops. For real workloads, wire aFaraday::Retrymiddleware with exponential backoff via the "bring your own Faraday connection" hook in Configuration above.
Return Value
Every service method returns the raw parsed JSON as a Hash:
response = client.market.get_kline(category: 'spot', symbol: 'BTCUSDT', interval: '1')
response['retCode'] # => 0
response['retMsg'] # => 'OK'
response['result'] # => { 'category' => 'spot', 'symbol' => 'BTCUSDT', 'list' => [...] }
response['time'] # => 1234567890000
Keys are strings, not symbols —
response[:result]isnil. This is a common first-hour gotcha; pass throughJSON.parsesemantics rather than converting.
Method Reference
Method names follow the Bybit V5 endpoint slug in snake_case, grouped by domain. A few illustrative mappings:
| Bybit V5 path | SDK method |
|---|---|
GET /v5/market/kline |
client.market.get_kline |
GET /v5/market/tickers |
client.market.get_tickers |
POST /v5/order/create |
client.trade.create_order |
POST /v5/order/cancel |
client.trade.cancel_order |
GET /v5/position/list |
client.position.get_positions |
GET /v5/account/wallet-balance |
client.account.get_wallet_balance |
For the full list, generate YARD docs locally:
bundle exec yard doc
open doc/index.html
or browse the published copy on rubydoc.info/gems/bybit-connector-ruby.
Pagination
Bybit V5 uses opaque cursor pagination — the response's
result['nextPageCursor'] (empty string when the page is the last one)
feeds back in as the cursor: argument on the next call:
cursor = nil
loop do
resp = client.trade.get_order_history(category: 'linear', limit: 50, cursor: cursor)
resp['result']['list'].each { |order| process(order) }
cursor = resp['result']['nextPageCursor']
break if cursor.nil? || cursor.empty?
end
The same pattern works for get_positions, get_execution_history,
get_transaction_log, and every other paginated endpoint.
WebSockets
Hosts (mirror the REST split):
- Mainnet —
wss://stream.bybit.com/v5/{public/<category>|private|trade} - Testnet —
wss://stream-testnet.bybit.com/v5/{public/<category>|private|trade}
WebSocket support ships behind an opt-in require so REST-only consumers don't
load the underlying websocket-client-simple dependency at boot. Four
callbacks are available; all are optional but you'll almost always want at
least on_message.
require 'bybit/websocket'
# Public stream — no auth needed.
public_ws = Bybit::WebSocket::Client.new(
channel: :linear, # :spot / :linear / :inverse / :option
testnet: true,
on_open: -> { puts 'connected' },
on_message: ->(msg) { puts msg.inspect }, # your handler
on_close: ->(_ev) { puts 'closed' },
on_error: ->(err) { warn "ws error: #{err}" }
).connect
public_ws.subscribe('tickers.BTCUSDT', 'orderbook.1.BTCUSDT')
# Private stream — HMAC-SHA256 auth over "GET/realtime" + expiry.
private_ws = Bybit::WebSocket::Client.new(
channel: :private,
testnet: true,
api_key: ENV['BYBIT_TESTNET_KEY'],
api_secret: ENV['BYBIT_TESTNET_SECRET'],
on_message: ->(msg) { puts msg.inspect } # your handler
).connect
# Subscribe returns immediately; the actual `op:subscribe` frame is deferred
# until the private-stream auth reply lands, so you can call subscribe
# anytime — before or after `#connect`.
private_ws.subscribe('position', 'order', 'wallet')
# Later:
private_ws.unsubscribe('wallet')
private_ws.disconnect
Supported channels: :spot, :linear, :inverse, :option, :private,
:trade. testnet: true swaps in the stream-testnet.bybit.com host.
Pings ship every 20 seconds — Bybit closes idle sockets after ~20s of
silence. There is no automatic reconnect on network drop: callers listening
on on_close can invoke #connect again to reopen the socket and replay
the buffered #subscriptions.
Development
bundle install
bundle exec rspec # tests
bundle exec rubocop # lint
bundle exec rake # both
License
MIT — see LICENSE.