WebSocket
Environment
| Environment | Base URL |
|---|---|
| Production | wss://api-studio.r25.xyz |
1. Document Overview & Capabilities
This document is for external partners integrating the OpenAPI trading WebSocket interface. It describes the trading endpoints exposed in the current version, the handshake signing method, the message protocol, and the callback structure. The Host, Client ID, and signature in the examples are placeholders; replace them with the values assigned by the platform or agreed upon by both parties.
The same AK/SK can be used for both WebSocket and REST: WebSocket handles trading write operations and real-time subscription pushes; for scenarios without a persistent connection, you can use the equivalent REST endpoints for order create/cancel and position close (see the REST documentation).
Current WebSocket capabilities include:
- Trading writes:
order.create,order.cancel,position.close; currently connected toHYPERLIQUIDandHABITTRADE. - Trading queries:
order.query,position.query. - Real-time subscriptions:
order.updated,position.updated, pushed per vault frame. - Account-level authorization:
data.vaultAddressof every business op must belong to a vault operable by the current AK, otherwiseVAULT_NOT_PERMITTED.
2. Base URL, Content-Type, Time/Number/Null Conventions
2.1 General Conventions
- Base URL: pick the corresponding
wssaddress from the environment table at the top; the trading endpoint path is fixed at/api/openapi/v1/ws. - Transport: UTF-8 JSON text frames; binary frames are closed. WebSocket does not use
Content-Typeor a request body. - Uplink frame limit: 5 KiB (exceeding returns
UPLINK_TOO_LARGE, connection not closed); exceeding 64 KiB closes the connection (code1009). - Authentication signs once during the handshake only; after the connection is established, no further auth is needed (see §4).
- Handshake URL query params (
clientId/timestamp/nonce/signature) should be UTF-8 percent-encoded. - This document only covers the trading WebSocket protocol available to partners.
2.2 Time Conventions
- Handshake
timestamp: Unix seconds (string); acceptsyyyy-MM-dd HH:mm:ssor a plain number (≥1e12 treated as milliseconds), valid window ±300 seconds. - Uplink request
ts: Unix millisecond timestamp, must be strictly increasing (ts≤ the cached previousts→ dropped withREQUEST_TS_STALE). - Downlink response/event
ts,acceptedAt, and order/positioncreatedAt/updatedAt/exchangeTime/serverTime: all epoch milliseconds.
2.3 Number & Null Conventions
- Trading numbers (quantity, price, leverage, slippage, USD value, fee, etc.) use decimal strings; do not parse them with lossy types such as JavaScript
Number.leverage.value/rawUsd,maxSlippageBps, etc. are also decimal strings. - Quantity must be a multiple of
sizeStepand limitpricea multiple ofpriceTick(see the overview table in §3); otherwiseINVALID_SIZE_STEP/INVALID_PRICE_TICKis returned. - Price fields are in USD per asset unless otherwise noted.
- Downlink serialization omits fields whose value is
nullby default (see §5 Message Protocol).
3. Overview
This document is for external partners integrating the OpenAPI trading WebSocket interface. The WebSocket interface supports order create/query/cancel, position query/close, and real-time order/position push subscriptions. The same AK/SK can be used for both WebSocket and REST.
| Item | Value |
|---|---|
| WS endpoint | wss://<host>/api/openapi/v1/ws |
| Transport | UTF-8 JSON text frames (binary frames are closed) |
| Protocol version | v field fixed to "1.0.0" |
| Exchanges | HYPERLIQUID (HL, perpetual contracts) / HABITTRADE (HT, spot rebalancing vault) |
| Business ops | order.create / order.query / order.cancel / position.query / position.close |
| Subscription topics | order.updated / position.updated (type=subscribe, op is the topic) |
| Quantity precision | HL sizeStep=0.001; HT sizeStep=1 (must be a multiple, otherwise INVALID_SIZE_STEP) |
| Price precision | HL priceTick=0.1; HT priceTick=0.01 (limit price must be a multiple) |
Only one active connection is allowed per AK: opening a new connection with the same AK closes the old one (code 4001). The new connection does not inherit the old connection's subscriptions or in-flight requests.
4. Authentication & Handshake Signing
WebSocket reuses the HTTP OpenAPI AK/SK: clientId is the AK and the Ed25519 private key is the SK. Signing happens only at the handshake; no auth afterwards.
Handshake Signing
wss://<host>/api/openapi/v1/ws?clientId=<AK>×tamp=<ts>&nonce=<nonce>&signature=<sig>
| Param | Description |
|---|---|
clientId | AK (same Access Key as HTTP OpenAPI) |
timestamp | Unix seconds (string). Accepts yyyy-MM-dd HH:mm:ss or a plain number (≥1e12 treated as ms). Valid window ±300 seconds |
nonce | Random string (UUID recommended), one-time, must not repeat within 300 seconds |
signature | Ed25519 signature of the string to sign, lowercase hex (64 bytes ≡ 128 chars) |
String to sign (three lines separated by \n):
<clientId>\n<timestamp>\n<nonce>
i.e. the UTF-8 bytes of clientId + "\n" + timestamp + "\n" + nonce are signed with Ed25519 and hex-encoded.
Any check failure → the handshake returns HTTP 401 and does not upgrade to WS. After a successful handshake, no further auth is required for the connection.
5. Message Protocol
Serialization omits null fields. Allowed uplink type values: request / subscribe / unsubscribe.
5.1 Uplink Request
{
"v": "1.0.0",
"type": "request",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"exchange": "HYPERLIQUID",
"data": { }
}
| Field | Required | Description |
|---|---|---|
v | Yes | Protocol version, fixed 1.0.0 |
type | Yes | request / subscribe / unsubscribe |
id | Yes | Request ID, unique per connection, used to match responses. Reuse on the same connection → DUPLICATE_REQUEST_ID |
op | Yes | For request, the business op; for subscribe/unsubscribe, the topic/fixed string |
ts | Yes | Client time (ms), must be strictly increasing (ts ≤ cached previous ts → REQUEST_TS_STALE dropped) |
exchange | No | HYPERLIQUID / HABITTRADE; omit only for order.query/position.query cross-exchange merged query |
data | Depends on op | Business params of each op, see §7/§8 |
The uplink frame limit is 5 KiB; exceeding returns
UPLINK_TOO_LARGE(connection not closed); exceeding 64 KiB closes the connection (code1009).
5.2 Downlink Response
{
"v": "1.0.0",
"type": "response",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"code": "SUCCESS",
"message": "accepted",
"data": { }
}
code is an error code name (see §10); code=SUCCESS means success.
5.3 Downlink Event (Subscription Push)
{
"v": "1.0.0",
"type": "event",
"topic": "position.updated",
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"seq": 41,
"exchange": "HYPERLIQUID",
"ts": 1787638868358,
"data": { }
}
seq is monotonically increasing within a connection, used by the client for dedup/alignment, reset to zero on reconnect.
6. Connection, Rate Limit, Heartbeat & Close Codes
| Convention | Default | Description |
|---|---|---|
| Max uplink frame | 5 KiB | Exceeds → UPLINK_TOO_LARGE; over 64 KiB → 1009 |
| Max downlink frame | 64 KiB | Exceeds → dropped |
| In-flight requests per connection | 10 | Duplicate id → DUPLICATE_REQUEST_ID; full → TOO_MANY_INFLIGHT_REQUESTS |
| Max subscriptions per connection | 4 | Re-subscribing the same exchange+topic overwrites the old (does not add quota) |
| Heartbeat Ping period | 30s | Server sends Ping periodically |
| Pong timeout | 60s | No Pong for 60s → close code 4002 |
Rate limit (clientId + IP dimensions): IP 60/60s; clientId 120/1h. Either hit → RATE_LIMIT.
Close codes:
| Code | Meaning |
|---|---|
| 1000 | Normal close |
| 1008 | Auth/config policy rejection; binary frame; transport error |
| 1009 | Uplink frame over 64 KiB |
| 4001 | Replaced by a new connection on the same AK |
| 4002 | Pong timeout (no Pong for 60s) |
7. HYPERLIQUID (HL) Business Endpoints
General:
data.vaultAddressis required for every business op; the server checks whether it is operable by the current AK. No permission →VAULT_NOT_PERMITTED;data.deniedVaultAddresseslists the denied addresses.vaultAddressis case-insensitive.
7.1 order.create
HL supports MARKET and LIMIT order types.
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Yes | Coin |
side | Yes | BUY / SELL |
orderType | Yes | MARKET / LIMIT (other → INVALID_REQUEST) |
quantity | Yes | Coin quantity, must be a multiple of sizeStep (0.001) |
leverage | Yes | Leverage |
price | LIMIT required | Limit price, must be a multiple of priceTick (0.1) |
maxSlippageBps | MARKET required | Market slippage, 1–1000 |
timeInForce | No | e.g. GTC / FrontendMarket |
reduceOnly | No | defaults to false |
Request example (market buy):
{
"v": "1.0.0",
"type": "request",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol": "ETH",
"side": "BUY",
"orderType": "MARKET",
"quantity": "0.01",
"leverage": "1",
"maxSlippageBps": "500",
"timeInForce": "FrontendMarket",
"reduceOnly": false
}
}
Response data:
| Field | Description |
|---|---|
orderId | HYPERLIQUID:<exchangeOrderId> |
exchangeOrderId | Upstream order id (same value used for cancel/query) |
status | Always PENDING on success |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | Always null for HL (omitted) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.create-4",
"op": "order.create",
"ts": 1787623689692,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HYPERLIQUID:525764570803",
"exchangeOrderId": "525764570803",
"status": "PENDING",
"acceptedAt": 1787623689690
}
}
7.2 order.cancel
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | Yes | exchangeOrderId returned by create/query |
symbol | Yes | Same coin as the order |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.cancel-3",
"op": "order.cancel",
"ts": 1787625671772,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"exchangeOrderId": "525796683535",
"symbol": "ETH"
}
}
Response data:
| Field | Description |
|---|---|
exchangeOrderId | Same as request |
status | CANCELED |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.cancel-3",
"op": "order.cancel",
"ts": 1787625672636,
"code": "SUCCESS",
"message": "success",
"data": {
"exchangeOrderId": "525796683535",
"status": "CANCELED"
}
}
7.3 position.close
HL close supports full close only (PARTIAL → FEATURE_UNSUPPORTED).
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
symbol | Yes | Coin |
maxSlippageBps | Yes | 1–1000 |
quantity | No | Omitted/empty = full close; if provided, must be a multiple of sizeStep |
Request example (full close):
{
"v": "1.0.0",
"type": "request",
"id": "position.close-2",
"op": "position.close",
"ts": 1787625645550,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol": "ETH",
"maxSlippageBps": "50"
}
}
Response data:
| Field | Description |
|---|---|
orderId | HYPERLIQUID:<exchangeOrderId> |
exchangeOrderId | Upstream order id |
status | Always PENDING on success |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | Always null for HL (omitted) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.close-2",
"op": "position.close",
"ts": 1787625646517,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HYPERLIQUID:525796683535",
"exchangeOrderId": "525796683535",
"status": "PENDING",
"acceptedAt": 1787625646517
}
}
7.4 order.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | No | Exchange order id |
limit | No | 1–100; merged query default 10, max 100 |
startDate / endDate | No | Time range |
symbol | No | Coin |
status | No | Unified status: PENDING/PARTIALLY_FILLED/FILLED/CANCELED/REJECTED |
exchange | No | Omit to merge HL/HT queries, take limit sorted by createdAt descending |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.query-2",
"op": "order.query",
"ts": 1787629031208,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"limit": 20
}
}
Response data.orders array, element fields:
| Field | Description |
|---|---|
exchange | HYPERLIQUID |
symbol | Coin |
tokenAddress | Always null for HL |
exchangeOrderId | Exchange id |
side | BUY / SELL |
orderType | MARKET / LIMIT / CANCEL, etc. |
quantity / filledQuantity / price / averagePrice / fee | Decimal strings |
status | PENDING / PARTIALLY_FILLED / FILLED / CANCELED / REJECTED |
reduceOnly | bool |
createdAt / updatedAt | epoch ms |
vaultAddress | Vault address |
Response example (data.orders** array, two shown):**
{
"v": "1.0.0",
"type": "response",
"id": "order.query-2",
"op": "order.query",
"ts": 1787629031344,
"code": "SUCCESS",
"message": "success",
"data": {
"orders": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": null,
"side": null,
"orderType": "CANCEL",
"quantity": null,
"filledQuantity": null,
"price": null,
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787625672000,
"updatedAt": 1787625672000,
"fee": null,
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
},
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": "525796683535",
"side": "SELL",
"orderType": "MARKET",
"quantity": "0.018100000000000000",
"filledQuantity": null,
"price": "2507.800000000000000000",
"averagePrice": null,
"status": "FILLED",
"reduceOnly": true,
"createdAt": 1787625646000,
"updatedAt": 1787625650000,
"fee": "0.020528000000000000",
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
]
}
}
7.5 position.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchange | No | Omit to merge both exchanges |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "position.query-1",
"op": "position.query",
"ts": 1787637839577,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
}
Response data.positions array, element fields (contract position):
| Field | Description |
|---|---|
exchange | HYPERLIQUID |
symbol | Coin |
tokenAddress | Always null for HL |
side | LONG / SHORT (net position direction; SHORT = short) |
quantity | Position size (decimal string) |
entryPrice | Entry price |
markPrice | Always null for HL |
unrealizedPnl | Unrealized PnL |
leverage | { type, value, rawUsd } (e.g. { "type": "cross", "value": "20", "rawUsd": null }) |
marginMode | CROSS / ISOLATED |
positionValue / marginUsed / returnOnEquity / maxLeverage | Decimal strings |
exchangeTime / serverTime | Upstream/server time (ms) |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.query-1",
"op": "position.query",
"ts": 1787637839823,
"code": "SUCCESS",
"message": "success",
"data": {
"positions": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"side": "LONG",
"quantity": "0.01",
"entryPrice": "2496.6",
"markPrice": null,
"unrealizedPnl": "0.131",
"leverage": { "type": "cross", "value": "20", "rawUsd": null },
"marginMode": "CROSS",
"positionValue": "25.097",
"marginUsed": "1.25485",
"returnOnEquity": "0.1049427221",
"maxLeverage": "25",
"exchangeTime": null,
"serverTime": 1787637839822
}
]
}
}
8. HABITTRADE Business Endpoints
HT (HabitTrade, spot rebalancing vault) semantics differ from HL:
orderType(MARKET/LIMIT) is an order type, not a direction; direction is determined byside(BUY/SELL).- HT does not support cancel →
order.cancelreturnsFEATURE_UNSUPPORTED. - Create/close ack carries
encodedData(ABI payload to sign; the client signs and submits on-chain); HL always omits it. - Precision:
sizeStep = 1,priceTick = 0.01.
8.1 order.create
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
tokenAddress | No (recommended) | Asset contract address |
symbol | Yes | Asset symbol |
side | Yes | BUY / SELL, mapped to upstream action |
orderType | Yes | MARKET / LIMIT (type, not direction) |
quantity | Yes | Share quantity, must be a multiple of sizeStep (1) |
maxSlippageBps | Yes | Slippage 1–1000 |
price | LIMIT required | Limit price, must be a multiple of priceTick (0.01) |
Request example (market buy):
{
"v": "1.0.0",
"type": "request",
"id": "order.create-ht-1",
"op": "order.create",
"ts": 1787751481326,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"symbol": "SPCHs",
"side": "BUY",
"quantity": "1",
"orderType": "MARKET",
"maxSlippageBps": "3000"
}
}
Response data:
| Field | Description |
|---|---|
orderId | HABITTRADE:<exchangeOrderId> |
exchangeOrderId | Upstream order id |
status | Fixed BUILT on success |
acceptedAt | Upstream accept time (epoch ms) |
encodedData | ABI payload to sign (0x… hex); client signs and submits on-chain |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.create-ht-1",
"op": "order.create",
"ts": 1787751486434,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HABITTRADE:62f917df0a7f483e9783d8fd4b7b8115",
"exchangeOrderId": "62f917df0a7f483e9783d8fd4b7b8115",
"status": "BUILT",
"acceptedAt": 1787751486433,
"encodedData": "0xfca9bd9e0000000000000000000000...(ABI to sign, actual length is longer)"
}
}
8.2 order.cancel — Unsupported
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.cancel-ht-1",
"op": "order.cancel",
"ts": 1787626010000,
"code": "FEATURE_UNSUPPORTED",
"message": "HabitTrade does not support order cancel",
"data": null
}
8.3 position.close
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
tokenAddress | Yes | Asset contract address |
symbol | Yes | Asset to close |
maxSlippageBps | Yes | Slippage 1–1000 |
quantity | No | "-1" or omitted = full close (only valid for SELL); positive = partial close (must be a multiple of sizeStep) |
Request example (full close, quantity omitted):
{
"v": "1.0.0",
"type": "request",
"id": "position.close-ht-1",
"op": "position.close",
"ts": 1787736302848,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"symbol": "SPCHs",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"maxSlippageBps": "50"
}
}
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.close-ht-1",
"op": "position.close",
"ts": 1787736303000,
"code": "SUCCESS",
"message": "accepted",
"data": {
"orderId": "HABITTRADE:62f917df0a7f483e9783d8fd4b7b8115",
"exchangeOrderId": "62f917df0a7f483e9783d8fd4b7b8115",
"status": "BUILT",
"acceptedAt": 1787736302999,
"encodedData": "0xfca9bd9e0000000000000000000000...(ABI to sign)"
}
}
8.4 order.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
exchangeOrderId | No | Exchange order id (for HT, the transaction hash) |
limit | No | Merged query default 10, max 100 |
startDate / endDate | No | Unix seconds or yyyy-MM-dd HH:mm:ss |
symbol | No | Asset contract address |
tokenAddress | No | Asset contract address |
status | No | Unified status |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "order.query-ht-1",
"op": "order.query",
"ts": 1787664502488,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df",
"limit": 20
}
}
Response data.orders array, element fields:
| Field | Description |
|---|---|
exchange | HABITTRADE |
symbol | Asset contract address |
tokenAddress | Asset contract address |
exchangeOrderId | Transaction hash |
side | BUY / SELL |
orderType | Always null (no market/limit concept) |
quantity / filledQuantity | 0 |
price / averagePrice | Fill price (same value) |
status | FILLED / PENDING / ERROR / PARTIALLY_FILLED |
reduceOnly | true for SELL, false for BUY |
createdAt / updatedAt | epoch ms |
fee | Always null |
vaultAddress | Vault address |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "order.query-ht-1",
"op": "order.query",
"ts": 1787664502653,
"code": "SUCCESS",
"message": "success",
"data": {
"orders": [
{
"exchange": "HABITTRADE",
"symbol": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"tokenAddress": "0x804688f293c15a5454586217a69f1cb36c20e15b",
"exchangeOrderId": "0x66560a584e68f8f9385d35acb950bfc0285fc3e739b708b8d71d6da628e54e33",
"side": "BUY",
"orderType": null,
"quantity": "0",
"filledQuantity": "0",
"price": null,
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787385339000,
"updatedAt": 1787385399000,
"fee": null,
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df"
}
]
}
}
8.5 position.query
Request data:
| Field | Required | Description |
|---|---|---|
vaultAddress | Yes | Vault address |
Request example:
{
"v": "1.0.0",
"type": "request",
"id": "position.query-ht-1",
"op": "position.query",
"ts": 1787662054736,
"exchange": "HABITTRADE",
"data": {
"vaultAddress": "0xd12041f181cbd0ec66b459de1f5ce219254c62df"
}
}
Response data.positions array, element fields (vault portfolio):
| Field | Description |
|---|---|
exchange | HABITTRADE |
symbol | Asset name |
tokenAddress | Asset contract address |
side | Always LONG (held by the vault) |
quantity | Current quantity |
markPrice | Current quote |
entryPrice | Entry price (from upstream) |
unrealizedPnl / returnOnEquity | null (spot has none) |
leverage | { "type": "cross", "value": "1", "rawUsd": "0" } |
marginMode | CROSS |
positionValue | Current USD value |
marginUsed | "0" (spot has no margin) |
maxLeverage | "1" |
exchangeTime / serverTime | ms |
Response example:
{
"v": "1.0.0",
"type": "response",
"id": "position.query-ht-1",
"op": "position.query",
"ts": 1787667041781,
"code": "SUCCESS",
"message": "success",
"data": {
"positions": [
{
"exchange": "HABITTRADE",
"symbol": "USDC",
"tokenAddress": "0xc879c018db60520f4355c26ed1a6d572cdac1815",
"side": "LONG",
"quantity": "16.435225",
"entryPrice": null,
"markPrice": "1",
"unrealizedPnl": null,
"leverage": { "type": "cross", "value": "1", "rawUsd": "0" },
"marginMode": "CROSS",
"positionValue": "16.435225",
"marginUsed": "0",
"returnOnEquity": null,
"maxLeverage": "1",
"exchangeTime": null,
"serverTime": 1787667041780
}
]
}
}
9. Subscription Endpoints
type=subscribe, op is the topic (order.updated / position.updated), data.vaultAddresses is an array; exchange is HYPERLIQUID or HABITTRADE.
9.1 Subscribe Request & ack
Request example (position subscription):
{
"v": "1.0.0",
"type": "subscribe",
"id": "position.updated-6",
"op": "position.updated",
"ts": 1787638868184,
"exchange": "HYPERLIQUID",
"data": {
"vaultAddresses": ["0x7097732c73b74e94d6e8cc9e77c287757a17afdc"]
}
}
ack (note the ack op is "subscribe", not the topic):
{
"v": "1.0.0",
"type": "subscribe",
"id": "position.updated-6",
"op": "subscribe",
"ts": 1787638868357,
"code": "SUCCESS",
"message": "subscribed",
"data": {
"subscription": {
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"exchange": "HYPERLIQUID",
"topic": "position.updated"
}
}
}
data.vaultAddressesis required; each address must be operable by the AK. Any without permission →VAULT_NOT_PERMITTED;data.deniedVaultAddresseslists the denied addresses (does not affect existing subscriptions).- Re-subscribing the same
exchange+topicoverwrites the old subscription; only brand-new subscriptions check the limit (≤ 4). - For HT subscriptions, change
exchangetoHABITTRADE; handshake/heartbeat are the same as HL.
9.2 Event Frames
Position event (one push immediately after subscribing with reason=INITIAL, then every 60s with reason=PERIODIC, one frame per vault, including empty snapshots):
{
"v": "1.0.0",
"type": "event",
"topic": "position.updated",
"subscriptionId": "sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd",
"seq": 41,
"exchange": "HYPERLIQUID",
"ts": 1787638868358,
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"snapshot": true,
"reason": "INITIAL",
"stale": false,
"asOf": 1787638868357,
"positions": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"side": "LONG",
"quantity": "0.01",
"entryPrice": "2496.6",
"markPrice": null,
"unrealizedPnl": "0.131",
"leverage": { "type": "cross", "value": "20", "rawUsd": null },
"marginMode": "CROSS",
"positionValue": "25.097",
"marginUsed": "1.25485",
"returnOnEquity": "0.1049427221",
"maxLeverage": "25",
"exchangeTime": null,
"serverTime": 1787638868357
}
]
}
}
Order event (one push immediately after subscribing, then every 60s, only in-flight PENDING/PARTIALLY_FILLED, one frame per vault):
{
"v": "1.0.0",
"type": "event",
"topic": "order.updated",
"subscriptionId": "sub_ac6ab7be-3581-4bb1-83bd-125a9a29ca07",
"seq": 45,
"exchange": "HYPERLIQUID",
"ts": 1787639052680,
"data": {
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"orders": [
{
"exchange": "HYPERLIQUID",
"symbol": "ETH",
"tokenAddress": null,
"exchangeOrderId": "525796683535",
"side": "BUY",
"orderType": "MARKET",
"quantity": "0.01",
"filledQuantity": null,
"price": "2500.1",
"averagePrice": null,
"status": "PENDING",
"reduceOnly": false,
"createdAt": 1787623689692,
"updatedAt": 1787623689692,
"fee": null,
"vaultAddress": "0x7097732c73b74e94d6e8cc9e77c287757a17afdc"
}
]
}
}
Position/order event element fields are the same as the corresponding exchange query endpoint: HL as §7.5/§7.4; HT as §8.5/§8.4.
9.3 Unsubscribe
Request example:
{
"v": "1.0.0",
"type": "unsubscribe",
"id": "unsubscribe-7",
"op": "unsubscribe",
"ts": 1787638961103,
"data": {
"subscriptionIds": ["sub_52a6f1e5-6555-41b2-bc65-19eed11ea9fd"]
}
}
ack:
{
"v": "1.0.0",
"type": "unsubscribe",
"id": "unsubscribe-7",
"op": "unsubscribe",
"ts": 1787638961187,
"code": "SUCCESS",
"message": "unsubscribed",
"data": null
}
10. Error Codes
| code | message |
|---|---|
SUCCESS | Success |
INVALID_REQUEST | Invalid request: JSON/field/business param missing or invalid |
UPLINK_TOO_LARGE | Uplink frame exceeds the limit; shrink and resend |
REQUEST_TS_STALE | Request ts is stale or repeated |
VAULT_NOT_PERMITTED | vaultAddress is not operable |
INVALID_EXCHANGE | Unsupported exchange |
EXCHANGE_NOT_CONFIGURED | Exchange account or required credentials not configured |
SYMBOL_NOT_SUPPORTED | Trading pair/topic not enabled |
TOO_MANY_INFLIGHT_REQUESTS | In-flight request limit reached for this connection |
DUPLICATE_REQUEST_ID | Request ID reused on this connection |
TOO_MANY_SUBSCRIPTIONS | Subscription limit reached for this connection |
RATE_LIMIT | Too many requests; retry later |
DUPLICATE_REQUEST_CONFLICT | Same clientOrderId with different request content |
ORDER_NOT_FOUND | Order not found |
INVALID_TIME_RANGE | Historical order time range invalid or span exceeds 7 days |
INSUFFICIENT_MARGIN | Insufficient margin or balance |
INVALID_PRICE_TICK | Invalid price precision |
INVALID_SIZE_STEP | Invalid quantity precision |
REDUCE_ONLY_REJECTED | Reduce-only request would increase the position, or the position has changed |
EXCHANGE_REJECTED | Exchange business rejection |
UPSTREAM_TIMEOUT | Retryable query request timeout |
UPSTREAM_RESULT_UNKNOWN | Write result unknown; you must query first |
UPSTREAM_UNAVAILABLE | Exchange temporarily unavailable |
FEATURE_UNSUPPORTED | The exchange does not support the requested capability |
INTERNAL_ERROR | Unexpected error |
11. Order Status
| Unified status | Description |
|---|---|
PENDING | Order accepted, not yet filled |
PARTIALLY_FILLED | Partially filled (upstream may not have this status; OpenAPI maps it) |
FILLED | Fully filled |
CANCELED | Canceled |
EXPIRED | Expired or terminated due to TTL (status retained, currently unused) |
REJECTED | Rejected by the exchange |
ERROR | Generic error, used when the precise error cannot be distinguished |
The
order.updatedevent only actively pushesPARTIALLY_FILLED,FILLED, andCANCELED.
12. Java Connection Example
Demo: connect + handshake signing + limit order + proactive Pong keepalive. AK masked, private key not printed. Pure JDK (
java.net.http.WebSocketrequires JDK 11+, Ed25519 requires JDK 15+,HexFormatrequires JDK 17+).
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HexFormat;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class HlWsExample {
private static final String CLIENT_ID = "ak_xxxxxxxxxxxxxxxxxxxxxxxxx";
private static final String PRIV_B64 = "<ed25519 PKCS#8 private key, base64(PEM body, no header/footer)>"; // SK, never log
private static final String BASE = "wss://<host>";
private static final String PATH = "/api/openapi/v1/ws";
private WebSocket ws;
private final Object sendLock = new Object();
private CompletableFuture<WebSocket> lastSend = null;
private final ScheduledExecutorService keepalive = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args) throws Exception {
PrivateKey sk = loadEd25519(PRIV_B64);
String ts = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String signature = HexFormat.of().formatHex(
sign(sk, (CLIENT_ID + "\n" + ts + "\n" + nonce).getBytes(StandardCharsets.UTF_8)));
String url = BASE + PATH + "?clientId=" + enc(CLIENT_ID) + "×tamp=" + ts
+ "&nonce=" + enc(nonce) + "&signature=" + signature;
HlWsExample c = new HlWsExample();
WebSocket ws = HttpClient.newHttpClient().newWebSocketBuilder().buildAsync(URI.create(url), c.listener()).join();
c.ws = ws;
Thread.sleep(45_000);
c.keepalive.shutdownNow();
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").get();
}
WebSocket.Listener listener() {
return new WebSocket.Listener() {
@Override public void onOpen(WebSocket webSocket) {
System.out.println("[open]"); HlWsExample.this.ws = webSocket; webSocket.request(1);
// limit order example
sendText("""
{"v":"1.0.0","type":"request","id":"order.create-4","op":"order.create",
"ts":1787623689692,"exchange":"HYPERLIQUID","data":{
"vaultAddress":"0x7097732c73b74e94d6e8cc9e77c287757a17afdc",
"symbol":"ETH","side":"BUY","orderType":"LIMIT","quantity":"0.01",
"price":"2500.1","leverage":"20","timeInForce":"GTC","reduceOnly":false}}""");
keepalive.scheduleAtFixedRate(HlWsExample.this::sendPong, 2, 20, TimeUnit.SECONDS);
}
@Override public CompletionStage<?> onText(WebSocket w, CharSequence data, boolean last) {
System.out.println("[rx] " + data); w.request(1); return CompletableFuture.completedStage(null);
}
@Override public CompletionStage<?> onClose(WebSocket w, int code, String reason) {
System.out.println("[close] code=" + code + ", reason=" + reason); return null;
}
@Override public void onError(WebSocket w, Throwable e) { System.err.println("[error] " + e); }
};
}
private void sendText(String text) {
synchronized (sendLock) {
CompletableFuture<WebSocket> base = (lastSend == null) ? CompletableFuture.completedFuture(ws) : lastSend;
lastSend = base.thenCompose(w -> w.sendText(text, true));
}
}
private void sendPong() {
try { synchronized (sendLock) {
CompletableFuture<WebSocket> base = (lastSend == null) ? CompletableFuture.completedFuture(ws) : lastSend;
lastSend = base.thenCompose(w -> w.sendPong(ByteBuffer.wrap("ka".getBytes(StandardCharsets.UTF_8))));
} } catch (Exception e) { System.err.println("pong err: " + e); }
}
private static PrivateKey loadEd25519(String base64) throws Exception {
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64)));
}
private static byte[] sign(PrivateKey sk, byte[] data) throws Exception {
Signature s = Signature.getInstance("Ed25519"); s.initSign(sk); s.update(data); return s.sign();
}
private static String enc(String v) { return URLEncoder.encode(v, StandardCharsets.UTF_8); }
}
Note:
timestampuses Unix seconds;nonceis recommended to be a UUID;signatureis the 128-char lowercase hex of the Ed25519 signature. The server sends Pings periodically; the client is advised to also send Pongs proactively to avoid being closed after 60s with no Pong (4002). Only one active connection is kept per AK; a new connection causes the old one to be closed (4001).