mishasaz / bybit-php
A clean, dependency-free PHP client for the Bybit v5 REST API (HMAC signing, typed endpoints, pluggable transport).
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A clean, dependency-free PHP client for the Bybit v5 REST API.
- HMAC-SHA256 request signing done for you
- Typed methods for the common linear (USDT-perpetual) endpoints
- Zero required dependencies - ships with a PHP-streams transport; swap in Guzzle / any PSR-18 client via one interface
- Returns Bybit's raw response envelope (
retCode/retMsg/result) - nothing hidden, one branch to check - Immutable, injectable config - no globals, no environment reads, fully testable
Requires PHP 8.1+.
Install
composer require mishasaz/bybit-php
Quick start
use Mishasaz\Bybit\BybitClient; use Mishasaz\Bybit\Config; $client = new BybitClient(new Config( apiKey: 'YOUR_KEY', apiSecret: 'YOUR_SECRET', testnet: false, )); $ticker = $client->getTicker('BTCUSDT'); echo $ticker['result']['list'][0]['lastPrice']; $order = $client->placeOrder([ 'category' => 'linear', 'symbol' => 'BTCUSDT', 'side' => 'Buy', 'orderType' => 'Limit', 'qty' => '0.001', 'price' => '50000', ]); if (($order['retCode'] ?? -1) === 0) { echo 'placed: ' . $order['result']['orderId']; } else { echo 'error: ' . $order['retMsg']; }
What's covered
Account: getBalance, getPositions, getClosedPnl, getFeeRate, setLeverage, setTradingStop($symbol, $takeProfit = null, $stopLoss = null, $tpOrderType = null, $tpLimitPrice = null) - pass 'Limit' with a limit price to have the take-profit rest as a limit order instead of firing at market:
// Market (Bybit's default): closes at whatever is there when the price is touched. $client->setTradingStop('BTCUSDT', takeProfit: '52000'); // Limit: rests at the price and fills as maker. $client->setTradingStop('BTCUSDT', takeProfit: '52000', tpOrderType: 'Limit', tpLimitPrice: '52000');
The difference is the fee side: a market exit pays taker plus whatever the book gives you, a resting limit pays maker. It matters most when the target sits close to the entry, where that spread is a large share of the trade.
Orders: placeOrder, placeReduceOrder, getOpenOrders (auto-paginated), getOrderHistory, cancelOrder, cancelAllOrders, closePosition($symbol, $cancelOrders = true) - pass false to close the position without sweeping the symbol's resting orders (useful when the caller cancels its own order ids selectively).
Market data: getTicker, getKline, getRecentTrades, getInstrumentInfo.
History: getExecutions($params) returns raw fills - execPrice, execQty, execFee, feeRate, isMaker, markPrice - which is what you need to measure what execution actually cost rather than what it was modelled to cost. It returns ONE page per call on purpose: Bybit caps the window at seven days and the page at 100 records, and returns nextPageCursor in the result, so paginating inside the client would hide both limits from you.
$res = $client->getExecutions(['startTime' => $from, 'endTime' => $to]); $cursor = $res['result']['nextPageCursor'] ?? '';
Anything not wrapped is one line away via the transport:
$res = $client->transport()->get('/v5/market/tickers', ['category' => 'spot', 'symbol' => 'BTCUSDT']);
Response format
Every method returns the decoded Bybit envelope as an array:
['retCode' => 0, 'retMsg' => 'OK', 'result' => [ ... ]]
retCode === 0 means success. A transport-level failure (timeout, DNS, invalid
JSON) is reported the same way with retCode = -1, so you always check one place:
if (($res['retCode'] ?? -1) !== 0) { // handle $res['retMsg'] }
Custom transport
The client talks to Bybit through a single Transport interface. The bundled
StreamTransport uses PHP streams and needs nothing extra. To use your own HTTP
stack (pooling, proxies, retries, PSR-18), implement the interface and inject it:
use Mishasaz\Bybit\{BybitClient, Config, Transport}; final class GuzzleTransport implements Transport { public function get(string $endpoint, array $params = []): array { /* ... */ } public function post(string $endpoint, array $params = []): array { /* ... */ } public function isConfigured(): bool { /* ... */ } } $client = new BybitClient($config, new GuzzleTransport(/* ... */));
Testnet
$client = new BybitClient(new Config($key, $secret, testnet: true));
Or point at any base URL / tune the request window and timeout:
new Config($key, $secret, timeoutSeconds: 15, recvWindowMs: 8000, baseUrl: 'https://api.bybit.com');
Disclaimer
Not affiliated with Bybit. Trading is risky; use at your own risk. This library mirrors the API faithfully and does not add strategy or risk-management logic.
Donate
If this library was useful and saved you a bit of precious time, a tip is welcome.
USDT (BEP-20): 0x7e5db543734E5BD59E0df1B27820A1EAF2BE438B
License
MIT. Do whatever you want with it.