API
Streaming
Live prices, GEX and chains over one WebSocket — tickets, the auth frame, subscriptions, control frames, and reconnecting correctly.
The streaming API is one WebSocket. You exchange your key for a short-lived ticket, open the socket, authenticate with the ticket in the first frame, then subscribe to topics. Control messages are JSON text frames; data arrives as binary protobuf Frame messages, defined in ws-protocol.proto.
Streaming is not included on the free tier: minting a ticket returns 403 not_entitled_tier.
1. Mint a ticket
curl -sS -X POST "https://api.itmatrixhq.com/v2/stream/ticket" \
-H "Authorization: Bearer $ITM_API_KEY"{ "data": { "ticket": "eyJ…", "exp": 1790257013559 }, "meta": {} }exp is epoch milliseconds. A ticket authorizes one connection handshake and carries your entitlements, so mint a fresh one for every new connection. It does not need renewing while the connection is healthy.
2. Connect and authenticate
Open wss://api.itmatrixhq.com/v2/ws. The HTTP upgrade takes no Authorization header; authentication happens in-band. Send this as the first frame:
{ "op": "auth", "ticket": "eyJ…", "proto": 3, "encoding": "protobuf" }The server answers {"op": "authed"}. Anything else first, or a bad ticket, closes the connection with invalid_ticket.
3. Subscribe
{ "op": "sub", "id": "gex-spy", "topic": "gex", "symbol": "SPY" }id is yours; the server echoes it. It replies {"op": "ack", "id": "gex-spy", "effective": {…}} with the subscription as it will actually be served, or {"op": "nack", "id": "gex-spy", "code": "…", "message": "…"} with a code from the error set. Entitlement is re-checked on every subscription.
| Topic | Data frames | Options |
|---|---|---|
stocks | trade, mid, bar_open, bar_close, quote | want: any of trades, mid, bars, quote; timeframe |
spot | spot | — |
gex | gex_snap, then gex_delta | expiries, by_expiry |
chain | chain_snap, then chain_delta | expiry, strike_gte, strike_lte (integer thousandths) |
To stop one: {"op": "unsub", "id": "gex-spy"}.
Each connection caps how many subscriptions and distinct symbols it may hold; a new one past the cap is refused with nack code quota_exceeded.
4. Data frames
Every binary message is exactly one protobuf Frame: the symbol plus one payload (trade, spot, gex_snap, …). Prices in frames are scaled integers — divide *_scaled fields by 10,000 for dollars. Timestamps are epoch milliseconds; strikes are integer thousandths.
5. Control frames
| Server sends | You do |
|---|---|
{"op": "ping", "t": 42} | Reply {"op": "pong", "t": 42} with the same t. |
{"op": "bye", "reconnect_after_ms": 2000} | The server is closing this connection. Reconnect after the given delay. |
{"op": "nack", …} | That subscription is gone; decide whether to retry it. |
6. Reconnecting
When the socket closes unexpectedly:
- Wait, then reconnect. Start at about 250 ms and double each failed attempt up to about 10 s; reset the delay after a successful
authed. If the server sentbyewithreconnect_after_ms, wait that long instead. - Mint a new ticket for the new connection.
- After
authed, re-send every active subscription.
Do not tear down a healthy connection because its handshake ticket's exp has passed — the ticket only matters at the handshake.
If your consumer falls behind, do not silently drop frames. The SDKs keep a bounded queue per subscription (64 frames by default) and raise an error when it overflows, so data loss is always explicit.
A complete client
Everything above in one program: mint a ticket, authenticate, subscribe, answer pings. Reconnecting is left out to keep it short; add it with the rules in Reconnecting. The SDK version of each tab uses the Python or TypeScript SDK, which owns all of this for you: one shared connection, tickets, pings, reconnects and resubscription. Python streaming through the SDK needs the extra: pip install "itmatrix[stream]".
import asyncio
import json
import os
import httpx
import websockets # pip install websockets
async def main():
ticket = httpx.post(
"https://api.itmatrixhq.com/v2/stream/ticket",
headers={"Authorization": f"Bearer {os.environ['ITM_API_KEY']}"},
).json()["data"]["ticket"]
async with websockets.connect("wss://api.itmatrixhq.com/v2/ws") as ws:
await ws.send(json.dumps({"op": "auth", "ticket": ticket, "proto": 3, "encoding": "protobuf"}))
await ws.send(json.dumps({"op": "sub", "id": "gex-spy", "topic": "gex", "symbol": "SPY"}))
async for message in ws:
if isinstance(message, bytes):
print("frame:", len(message), "bytes") # one protobuf Frame
continue
control = json.loads(message) # authed, ack, nack, ping, bye
if control["op"] == "ping":
await ws.send(json.dumps({"op": "pong", "t": control["t"]}))
else:
print(control)
asyncio.run(main())import asyncio
import os
import itmatrix as itm
async def main():
async with itm.AsyncITMClient(api_key=os.environ["ITM_API_KEY"]) as client:
async with client.stream.gex("SPY") as updates:
async for frame in updates:
print(frame.symbol, frame.WhichOneof("payload"))
asyncio.run(main())// Node 22+ and browsers have WebSocket built in.
const minted = await fetch("https://api.itmatrixhq.com/v2/stream/ticket", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ITM_API_KEY}` },
});
const { data } = await minted.json();
const ws = new WebSocket("wss://api.itmatrixhq.com/v2/ws");
ws.binaryType = "arraybuffer";
ws.onopen = () => {
ws.send(JSON.stringify({ op: "auth", ticket: data.ticket, proto: 3, encoding: "protobuf" }));
ws.send(JSON.stringify({ op: "sub", id: "gex-spy", topic: "gex", symbol: "SPY" }));
};
ws.onmessage = ({ data: message }) => {
if (typeof message !== "string") {
console.log("frame:", message.byteLength, "bytes"); // one protobuf Frame
return;
}
const control = JSON.parse(message); // authed, ack, nack, ping, bye
if (control.op === "ping") ws.send(JSON.stringify({ op: "pong", t: control.t }));
else console.log(control);
};import { ITMClient } from "@itmatrixhq/core";
const client = new ITMClient({ apiKey: process.env.ITM_API_KEY });
for await (const frame of client.stream.gex("SPY")) {
console.log(frame.symbol, frame.gexSnap?.netGex ?? frame.gexDelta);
}Frames arrive as binary protobuf Frame messages; decode them with bindings generated from ws-protocol.proto.
In the SDKs, subscriptions are async iterators. Closing one (leaving the async with, or break out of the loop) unsubscribes it, and the socket closes when the last subscription does.