websocket real-time feed
Provides real-time, low-latency market data via Azure Web PubSub WebSocket streams.
Provides real-time, low-latency market data via binary WebSocket streams.
Extended Python SDK & Samples →
1. Negotiation (POST /negotiate)
Negotiate WebSocket connections by providing the analytics groups and spot price channels your application requires. A POST request with no spot groups returns current-generation hub URLs for compatibility.
Request
Headers
| Name | Type | Description |
|---|---|---|
Authorization | string | Required. Format: "Bearer <YOUR_API_KEY>" |
Content-Type | string | Required. Set to "application/json". |
User-Agent | string | Required. Identify your client application. |
Accept | string | Required. Set to "application/json". |
Request Body
| Field | Type | Description |
|---|---|---|
groups | array[string] | Required. List of unprefixed group names. Include one {TICKER}_spot group for each analytics ticker. |
Explicit-expiry groups
An explicit-expiry group ends with an 8-digit YYYYMMDD date, for example SOXL_state_gamma_20260717. The rules:
- The date must have 8 digits.
- The date must be a listed expiry for the ticker. Read the list from
GET /options/{ticker}/expiries. A date that is not listed returns400withNo valid option expiry exists for {ticker} on {date}. - The date must be between today and 90 days ahead, in Eastern Time. A date outside that range returns
400withExpiry websocket groups are only available from YYYY-MM-DD through YYYY-MM-DD. - Synthetic futures tickers (
ES_SPX,NQ_NDX) have no explicit-expiry groups. A request for one returns400withExpiry websocket groups are not available for synthetic futures tickers.
{
"groups": [
"SPX_classic_gex_full",
"SPX_state_gex_full",
"SPX_spot",
"NQ_NDX_state_gex_full",
"NQ_NDX_spot",
"SOXL_state_gamma_20260717",
"SOXL_spot"
]
}Response
| Field | Type | Description |
|---|---|---|
websocket_urls | object | An object containing the WebSocket connection URLs with embedded authentication tokens. |
websocket_urls.v2_classic | string | Connection URL for the Classic GEX hub. |
websocket_urls.v2_state_gex | string | Connection URL for the State GEX Profile hub. |
websocket_urls.v2_state_greeks_zero | string | Connection URL for the 0DTE State Greeks hub. |
websocket_urls.v2_state_greeks | string | Connection URL for the explicit-expiration State Greeks hub (YYYYMMDD). |
websocket_urls.v2_state_greeks_one | string | Connection URL for the 1DTE+ State Greeks hub. |
websocket_urls.v2_orderflow | string | Connection URL for the Orderflow hub. |
{
"websocket_urls": {
"v2_classic": "wss://ws.gex.bot:443/client/hubs/v2_classic?access_token=<...>",
"v2_state_gex": "wss://ws.gex.bot:443/client/hubs/v2_state_gex?access_token=<...>",
"v2_state_greeks_zero": "wss://ws.gex.bot:443/client/hubs/v2_state_greeks_zero?access_token=<...>",
"v2_state_greeks": "wss://ws.gex.bot:443/client/hubs/v2_state_greeks?access_token=<...>",
"v2_state_greeks_one": "wss://ws.gex.bot:443/client/hubs/v2_state_greeks_one?access_token=<...>",
"v2_orderflow": "wss://ws.gex.bot:443/client/hubs/v2_orderflow?access_token=<...>"
}
}{
"type": "object",
"properties": {
"websocket_urls": {
"type": "object",
"properties": {
"v2_classic": { "type": "string", "format": "uri" },
"v2_state_gex": { "type": "string", "format": "uri" },
"v2_state_greeks_zero": { "type": "string", "format": "uri" },
"v2_state_greeks": { "type": "string", "format": "uri" },
"v2_state_greeks_one": { "type": "string", "format": "uri" },
"v2_orderflow": { "type": "string", "format": "uri" }
}
}
}
}2. Dynamic Subscription Updates (PATCH /negotiate)
Use PATCH /negotiate to replace all active memberships for the current API WebSocket session.
The call updates subscriptions without reconnecting.
Any omitted membership is removed immediately.
Prerequisites
The client must maintain an active connection to each hub specified in the request. Call POST /negotiate first to obtain signed URLs for new hubs. A PATCH that names a hub with no live connection returns 409 Conflict with No active websocket connection for: <hub>. No membership changes. PATCH never closes a connection.
Request
Headers
| Name | Type | Description |
|---|---|---|
Authorization | string | Required. Format: "Bearer <YOUR_API_KEY>" |
Content-Type | string | Required. Set to "application/json". |
User-Agent | string | Required. Identify your client application. |
Accept | string | Required. Set to "application/json". |
Request Body
| Field | Type | Description |
|---|---|---|
groups | array[object] | Required. List of hub-and-group objects. Repeat spot memberships for each hub streaming analytics for that ticker. An empty array removes every membership and needs no live connection. |
{
"groups": [
{ "hub": "v2_classic", "group": "SPX_classic_gex_full" },
{ "hub": "v2_classic", "group": "SPX_spot" },
{ "hub": "v2_state_greeks_zero", "group": "SPX_state_gamma_zero" },
{ "hub": "v2_state_greeks_zero", "group": "SPX_spot" },
{ "hub": "v2_orderflow", "group": "ES_SPX_orderflow_orderflow" },
{ "hub": "v2_orderflow", "group": "ES_SPX_spot" }
]
}Response
| Field | Type | Description |
|---|---|---|
updated_groups | integer | Total count of active group memberships across all hubs. |
hubs | object | Key-value mapping of hub names to active group membership counts. The keys are the hubs of the generation the request selected. |
{
"updated_groups": 6,
"hubs": {
"v2_classic": 2,
"v2_state_gex": 0,
"v2_state_greeks_zero": 2,
"v2_state_greeks": 0,
"v2_state_greeks_one": 0,
"v2_orderflow": 2
}
}{
"type": "object",
"properties": {
"updated_groups": { "type": "integer" },
"hubs": {
"type": "object",
"properties": {
"v2_classic": { "type": "integer" },
"v2_state_gex": { "type": "integer" },
"v2_state_greeks_zero": { "type": "integer" },
"v2_state_greeks": { "type": "integer" },
"v2_state_greeks_one": { "type": "integer" },
"v2_orderflow": { "type": "integer" }
}
}
}
}3. Legacy GET Compatibility (GET /negotiate)
GET /negotiate returns current-generation hub URLs and a group prefix for backward compatibility.
Deprecated for Custom Quant
New custom Quant integrations must use POST and PATCH. Do not create new clients with GET /negotiate. Each GET response carries Deprecation: true and a Warning: 299 header.
Example Response
{
"websocket_urls": {
"classic": "wss://ws.gex.bot:443/client/hubs/classic?access_token=<...>",
"state_gex": "wss://ws.gex.bot:443/client/hubs/state_gex?access_token=<...>",
"state_greeks_zero": "wss://ws.gex.bot:443/client/hubs/state_greeks_zero?access_token=<...>",
"state_greeks": "wss://ws.gex.bot:443/client/hubs/state_greeks?access_token=<...>",
"state_greeks_one": "wss://ws.gex.bot:443/client/hubs/state_greeks_one?access_token=<...>",
"orderflow": "wss://ws.gex.bot:443/client/hubs/orderflow?access_token=<...>"
},
"prefix": "blue"
}Clients send manual joinGroup messages after connecting to legacy hubs. Prefix the group name with the returned prefix (for example, blue_SPX_orderflow_orderflow).
Wire Format & Protobuf Decoding
Incoming binary WebSocket frames use Zstandard (zstd) compression and Protocol Buffers (protobuf) serialization.
Message Envelope
Each binary frame is a google.protobuf.Any. The value is Zstandard-compressed. Match on the suffix of the type URL to select the decoder:
| Type URL contains | Message | Groups |
|---|---|---|
proto.gex | gex_profile.Gex | Classic and State GEX groups |
proto.greek | option_profile.OptionProfile | State delta, gamma, vanna and charm groups |
proto.orderflow | orderflow_proto.Orderflow | Orderflow groups |
proto.spot | spot_price.SpotPrice | Spot groups |
Spot Price Messages
Spot messages arrive in a google.protobuf.Any envelope.
The type URL is:
proto.spotThe decompressed payload contains a SpotPrice Protocol Buffer message:
syntax = "proto3";
package spot_price;
message SpotPrice {
int64 timestamp = 1;
string ticker = 2;
uint32 spot = 3;
optional int64 source_timestamp_ms = 4;
}- Wire Scaling: The wire
spotinteger is multiplied by 100. Divide it by 100 to calculate decimal spot price. - Timestamp Matching: Analytics and spot messages for one calculation share the same ticker and timestamp. Cache either message briefly if your application combines them.
Protobuf Schemas & Scaling Factors
SpotPrice(spot_price.proto, type URLproto.spot):spot: Divide integer value by100to obtain decimal price.
Gex(gex.proto, type URLproto.gex):spot,zero_gamma,major_pos_vol,major_pos_oi,major_neg_vol,major_neg_oi, eachstrikes[].strike_price,value_1,value_2andpriors.values[], andmax_priors.tuples[].first_value: Divide by100.sum_gex_vol,sum_gex_oi,delta_risk_reversalandmax_priors.tuples[].second_value: Divide by1000.
OptionProfile(option_profile.proto, type URLproto.greek):- Strike-by-strike IV quotes and classified trade volumes.
Orderflow(orderflow.proto, type URLproto.orderflow):- Sub-second timestamped Net GEX, Aggregate DEX, and Net Convexity ticks.
Connection Guidelines
Best Practices
- Server-Side Joining: In the V2 architecture, groups passed in
POST /negotiateare joined automatically on the server. Do not send client-sidejoinGroupmessages. - Multi-Hub Spot Expansion: The server expands each
{ticker}_spotgroup to every hub streaming analytics for that ticker. - Group Limits: Standard Quant API keys allow 150 active hub-and-group memberships by default. Exceeding the limit returns
403 Forbidden. Server-expanded spot memberships count toward this limit. - Token Expiry: Authorized URLs expire after 15 minutes if not connected.
- Keepalive & Heartbeats: The server sends WebSocket ping frames every 30 seconds. Clients must respond with standard pong frames.
- Connection Lifetime: Active connections can remain open throughout the trading day. Edge network timeouts or proxy interruptions can cause disconnects.
- One session per slot: All custom API keys of one account share one WebSocket session slot across every hub. Official platform-integration keys (ATAS, Bookmap, NinjaTrader, MotiveWave, Quantower, Sierra Chart, TradingView) share a separate integration slot, so one custom-script session and one platform-integration session can run at the same time. A new
POSTorGET /negotiatecloses the connections of that slot on every hub, sends the text messageSESSION_LIMIT_REACHED, and then returns new URLs. A second negotiate call with any custom key of the same account closes the first session.PATCHnever closes a connection. Run one process that negotiates per account. Unexplained disconnects usually mean two processes negotiate with keys of the same account. - Recommended Reconnect Pattern: Use exponential backoff when reconnecting (5s, 10s, 20s, up to 60s max). Track elapsed time using monotonic timestamps. Avoid tight reconnection loops on network drops.
- Market Hours: Live updates stream during regular US market hours (9:30 AM–4:00 PM ET).
Status Codes
Errors that the API raises have the body {"error": "<message>"}. An invalid API key returns 401 with no body. A missing Authorization or User-Agent header returns 400.
| Status | Meaning |
|---|---|
400 Bad Request | Missing Authorization or User-Agent header, invalid group name, missing spot coverage, or an expiry rule error. |
401 Unauthorized | Invalid API key. No {"error": ...} body. |
403 Forbidden | Subscription, permission or group-limit denial. |
409 Conflict | PATCH names a hub with no live connection. |
429 Too Many Requests | The account exceeded a quota configured for this endpoint. Body: {"error": "Rate limit exceeded."}. No default quota applies to /negotiate. |
503 Service Unavailable | The option expiry universe could not be read. |