Search by

tigusigalpa / zoomex-php

tigusigalpa

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.

Package info

github.com/tigusigalpa/zoomex-php

pkg:composer/tigusigalpa/zoomex-php

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 17

Open Issues: 0

v1.1.1 2026-04-13 02:19 UTC

This package is auto-updated.

Last update: 2026-09-15 14:35:36 UTC


README

Zoomex PHP

Latest Version on Packagist PHP Version License Tests Coverage CodeQL for Actions Security codecov

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 returned retCode !== 0. It contains retCode, retMsg, and optional retExtInfo.
  • 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 orderLinkId together 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

Igor Sazonov

Based on zoomex-go.

License

MIT. See LICENSE.

Links