bybit-exchange / bybit-connector-php
Official Bybit V5 REST API connector for PHP — typed method signatures, HMAC-SHA256 signing, typed exception hierarchy, Guzzle-based transport.
Package info
github.com/bybit-exchange/bybit.php.api
pkg:composer/bybit-exchange/bybit-connector-php
Requires
- php: >=8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.5
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.50
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-07-27 06:46:14 UTC
README
Official lightweight PHP connector for the Bybit V5 REST API.
bybit-connector-php wraps the Bybit V5 HTTP endpoints as a set of typed PHP methods with explicit required arguments plus an array $options = [] catch-all for optional parameters. Its goal is the same as pybit on the Python side and bybit-connector-ruby on the Ruby side: an easy-to-use, high-performance connector with a small dependency footprint.
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 the
testnet: true|false constructor argument on Configuration (or an explicit
baseUrl: override).
Installation
PHP >= 8.1 is required (PHP 8.3.x recommended). Composer 2.x.
composer require bybit-exchange/bybit-connector-php
Quick Start
<?php require __DIR__ . '/vendor/autoload.php'; use Bybit\Client; use Bybit\Configuration; // All calls below run against testnet — flip testnet: false for mainnet // once you're happy with the behavior. $config = new Configuration( apiKey: getenv('BYBIT_TESTNET_KEY') ?: null, apiSecret: getenv('BYBIT_TESTNET_SECRET') ?: null, testnet: true, ); $client = new Client($config); // 1. Public endpoint — no auth needed. print_r($client->market->getServerTime()); // 2. Signed endpoint — apiKey + apiSecret required. $wallet = $client->account->getWalletBalance('UNIFIED'); print_r($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->createOrder( 'linear', 'BTCUSDT', 'Buy', 'Limit', '0.01', ['price' => '10000', 'timeInForce' => 'GTC'] ); $orderId = $order['result']['orderId']; echo "orderId: {$orderId}" . PHP_EOL; // 4. Cancel it before moving on. $client->trade->cancelOrder('linear', 'BTCUSDT', ['orderId' => $orderId]);
⚠️ Before switching
testnet = false: verify theprice/qtyincreateOrderwon'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.php for a runnable script.
Configuration
All options live on Bybit\Configuration. Instantiate with named arguments
and pass to Client:
use Bybit\Configuration; $config = new Configuration( apiKey: getenv('BYBIT_TESTNET_KEY') ?: null, apiSecret: getenv('BYBIT_TESTNET_SECRET') ?: null, recvWindow: '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. testnet: true, // false selects mainnet (default) timeout: 10, // Guzzle timeout, seconds );
testnet, baseUrl, timeout, and httpClient are readonly — set them
via the constructor and Configuration is a snapshot the Session captures at
Client construction. apiKey, apiSecret, and recvWindow remain writable
on the object so callers can rotate credentials mid-run without rebuilding
the Client.
Bring your own Guzzle client to inject retries / logging / middleware:
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; $stack = HandlerStack::create(); $stack->push(Middleware::retry( fn($retries, $req, $resp, $err) => $retries < 3 && ($err !== null || ($resp && $resp->getStatusCode() >= 500)), fn($retries) => (int) pow(2, $retries) * 1000 // 1s, 2s, 4s (ms) )); $config = new Configuration( apiKey: getenv('BYBIT_TESTNET_KEY') ?: null, apiSecret: getenv('BYBIT_TESTNET_SECRET') ?: null, httpClient: new GuzzleClient([ 'base_uri' => 'https://api-testnet.bybit.com', 'handler' => $stack, 'timeout' => 5, ]), );
⚠️ When you inject
httpClient, the SDK does not re-applyConfiguration::$baseUrl/$testnet/$timeoutonto your client — the injected Guzzle instance is used as-is. Setbase_uriandtimeouton the Guzzle client yourself (as shown above), and pickapi.bybit.comvsapi-testnet.bybit.comexplicitly.
Base URLs (exported constants):
Bybit\Bybit::BASE_URL_MAINNET—https://api.bybit.comBybit\Bybit::BASE_URL_TESTNET—https://api-testnet.bybit.com
Services
Each API group is a readonly 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-position$client->account— wallet, margin, collateral, fee-rate, transaction log$client->asset— coin balance, funding history$client->user— sub-accounts, API-key management$client->affiliate— sub-affiliate lists$client->broker— broker earnings, distributions$client->cryptoLoan— flexible / fixed crypto loans$client->rfq— request-for-quote (block trades)$client->spotMargin— UTA spot margin$client->earn— earn, liquidity mining, RWA, PWM, hold-to-earn$client->p2p— P2P advertise / order / chat$client->bot— DCA / grid / futures-combo / futures-grid / martingale
Error Handling
Every failure is a subclass of Bybit\Exception\BybitException:
use Bybit\Exception\{ AuthException, RateLimitException, TimeoutException, NetworkException, ServerException, ClientException, ParseException, ApiException, }; try { $client->trade->createOrder('linear', 'BTCUSDT', 'Buy', 'Limit', '0.01', ['price' => '10000']); } catch (AuthException $e) { // retCode 10002/10003/10004/10005/10007/10009/10010/10029, or HTTP 401/403 // bad key / bad sign / permission } catch (RateLimitException $e) { // retCode 10006/10018, or HTTP 429 sleep(1); } catch (TimeoutException $e) { // Guzzle ConnectException / RequestException(timeout) } catch (NetworkException $e) { // Guzzle ConnectException (connection refused / DNS / SSL) } catch (ServerException $e) { // HTTP 5xx w/ non-JSON body } catch (ClientException $e) { // non-auth 4xx w/ non-JSON body (WAF, CDN, etc.) } catch (ParseException $e) { // unrecognized body shape; $e->getBody() holds raw payload } catch (ApiException $e) { // any other retCode != 0 — catch-all API error }
Full hierarchy:
Bybit\Exception\BybitException(\RuntimeException)Bybit\Exception\ConfigurationException— missing apiKey / conflicting optionsBybit\Exception\TransportExceptionBybit\Exception\TimeoutExceptionBybit\Exception\NetworkExceptionBybit\Exception\ServerException(5xx w/o body)Bybit\Exception\ClientException(non-auth 4xx w/o body)Bybit\Exception\ParseException— body did not parse or shape mismatch (hasgetBody(),getHttpStatus())
Bybit\Exception\ApiException— Bybit V5 body with retCode != 0Bybit\Exception\AuthExceptionBybit\Exception\RateLimitException
Every ApiException exposes getRetCode(), getRetMsg(), getResult(), getTime(), getHttpStatus(). See the Bybit V5 error-code list for meanings.
The
sleep(1)on rate-limit shown above is fine for exploration, not for production — Bybit will escalate throttling on tight retry loops. For real workloads, wire a GuzzleMiddleware::retrywith exponential backoff via the "bring your own Guzzle client" hook in Configuration above.
Return Value
Every service method returns the raw parsed JSON as an associative array:
$response = $client->market->getKline('spot', 'BTCUSDT', '1'); $response['retCode']; // => 0 $response['retMsg']; // => 'OK' $response['result']; // => ['category' => 'spot', 'symbol' => 'BTCUSDT', 'list' => [...]] $response['time']; // => 1234567890000
P2P endpoints return a legacy snake_case envelope (
ret_code/ret_msg/time_now/ext_info) instead of the V5 standard. The SDK normalizes these intoretCode/retMsg/time/retExtInfoautomatically —$response['retCode']works uniformly across every service. The original snake_case keys are preserved on the array for callers that want the wire shape verbatim.
Method Reference
Method names follow the Bybit V5 endpoint slug in camelCase, grouped by
domain. Required path/body params are explicit typed arguments; every optional
parameter goes into the final array $options = []. A few illustrative
mappings:
| Bybit V5 path | HTTP | SDK method |
|---|---|---|
/v5/market/kline |
GET | $client->market->getKline($category, $symbol, $interval, $options) |
/v5/market/tickers |
GET | $client->market->getTickers($category, $options) |
/v5/order/create |
POST | $client->trade->createOrder($category, $symbol, $side, $orderType, $qty, $options) |
/v5/order/cancel |
POST | $client->trade->cancelOrder($category, $symbol, $options) |
/v5/position/list |
GET | $client->position->getInfo($category, $options) |
/v5/account/wallet-balance |
GET | $client->account->getWalletBalance($accountType, $options) |
For the full list of 240+ endpoints, browse the service class source under
src/RestApi/ — every method carries a PHPDoc block naming its HTTP verb,
path, params, and a @see link to the Bybit docs page.
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 option on the next call:
$cursor = null; do { $opts = ['limit' => 50]; if ($cursor !== null && $cursor !== '') { $opts['cursor'] = $cursor; } $resp = $client->trade->getOrderHistory('linear', $opts); foreach ($resp['result']['list'] as $order) { // process($order); } $cursor = $resp['result']['nextPageCursor'] ?? null; } while ($cursor !== null && $cursor !== '');
The same pattern works for getClosedPnl, getTransactionLog,
getExecutionList, and every other paginated endpoint.
Signing Invariant (for the curious)
Session guarantees that the query string signed matches the query string
sent on the wire byte-for-byte. It does this by:
- Sorting
paramskeys and encoding viarawurlencodeinto one canonical string (sortAndEncode()). - Feeding that exact string into
Authentication::signV5(). - Building the request URL manually as
$path . '?' . $queryStrrather than letting Guzzle re-serializeparams— Guzzle's default query handler can reorder keys and array-bracket lists differently, which would break the HMAC.
Booleans are wire-serialized as literal 'true' / 'false', floats are
formatted non-scientifically (so 1.0e-9 becomes 0.000000001) — both
avoid Bybit's parameter validators rejecting the PHP default casts.
Development
composer install
composer test # PHPUnit
composer stan # PHPStan level 6
composer cs-check # PHP-CS-Fixer dry-run
composer cs-fix # PHP-CS-Fixer auto-fix
License
MIT — see LICENSE.