tigusigalpa / zoomex-php
PHP client for Zoomex V3 REST API and V3/V5 WebSocket endpoints: market data, trading, positions, and account streams. HMAC-SHA256 authentication with testnet/mainnet support.
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
- ratchet/pawl: ^0.4
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Also available: Zoomex Go Client
An approachable PHP/Laravel SDK for Zoomex. It wraps the V3 REST API and V3/V5 WebSocket endpoints with typed request objects, native PHP enums, and Laravel-friendly configuration.
Start with read-only market data, switch to testnet when you are ready to try trading, and use the same client in a plain PHP process, Laravel job, command, or HTTP controller. Every REST method returns the API's result payload as a PHP array; API and transport failures become dedicated exceptions.
Need an endpoint or field that is not covered below? See the project Wiki and the official API documentation linked at the end of this file.
Why use this package?
- Typed DTOs keep request fields explicit and remove magic arrays from most calls.
- Native enums for market category, order side, order type, interval, account type, and more.
- HMAC-SHA256 signing for private REST endpoints.
- Laravel auto-discovery, configuration publishing, facade, and container binding.
- One configuration shape for mainnet, testnet, custom timeouts, and receive window.
- V5 public WebSocket channels for linear, inverse, and spot data; authenticated V3 private streams.
- PHP 8.1+ and strict types.
A note on scope. REST support is focused on the V3 market, trade, position, and account endpoints. Asset methods are retained for source compatibility, but they use legacy API conventions; see the warning in the API reference before relying on them in a new application.
Requirements
- PHP 8.1+
- Composer
- Laravel 10/11/12/13 (optional)
Installation
composer require tigusigalpa/zoomex-php
Quick start: read a ticker
Market-data calls do not need API credentials. This is a good first request to confirm that your environment and network can reach Zoomex.
<?php use Tigusigalpa\Zoomex\Client; use Tigusigalpa\Zoomex\DTO\Market\GetTickersRequest; use Tigusigalpa\Zoomex\Enums\Category; $zoomex = new Client(); $ticker = $zoomex->market()->getTickers(new GetTickersRequest( category: Category::LINEAR, symbol: 'BTCUSDT', )); print_r($ticker);
Every successful REST call returns only the result section from the Zoomex response. The exact fields are defined by Zoomex and can vary between endpoints, so treat the returned value as an API payload rather than a package-specific model.
Configuration
Laravel
Laravel discovers the package automatically. Publish the configuration once if you want a local config file:
php artisan vendor:publish --tag=zoomex-config
Add the credentials and connection settings to .env. Start with testnet while developing a strategy or order flow.
ZOOMEX_API_KEY=your_api_key ZOOMEX_SECRET_KEY=your_secret_key ZOOMEX_TESTNET=true ZOOMEX_RECV_WINDOW=5000 ZOOMEX_TIMEOUT=30 ZOOMEX_CONNECT_TIMEOUT=10
Set ZOOMEX_TESTNET=false only when you intentionally want to send requests to mainnet. Never commit a real key or secret to the repository.
Use the facade in controllers, commands, jobs, and anywhere Laravel is bootstrapped:
use Tigusigalpa\Zoomex\DTO\Market\GetTickersRequest; use Tigusigalpa\Zoomex\Enums\Category; use Tigusigalpa\Zoomex\Facades\Zoomex; $ticker = Zoomex::market()->getTickers(new GetTickersRequest( category: Category::SPOT, symbol: 'BTCUSDT', ));
Or inject the client contract when you prefer explicit dependencies:
use Tigusigalpa\Zoomex\Contracts\ClientInterface; use Tigusigalpa\Zoomex\DTO\Position\GetPositionInfoRequest; use Tigusigalpa\Zoomex\Enums\Category; final class PositionSnapshot { public function __construct(private ClientInterface $zoomex) { } public function forBtcUsdt(): array { return $this->zoomex->position()->getPositionInfo( new GetPositionInfoRequest( category: Category::LINEAR, symbol: 'BTCUSDT', ), ); } }
Standalone PHP
<?php use Tigusigalpa\Zoomex\Client; $client = new Client([ 'api_key' => 'your_api_key', 'secret_key' => 'your_secret_key', 'testnet' => true, 'recv_window' => 5000, 'timeout' => 30, 'connect_timeout' => 10, ]); // The same setup in fluent style: $client = Client::make() ->withApiKey('your_api_key') ->withSecretKey('your_secret_key') ->withTestnet(true) ->withRecvWindow(5000);
For a custom HTTP transport—for example, to add corporate proxy settings—pass an already configured GuzzleHttp\Client as http_client. You can also override base_url in tests or in a controlled proxy environment.
| Configuration key | Default | Purpose |
|---|---|---|
testnet |
false |
Chooses the testnet REST endpoint when true. |
recv_window |
5000 |
Maximum accepted request age in milliseconds for signed requests. |
timeout |
30 |
Total HTTP request timeout in seconds. |
connect_timeout |
10 |
Connection timeout in seconds. |
base_url |
Zoomex mainnet/testnet URL | Optional override for controlled environments and tests. |
REST recipes
The examples below use Laravel's Zoomex facade for brevity. In standalone PHP, replace Zoomex with the $client instance created above.
Server time and a single ticker
Use this for a small health check, a command, or a dashboard card. No API key is required.
use Tigusigalpa\Zoomex\DTO\Market\GetTickersRequest; use Tigusigalpa\Zoomex\Enums\Category; use Tigusigalpa\Zoomex\Facades\Zoomex; $serverTime = Zoomex::market()->getServerTime(); $ticker = Zoomex::market()->getTickers(new GetTickersRequest( category: Category::LINEAR, symbol: 'BTCUSDT', )); // Field names are supplied by Zoomex; inspect the returned result while integrating. logger()->info('BTCUSDT ticker', $ticker);
Candles, order book, and recent trades
Time values in REST requests are Unix timestamps in milliseconds. The package deliberately leaves date handling to your application, so there is no hidden timezone conversion.
use Tigusigalpa\Zoomex\DTO\Market\{ GetKlineRequest, GetOrderbookRequest, GetPublicTradingHistoryRequest, }; use Tigusigalpa\Zoomex\Enums\{Category, Interval}; use Tigusigalpa\Zoomex\Facades\Zoomex; $end = (int) floor(microtime(true) * 1000); $start = $end - (60 * 60 * 1000); // one hour earlier $candles = Zoomex::market()->getKline(new GetKlineRequest( category: Category::LINEAR, symbol: 'BTCUSDT', interval: Interval::FIVE_MINUTES, start: $start, end: $end, limit: 12, )); $orderbook = Zoomex::market()->getOrderbook(new GetOrderbookRequest( category: Category::LINEAR, symbol: 'BTCUSDT', limit: 50, )); $trades = Zoomex::market()->getPublicTradingHistory( new GetPublicTradingHistoryRequest( category: Category::LINEAR, symbol: 'BTCUSDT', limit: 20, ), );
Discover instrument rules before sending an order
Symbols have their own price tick, quantity step, and minimum order rules. Read instrument metadata first and validate the order in your application; an exchange rejection is slower and less friendly to users.
use Tigusigalpa\Zoomex\DTO\Market\GetInstrumentsInfoRequest; use Tigusigalpa\Zoomex\Enums\Category; use Tigusigalpa\Zoomex\Facades\Zoomex; $instrument = Zoomex::market()->getInstrumentsInfo( new GetInstrumentsInfoRequest( category: Category::LINEAR, symbol: 'BTCUSDT', ), );
Place a limit order with a client-side identifier
Private endpoints require a key with the corresponding account permission. orderLinkId is useful for safely reconciling the order with your database or strategy.
use Tigusigalpa\Zoomex\DTO\Trade\PlaceOrderRequest; use Tigusigalpa\Zoomex\Enums\{Category, OrderSide, OrderType, TimeInForce}; use Tigusigalpa\Zoomex\Facades\Zoomex; $order = Zoomex::trade()->placeOrder(new PlaceOrderRequest( category: Category::LINEAR, symbol: 'BTCUSDT', side: OrderSide::BUY, orderType: OrderType::LIMIT, qty: '0.01', price: '50000', timeInForce: TimeInForce::GTC, orderLinkId: 'rebalance-' . bin2hex(random_bytes(8)), )); $orderId = $order['orderId'] ?? null;
Place a market order with take-profit and stop-loss
Market orders do not need price. Values that affect balances or execution are strings intentionally, so application code does not accidentally introduce floating-point rounding.
use Tigusigalpa\Zoomex\DTO\Trade\PlaceOrderRequest; use Tigusigalpa\Zoomex\Enums\{Category, OrderSide, OrderType}; use Tigusigalpa\Zoomex\Facades\Zoomex; $order = Zoomex::trade()->placeOrder(new PlaceOrderRequest( category: Category::LINEAR, symbol: 'BTCUSDT', side: OrderSide::BUY, orderType: OrderType::MARKET, qty: '0.01', takeProfit: '52000', stopLoss: '48000', tpTriggerBy: 'LastPrice', slTriggerBy: 'LastPrice', ));
Trading safety: these snippets can create real orders when
ZOOMEX_TESTNET=false. Confirm account mode, symbol rules, quantity, and API-key permissions before using any private trading code in production.
Find, amend, and cancel an order
Persist either the exchange orderId or your orderLinkId. Both are accepted by the request objects where Zoomex permits them.
use Tigusigalpa\Zoomex\DTO\Trade\{ AmendOrderRequest, CancelOrderRequest, GetOpenOrdersRequest, }; use Tigusigalpa\Zoomex\Enums\Category; use Tigusigalpa\Zoomex\Facades\Zoomex; $openOrders = Zoomex::trade()->getOpenOrders(new GetOpenOrdersRequest( category: Category::LINEAR, symbol: 'BTCUSDT', limit: 50, )); Zoomex::trade()->amendOrder(new AmendOrderRequest( category: Category::LINEAR, symbol: 'BTCUSDT', orderLinkId: 'rebalance-2f8a9c', price: '49950', )); Zoomex::trade()->cancelOrder(new CancelOrderRequest( category: Category::LINEAR, symbol: 'BTCUSDT', orderLinkId: 'rebalance-2f8a9c', ));
Position, leverage, and wallet balance
use Tigusigalpa\Zoomex\DTO\Account\GetWalletBalanceRequest; use Tigusigalpa\Zoomex\DTO\Position\{GetPositionInfoRequest, SetLeverageRequest}; use Tigusigalpa\Zoomex\Enums\{AccountType, Category}; use Tigusigalpa\Zoomex\Facades\Zoomex; $positions = Zoomex::position()->getPositionInfo( new GetPositionInfoRequest( category: Category::LINEAR, symbol: 'BTCUSDT', ), ); $balance = Zoomex::account()->getWalletBalance( new GetWalletBalanceRequest( accountType: AccountType::CONTRACT, coin: 'USDT', ), ); // This changes account risk. Use testnet first. Zoomex::position()->setLeverage(new SetLeverageRequest( category: Category::LINEAR, symbol: 'BTCUSDT', buyLeverage: '3', sellLeverage: '3', ));
Set or update a trading stop
For an existing position, keep the request focused on the values you intend to change. Omitted optional values are not sent.
use Tigusigalpa\Zoomex\DTO\Position\SetTradingStopRequest; use Tigusigalpa\Zoomex\Enums\{Category, TpSlMode}; use Tigusigalpa\Zoomex\Facades\Zoomex; Zoomex::position()->setTradingStop(new SetTradingStopRequest( category: Category::LINEAR, symbol: 'BTCUSDT', takeProfit: '52000', stopLoss: '48000', tpTriggerBy: 'LastPrice', slTriggerBy: 'LastPrice', tpSlMode: TpSlMode::FULL, ));
WebSocket recipes
WebSocket subscriptions are asynchronous. Register all subscriptions first, then call run() to start the ReactPHP event loop. run() blocks until the loop is stopped, so a long-running Laravel worker or console command is usually a better fit than a short-lived HTTP request.
Stream a linear order book
use Tigusigalpa\Zoomex\WebSocket; $ws = new WebSocket(['testnet' => true]); $ws->subscribeOrderbook('BTCUSDT', 50, function ($data) { // $data is the payload for the subscribed topic. print_r($data); }); $ws->run();
Subscribe to multiple public topics
Choose the category explicitly for spot or inverse markets. A WebSocket instance maintains one public category connection; create another instance if one process needs both spot and linear feeds.
use Tigusigalpa\Zoomex\Enums\Category; use Tigusigalpa\Zoomex\WebSocket; $spot = new WebSocket(['testnet' => true]); $spot->subscribeWithCategory(Category::SPOT, [ 'orderbook.50.BTCUSDT', 'publicTrade.BTCUSDT', 'tickers.BTCUSDT', ], function ($data) { print_r($data); }); $spot->run();
Listen for private order, execution, position, and wallet changes
Private WebSocket subscriptions authenticate before subscribing. Supply credentials and give the key only the permissions it needs.
use Tigusigalpa\Zoomex\WebSocket; $private = new WebSocket([ 'api_key' => getenv('ZOOMEX_API_KEY'), 'secret_key' => getenv('ZOOMEX_SECRET_KEY'), 'testnet' => true, ]); $private->subscribePrivateOrders(function ($data) { // Update an order record by orderId/orderLinkId in your application. print_r($data); }); $private->subscribePrivateExecutions(function ($data) { print_r($data); }); $private->subscribePrivatePositions(function ($data) { print_r($data); }); $private->subscribePrivateWallet(function ($data) { print_r($data); }); $private->run();
The client sends a heartbeat and checks the private-auth response. It does not reconnect or re-subscribe automatically after a network disconnect; production daemons should supervise the process, create a fresh client on reconnect, and re-register their subscriptions.
Errors, retries, and observability
There are two exception types:
ZoomexApiException: Zoomex answered the request but returnedretCode !== 0. It containsretCode,retMsg, and optionalretExtInfo.ZoomexRequestException: network failure, invalid JSON, failed JSON encoding, or an HTTP-level failure. It contains the URL and HTTP method when available.
use Tigusigalpa\Zoomex\Exceptions\{ZoomexApiException, ZoomexRequestException}; use Tigusigalpa\Zoomex\Facades\Zoomex; use Tigusigalpa\Zoomex\DTO\Trade\PlaceOrderRequest; function submitOrder(PlaceOrderRequest $request) { try { return Zoomex::trade()->placeOrder($request); } catch (ZoomexApiException $e) { report($e); // Example: map a known exchange rejection to your application's error format. throw new \RuntimeException("Zoomex rejected the order: {$e->retMsg}", $e->retCode, $e); } catch (ZoomexRequestException $e) { report($e); throw new \RuntimeException('Zoomex is temporarily unavailable.', 0, $e); } }
The package does not retry requests automatically. Retry only safe, idempotent reads by default. For order creation, use a stable orderLinkId and reconcile the outcome with the exchange before attempting another request.
Working safely
- Begin with market-data methods, then use a separate testnet key for private flows.
- Keep API keys in environment variables or a secrets manager, never in source code or queue payloads.
- Give each key the smallest necessary permission set; do not use withdrawal permissions for a trading-only service.
- Validate quantity and price against instrument metadata before sending an order.
- Log the exchange order ID and your
orderLinkIdtogether for every strategy action. - Respect Zoomex rate limits and add application-level backoff around polling jobs.
API Reference
Market (public)
| Method | Endpoint | Description |
|---|---|---|
getServerTime() |
/cloud/trade/v3/market/time |
Get server time |
getKline() |
/cloud/trade/v3/market/kline |
Get kline data |
getMarkPriceKline() |
/cloud/trade/v3/market/mark-price-kline |
Get mark price kline |
getIndexPriceKline() |
/cloud/trade/v3/market/index-price-kline |
Get index price kline |
getPremiumIndexPriceKline() |
/cloud/trade/v3/market/premium-index-price-kline |
Get premium index price kline |
getInstrumentsInfo() |
/cloud/trade/v3/market/instruments-info |
Get instruments info |
getOrderbook() |
/cloud/trade/v3/market/orderbook |
Get orderbook |
getTickers() |
/cloud/trade/v3/market/tickers |
Get tickers |
getFundingRateHistory() |
/cloud/trade/v3/market/funding/history |
Get funding rate history |
getPublicTradingHistory() |
/cloud/trade/v3/market/recent-trade |
Get public trading history |
getRiskLimit() |
/cloud/trade/v3/market/risk-limit |
Get risk limit |
Trade (private)
| Method | Endpoint | Description |
|---|---|---|
placeOrder() |
/cloud/trade/v3/order/create |
Place order |
amendOrder() |
/cloud/trade/v3/order/amend |
Amend order |
cancelOrder() |
/cloud/trade/v3/order/cancel |
Cancel order |
getOpenOrders() |
/cloud/trade/v3/order/realtime |
Get open orders |
cancelAllOrders() |
/cloud/trade/v3/order/cancel-all |
Cancel all orders |
getOrderHistory() |
/cloud/trade/v3/order/history |
Get order history |
getTradeHistory() |
/cloud/trade/v3/execution/list |
Get trade history |
Position (private)
| Method | Endpoint | Description |
|---|---|---|
getPositionInfo() |
/cloud/trade/v3/position/list |
Get position info |
setLeverage() |
/cloud/trade/v3/position/set-leverage |
Set leverage |
switchIsolated() |
/cloud/trade/v3/position/switch-isolated |
Switch cross/isolated margin |
setTpSlMode() |
/cloud/trade/v3/position/set-tpsl-mode |
Set TP/SL mode |
switchPositionMode() |
/cloud/trade/v3/position/switch-mode |
Switch position mode |
setRiskLimit() |
/cloud/trade/v3/position/set-risk-limit |
Confirm new risk limit |
setTradingStop() |
/cloud/trade/v3/position/trading-stop |
Set trading stop |
setAutoAddMargin() |
/cloud/trade/v3/position/set-auto-add-margin |
Set auto add margin |
addOrReduceMargin() |
/cloud/trade/v3/position/add-margin |
Add or reduce margin |
getClosedPnl() |
/cloud/trade/v3/position/closed-pnl |
Get closed PnL |
Account (private)
| Method | Endpoint | Description |
|---|---|---|
getWalletBalance() |
/cloud/trade/v3/account/wallet-balance |
Get wallet balance |
getFeeRate() |
/cloud/trade/v3/account/fee-rate |
Get fee rate |
Asset (private)
The asset methods remain only for source compatibility. Zoomex documents these legacy endpoints with a different V1 signing scheme and some payloads differ from the V3 implementation. Do not use them in new integrations until this migration is completed.
| Method | Endpoint | Description |
|---|---|---|
getCoinExchangeRecords() |
/cloud/trade/v3/asset/coin-exchange-record |
Get coin exchange records |
getDeliveryRecord() |
/cloud/trade/v3/asset/delivery-record |
Get delivery record |
getSettlementRecord() |
/cloud/trade/v3/asset/settlement-record |
Get settlement record |
getAssetInfo() |
/cloud/trade/v3/asset/info |
Get asset info |
getAllCoinsBalance() |
/cloud/trade/v3/asset/all-coins-balance |
Get all coins balance |
getInternalTransferRecords() |
/cloud/trade/v3/asset/transfer/query-inter-transfer-list |
Get internal transfer records |
getSubUIDList() |
/cloud/trade/v3/asset/transfer/query-sub-member-list |
Get sub UID list |
createInternalTransfer() |
/cloud/trade/v3/asset/transfer/inter-transfer |
Create internal transfer |
getDepositRecords() |
/cloud/trade/v3/asset/deposit/query-record |
Get deposit records |
getWithdrawalRecords() |
/cloud/trade/v3/asset/withdraw/query-record |
Get withdrawal records |
getCoinInfo() |
/cloud/trade/v3/asset/coin/query-info |
Get coin info |
withdraw() |
/cloud/trade/v3/asset/withdraw/create |
Withdraw |
cancelWithdrawal() |
/cloud/trade/v3/asset/withdraw/cancel |
Cancel withdrawal |
WebSocket Topics
| Channel Type | Topic Pattern | Description |
|---|---|---|
| Public | orderbook.{depth}.{symbol} |
Orderbook (depth: 1, 50, 200, 1000); use subscribeOrderbookWithCategory() for spot/inverse |
| Public | publicTrade.{symbol} |
Public trades |
| Public | tickers.{symbol} |
Ticker |
| Public | kline.{interval}.{symbol} |
Kline |
| Public | liquidation.{symbol} |
All liquidation |
| Private | order |
Order updates |
| Private | position |
Position updates |
| Private | execution |
Execution updates |
| Private | wallet |
Wallet updates |
Testing
composer test
Contributing
See CONTRIBUTING.md.
Security
Found a vulnerability? Email sovletig@gmail.com (don't open public issues).
Author
Based on zoomex-go.
License
MIT. See LICENSE.