Skip to main content

WebSocket API

Real-time data streaming for Hypercall options trading.

Interactive Reference

Check out the Interactive WebSocket API Reference for a better browsing experience with live examples and schema details.

Machine-Readable Spec

Download the AsyncAPI specification for programmatic use.

Additive schema evolution

WebSocket message objects may gain new fields without advance notice. Clients must ignore fields they do not recognize. Hypercall does not remove fields from an existing message schema.

Connection​

Connect to wss://HOST/ws:

Endpoints:

  • Production: wss://api.hypercall.xyz/ws
  • Local: ws://localhost:3000/ws
Testnet status

Testnet is temporarily disabled until Hypercall acquires more testnet HYPE.

Compression​

/ws supports RFC 7692 permessage-deflate. Enable it in your client to cut market-data bandwidth: on captured production traffic, independently compressed frames are about 45% smaller, and index price batches about 70% smaller.

  • Your client offers the extension in the upgrade request, for example Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits.
  • When the server accepts, the 101 response carries Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover. Each server message is then compressed on its own, without a shared sliding window, and messages under a few hundred bytes are still sent uncompressed. Your library decompresses transparently.
  • If the response has no Sec-WebSocket-Extensions header, every frame is uncompressed. Compression is enabled per environment, so clients must handle both; the JSON messages are identical either way.
  • Your client may compress what it sends. The server inflates each client message up to 256 KiB and closes the connection if a message would exceed that.

Browsers negotiate the extension automatically. Most libraries need no change:

Python (websockets enables compression by default):

import websockets

async with websockets.connect("wss://api.hypercall.xyz/ws", compression="deflate") as ws:
...

Node.js (ws):

import WebSocket from "ws";

const ws = new WebSocket("wss://api.hypercall.xyz/ws", { perMessageDeflate: true });

Rust: the hypercall-client WsClient negotiates permessage-deflate by default (see Example: Rust Client). Plain tungstenite and tokio-tungstenite do not implement it yet, so they connect uncompressed. For compression with your own socket, use a client that implements it, such as yawc:

let url = "wss://api.hypercall.xyz/ws".parse()?;
let mut ws = yawc::WebSocket::connect(url)
.with_options(yawc::Options::default().with_low_latency_compression())
.await?;

Wallet Identification​

To receive data on authenticated channels (orders, fills, portfolio), identify your wallet after connecting by sending an Authenticate message:

{"type": "Authenticate", "wallet": "0x1234..."}

The server responds with confirmation:

{"type": "Authenticated", "wallet": "0x1234..."}

After receiving Authenticated, you can subscribe to authenticated channels. If the wallet address is invalid, the server responds with an Error message and the connection remains open.

Deprecated: Query Parameter Authentication

The ?wallet= query parameter is still supported for backward compatibility but is deprecated and will be removed in a future release. Prefer the message-based approach above.

Connection Liveness​

The server enforces a WebSocket heartbeat:

  • Sends a Ping control frame every 20 seconds
  • Expects a matching Pong within 60 seconds
  • Closes the connection with close code 1008 and reason pong timeout if the client stops answering

Browser WebSocket implementations handle ping/pong automatically. Many Rust websocket libraries, including tungstenite and tokio-tungstenite, also handle control-frame ping/pong for you. Check your client's library documentation before adding manual Pong handling. Custom or raw socket implementations must reply to Ping frames with Pong.

Slow Consumer Recovery​

The server closes a /ws connection that cannot drain outbound data within the configured message, encoded-byte, queue-age, or socket-write safety ceiling. When the connection can still accept a close frame, the server uses code 1008 and a compact JSON reason:

{"error":"slow_consumer","class":"ordered_public","cause":"message_age","recovery":"snapshot_resubscribe"}

The reason fields are:

FieldMeaning
classDelivery class whose frame crossed the safety boundary.
causemessage_limit, byte_limit, message_age, or write_timeout.
recoveryRequired next action, such as resubscribe, snapshot_resubscribe, portfolio_refetch, or rest_reconcile.

After any disconnect, reconnect, identify the wallet again when required, resubscribe, and reconcile current state before processing new events. Ordered public channels require a fresh snapshot. Private event channels require reconciliation through the authoritative REST surface because cursor replay is not available yet. A fully stalled connection may terminate before it can read the close reason, so clients must use this recovery flow for an unclean close too.

Use separate connections for high-rate public market data and authenticated commands or private streams. Delivery classes select metrics and recovery behavior, but frames on one connection still share one ordered socket write path. A stalled public write can therefore delay later private frames on that same connection until the write deadline closes it.

Subscribing to Channels​

Send a JSON message to subscribe:

{"type": "Subscribe", "channel": "orderbook"}

To unsubscribe:

{"type": "Unsubscribe", "channel": "orderbook"}

You'll receive a confirmation:

{"type": "Subscribed", "channel": "orderbook"}

Symbol Filtering​

The order_updates and fills channels support an optional symbols filter. When provided, the server only sends messages whose underlying matches one of the specified symbols.

{"type": "Subscribe", "channel": "order_updates", "symbols": ["BTC"]}

Both bare underlyings ("BTC") and full instrument names ("BTC-20260131-100000-C") are accepted. To add more symbols, send another Subscribe. To remove specific symbols:

{"type": "Unsubscribe", "channel": "order_updates", "symbols": ["BTC"]}

When no symbols are specified, all updates for your wallet are forwarded.

Options Chain Filtering​

The options_chain channel supports filtering by underlying symbols, expiry date, and option type:

{
"type": "Subscribe",
"channel": "options_chain",
"symbols": ["BTC-20260131-100000-C"],
"expiry": "2026-01-31",
"option_type": "call"
}
FilterValuesDefault
symbolsArray of full instrument symbols (e.g., ["BTC-20260131-100000-C"])All instruments
expiryDate string "YYYY-MM-DD"All expiries
option_type"call", "put", or omit for bothBoth

Batched Delivery​

indicative_market_data_v2 carries the same rows as indicative_market_data, several rows per frame:

{"type": "Subscribe", "channel": "indicative_market_data_v2", "symbols": ["BTC-20260131-100000-C"]}

The server confirms with a batch marker:

{"type": "Subscribed", "channel": "indicative_market_data_v2", "batch": true}

It then holds each row for at most one batch window, 25 ms by default, and sends the rows published in that window as one IndicativeMarketDataBatch frame. A batch is also sent early once it reaches 256 rows.

  • Detecting support: only a confirmation with "batch": true means the server batches. Servers released before this channel confirm any channel name without the marker and then send nothing, and current servers answer an unknown channel with an unknown_channel error. In either case, unsubscribe from indicative_market_data_v2 and subscribe to indicative_market_data on the same connection.
  • Latency: a row arrives up to one window later than on indicative_market_data. On production traffic with the default window, rows waited 12 ms on average and never more than 25 ms. Use the v1 channel if you act on individual quote updates within milliseconds.
  • Bandwidth: batching alone saves about 7% of bytes. The large saving comes with permessage-deflate: compressed batches are about 9.8 times smaller than uncompressed one-row frames for an unfiltered subscription, and about 8.3 times smaller for one options chain, against about 1.8 times for rows compressed one by one.
  • Order: rows in updates are in publish order, and batches arrive in order. Rows are not conflated, so one instrument can appear more than once in a batch. Apply the rows in order and you reach the same state as on the v1 channel.
  • Snapshot: the subscribe-time snapshot also arrives as batch frames.
  • Filters: symbols works as on indicative_market_data. Narrowing the filter or unsubscribing first sends any rows already pending under the old filter, then the Unsubscribed confirmation, so no row is lost or duplicated.
  • Independent channels: a connection can subscribe to both channels with different filters. indicative_market_data is unchanged.

Available Channels​

ChannelAuth RequiredDescription
orderbookNoL2 orderbook updates for all symbols
tradesNoPublic trade feed
market_updatesNoMarket listing changes (created/deleted/expired)
options_chainNoIncremental options chain updates (filterable by symbols, expiry, option_type)
index_pricesNoReal-time spot/index prices for all underlyings
indicative_market_dataNoAllowlisted quote-provider stream. Not generally available yet
indicative_market_data_v2NoThe same stream in batch frames. See Batched Delivery
order_updatesYesYour order status changes (filterable by symbol)
fillsYesYour trade fills (filterable by symbol)
portfolioYesYour position and balance updates
liquidationYesYour liquidation state changes
rfqYesRFQ quotes, status updates, and fill notifications

Competition and leaderboard streams are not available on this WebSocket. Use the HTTP endpoints GET /competitions, GET /competitions/:id, GET /competitions/leaderboard, and GET /competitions/all-time/leaderboard instead.

Message Types​

Place Order (Authenticated)​

Place an order through the WebSocket command path.

{
"type": "PlaceOrder",
"wallet": "0x1234...",
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"size": "1",
"price": "100",
"tif": "gtc",
"route": "book_only",
"client_id": "my-order-1",
"nonce": 1780000000000,
"signature": "0x..."
}

The nonce above is illustrative. Generate a fresh one for every order from the current Unix time in milliseconds, for example Date.now() in JavaScript. A copied fixed value falls outside the accepted window and is rejected.

FieldTypeDescription
walletstringWallet address that owns the order
symbolstringOption symbol
sidestring"Buy" or "Sell"
sizestringContract size, exactly matching the signed value
pricestringLimit price, exactly matching the signed value
tifstringOptional time-in-force, defaults to "gtc"
routestringOptional route. Use "book_only" for route-aware WebSocket orders. Omitted route remains accepted until at least July 4, 2026.
client_idstringOptional client order ID
nonceintegerRequired signing nonce: the current Unix time in milliseconds. It must fall within two days before and one day after the current time. Each nonce is accepted once per signer, and REST and WebSocket orders share the same nonce space. A replayed frame is rejected.
signaturestringEIP-712 PlaceOrder signature

WebSocket PlaceOrder currently dispatches directly to the orderbook. route="best_execution" and route="rfq_only" are rejected on WebSocket because this path does not run RPI/RFQ routing yet. Use POST /order for best_execution.

Orderbook Update​

L2 orderbook snapshot/update for a symbol.

{
"type": "OrderbookUpdate",
"symbol": "BTC-20260131-100000-C",
"bids": [["95000.5", "10.5"], ["94999.0", "25.0"]],
"asks": [["95001.0", "8.0"], ["95002.5", "15.0"]],
"timestamp": 1737331200000
}
FieldTypeDescription
symbolstringOption symbol
bidsarrayBid levels as [price, size] tuples, size is in human-readable contracts
asksarrayAsk levels as [price, size] tuples, size is in human-readable contracts
timestampintegerUnix timestamp (milliseconds)

Trade​

Public trade event.

{
"type": "Trade",
"symbol": "BTC-20260131-100000-C",
"price": "0.0523",
"size": "5.0",
"side": "buy",
"timestamp": 1737331200000
}
FieldTypeDescription
symbolstringOption symbol
pricestringTrade price in USD
sizestringTrade size in contracts
sidestringAggressor side (buy or sell)
timestampintegerUnix timestamp (milliseconds)

Fill (Authenticated)​

Your trade fill notification.

{
"type": "Fill",
"order_id": 12345,
"fill_id": 67890,
"symbol": "BTC-20260131-100000-C",
"side": "buy",
"price": "0.0523",
"size": "5.0",
"timestamp": 1737331200000,
"wallet_address": "0x1234...abcd",
"fee": "0",
"trade_id": 99999,
"is_taker": true
}
FieldTypeDescription
order_idintegerYour order ID
fill_idintegerFill ID
symbolstringOption symbol
sidestringTrade side (buy or sell)
pricestringFill price in USD
sizestringFill size in contracts
timestampintegerUnix timestamp (milliseconds)
wallet_addressstringYour wallet address
feestringTrading fee charged. Returns 0 while launch venue fees are disabled
trade_idintegerUnique trade ID
is_takerbooleanWhether you were the taker
builder_code_addressstring?Builder code wallet (if any)
builder_code_feestring?Builder code fee. Returns null while launch venue fees are disabled

Portfolio Update (Authenticated)​

Portfolio stream update for positions, balances, margin, and Greeks.

Greeks update example:

{
"type": "PortfolioUpdate",
"timestamp": 1737331200000,
"per_leg": [
{
"symbol": "BTC-20260131-100000-C",
"quantity": "2.0",
"delta": 0.91,
"gamma": 0.003,
"theta": -0.12,
"vega": 0.44,
"iv": 0.63
}
],
"aggregate": {
"delta": 0.91,
"gamma": 0.003,
"theta": -0.12,
"vega": 0.44,
"iv": 0.63
}
}

For empty portfolios, Greeks updates use:

  • per_leg: []
  • aggregate: null

Order Update (Authenticated)​

Order status change notification. PARTIALLY_FILLED updates are included. filled_size is the cumulative quantity filled across the order's executions, not the quantity of the latest fill.

{
"type": "OrderUpdate",
"order_id": 12345,
"request": {
"price": "0.0523",
"size": "10.0",
"symbol": "BTC-20260131-100000-C",
"side": "Buy",
"tif": "gtc"
},
"status": "PARTIALLY_FILLED",
"filled_size": "0.75",
"timestamp": 1767225600000,
"reason": null,
"wallet_address": "0x0000000000000000000000000000000000000001",
"instrument_type": "option"
}

Market Update​

Market listing changes.

Market Created:

{
"type": "MarketUpdate",
"action": "Created",
"symbol": "BTC-20260131-100000-C",
"strike": "100000",
"is_call": true,
"underlying": "BTC",
"expiry": 1738281600,
"timestamp": 1737331200000
}

Market Expired:

{
"type": "MarketUpdate",
"action": "Expired",
"symbol": "BTC-20260131-100000-C",
"strike": "100000",
"is_call": true,
"underlying": "BTC",
"expiry": 1738281600,
"timestamp": 1738281600000
}

Position Expired (Authenticated)​

Notification when your position settles at expiry.

{
"type": "PositionExpired",
"wallet_address": "0x1234...abcd",
"symbol": "BTC-20260131-100000-C",
"position_size": "10.0",
"settlement_price": "105000",
"settlement_value": "500.0",
"timestamp": 1738281600000
}

Liquidation State Change (Authenticated)​

Your account liquidation state change.

{
"type": "LiquidationStateChange",
"wallet_address": "0x1234...abcd",
"previous_state": "Normal",
"new_state": "Warning",
"equity": "10000.0",
"mm_required": "9500.0",
"shortfall": "0",
"auction_id": null,
"timestamp": 1737331200000
}
StateDescription
NormalAccount is healthy
WarningApproaching margin call
LiquidatingLiquidation auction active

Index Price Update​

Batched spot/index prices for all underlyings.

{
"type": "IndexPriceUpdate",
"prices": [
{"underlying": "BTC", "price": "97250.50", "timestamp": 1737331200000},
{"underlying": "ETH", "price": "3200.00", "timestamp": 1737331199980},
{"underlying": "HYPE", "price": "28.50", "timestamp": 1737331199950}
],
"timestamp": 1737331199950
}
FieldTypeDescription
pricesarrayArray of {underlying, price, timestamp} entries for each tracked underlying
prices[].underlyingstringUnderlying symbol (e.g., "BTC", "ETH")
prices[].pricestringCurrent spot/index price in USD
prices[].timestampintegerSource observation time for this underlying, as Unix milliseconds
timestampintegerOldest source observation time included in prices, as Unix milliseconds

Indicative Market Data​

Allowlisted quote-provider stream with aggregated best bid/ask from registered quote providers. This channel is not generally available yet. Use REST market data and authenticated order/fill/portfolio channels unless Hypercall has enabled quote-provider streaming for your integration.

{
"type": "IndicativeMarketData",
"instrument": "BTC-20260131-100000-C",
"best_bid": "0.0520",
"best_ask": "0.0530",
"indicative_bid_size": "50.0",
"indicative_ask_size": "25.0",
"num_providers": 3,
"timestamp": 1737331200000
}
FieldTypeDescription
instrumentstringOption symbol
best_bidstringOptional best aggregated bid price
best_askstringOptional best aggregated ask price
bid_ivnumberOptional implied volatility of best bid
ask_ivnumberOptional implied volatility of best ask
indicative_bid_sizestringOptional total bid size across providers
indicative_ask_sizestringOptional total ask size across providers
num_providersintegerNumber of active quote providers
timestampintegerUnix timestamp (milliseconds)

Indicative Market Data Batch​

Sent on the indicative_market_data_v2 channel (see Batched Delivery). Each element of updates has the same fields as an IndicativeMarketData message, without type.

{
"type": "IndicativeMarketDataBatch",
"flushed_at": 1737331200025,
"updates": [
{
"instrument": "BTC-20260131-100000-C",
"best_bid": "0.0520",
"best_ask": "0.0530",
"num_providers": 3,
"published_at": 1737331200004,
"timestamp": 1737331200000
},
{
"instrument": "BTC-20260131-105000-C",
"best_bid": "0.0310",
"best_ask": "0.0322",
"num_providers": 2,
"published_at": 1737331200004,
"timestamp": 1737331200001
}
]
}
FieldTypeDescription
flushed_atintegerAPI-clock time the batch was sent, Unix milliseconds. flushed_at - published_at is the batching delay of a row
updatesarrayRows published during the window, oldest first

RFQ Quotes (Authenticated)​

Quotes received in response to your RFQ submission.

{
"type": "RfqQuotes",
"rfq_id": "550e8400-e29b-41d4-a716-446655440000",
"quotes": [
{
"quote_id": "660e8400-e29b-41d4-a716-446655440001",
"net_premium": "52.30",
"expires_at": 1737331225000
}
],
"status": "quoted",
"taker_wallet": "0x1234...abcd"
}

RFQ Status Update (Authenticated)​

Status change for an RFQ you submitted.

{
"type": "RfqStatusUpdate",
"rfq_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "executed",
"taker_wallet": "0x1234...abcd"
}

Error​

Server error message. Match on code when present, not on message.

{
"type": "Error",
"message": "Unknown channel 'foobar'",
"code": "unknown_channel",
"channel": "foobar"
}
FieldTypeDescription
messagestringHuman-readable description
codestringOptional stable reason. unknown_channel: a Subscribe named a channel that does not exist
channelstringOptional. The channel a rejected Subscribe named

A Subscribe to a channel that is not listed in Available Channels is answered with the unknown_channel error, and no subscription is created. Earlier servers confirmed any name and then sent nothing.

Authentication​

Authenticated channels require a wallet identification message after connecting:

{"type": "Authenticate", "wallet": "0x1234567890abcdef..."}

Messages on authenticated channels are filtered to only show data for your wallet. No signature is required for WebSocket connections.

Example: Python Client​

import asyncio
import websockets
import json

async def main():
uri = "wss://api.hypercall.xyz/ws"

async with websockets.connect(uri) as ws:
# Identify the wallet before subscribing to authenticated channels.
await ws.send(json.dumps({
"type": "Authenticate",
"wallet": "0xYourWallet"
}))

# Subscribe to orderbook
await ws.send(json.dumps({
"type": "Subscribe",
"channel": "orderbook"
}))

# Subscribe to fills for BTC only
await ws.send(json.dumps({
"type": "Subscribe",
"channel": "fills",
"symbols": ["BTC"]
}))

# Listen for messages
async for message in ws:
data = json.loads(message)
print(f"Received: {data['type']}")

asyncio.run(main())

Example: TypeScript Client​

const ws = new WebSocket("wss://api.hypercall.xyz/ws");

ws.onopen = () => {
ws.send(JSON.stringify({ type: "Authenticate", wallet: "0xYourWallet" }));

// Subscribe to channels
ws.send(JSON.stringify({ type: "Subscribe", channel: "orderbook" }));

// Subscribe to order updates filtered to BTC
ws.send(JSON.stringify({
type: "Subscribe",
channel: "order_updates",
symbols: ["BTC"],
}));
};

ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(`Received: ${msg.type}`);

if (msg.type === "OrderbookUpdate") {
console.log(`${msg.symbol}: ${msg.bids.length} bids, ${msg.asks.length} asks`);
}
};

Example: Rust Client​

The hypercall-client crate's WsClient identifies the wallet, restores subscriptions after a reconnect, and answers server pings for you. It offers permessage-deflate compression by default and falls back to uncompressed frames when the server does not accept it, with the same JSON messages either way. Call with_permessage_deflate(false) to stop offering it.

use hypercall_client::WsClient;

#[tokio::main]
async fn main() -> hypercall_client::Result<()> {
let ws = WsClient::new();
ws.connect("https://api.hypercall.xyz", None).await?;
ws.subscribe(vec!["orderbook", "index_prices"]).await?;

if let Some(msg) = ws.wait_for_message(|_| true, 5_000).await {
println!("Received: {msg:?}");
}
Ok(())
}