1. Getting Started
Prerequisites
Before connecting to our WebSocket API, ensure you have the following:
Paid Direct Subscribers (via our website)
If you've subscribed to a paid plan directly through our website, you can use the same API key from your dashboard for both the REST API and WebSocket API. No separate WebSocket credential is required.
Free Website Plan
Free website API keys are for REST API access only. Upgrade to a paid website plan before connecting to the WebSocket endpoints.
- API Key - The API key from your paid-plan dashboard
- WebSocket Client Library - Choose one that supports automatic reconnection and retry logic
- Connection Settings - Configure appropriate timeouts and implement ping/pong for stability
Connecting to the Server
Our WebSocket API provides two specialized endpoints for different types of financial data.
Market Data
wss://realtime.insightsentry.com/liveReal-time quotes, time series data, and tick data
News Feed
wss://realtime.insightsentry.com/newsfeedLatest financial news and market updates
WebSocket Transport
Both endpoints use the standard HTTP/1.1 WebSocket Upgrade. Most WebSocket libraries handle the upgrade automatically. If a proxy sits between your client and the API, it must preserve the WebSocket upgrade.
Authentication
Both endpoints require an API key. If you subscribed to a paid plan directly via our website, use your dashboard API key. Free website API keys are REST-only. The authentication format varies by endpoint:
Market Data Authentication
Combine authentication with your subscription request (covered in the next section).
News Feed Authentication
Send only your API key in this simplified format:
{
"api_key": "<your_websocket_api_key>"
}News Feed Instant Access
After the request becomes active, /newsfeed replays up to 10 of the most recent matching news items, then streams new matching items. Optional filters are documented in Section 2.
Connection Lifecycle & Limits
Open the socket, then send the endpoint's authentication/subscription JSON within five minutes. While an accepted request is being connected, either endpoint can send:
{
"event": "connection_status",
"status": "connecting",
"message": "Connecting..."
}This is progress information, not proof that the subscription is ready. Wait for the following message before treating the request as active:
{
"event": "subscription_status",
"status": "active",
"operation": "initial",
"subscription_count": 2
}operation is initial for the first accepted request and update after a replacement. For market data, subscription_count counts active quote/series stream legs, so one both request counts as two. Newsfeed reports one active subscription.
- Initial request timeout: You must send a valid subscription message within 5 minutes of connecting. Idle connections that never authenticate are closed automatically.
- Connection limit: You can keep up to your plan's allowed concurrent connections open per account. Market data may replace an older local connection and send it
connection_evicted. Otherwise, including when the older connection is on another server, the new socket is rejected withmax_connection_reached. Newsfeed rejects the new socket when the limit is reached. See Section 6 for details.
2. Subscribing to Data Feeds
Market Data Subscription
After connecting to the market data endpoint, send a JSON message to authenticate and subscribe to data feeds.
This single message handles both authentication and your initial symbol subscriptions.
Required Message Format:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "quote"}
]
}Optional Adaptive Delivery
Add slow_consumer_policy: "adaptive" at the top level if preserving the connection with fresh, lower-frequency market data is preferable to receiving every intermediate update. Without it, or with "disconnect", the existing close-on-backlog behavior remains in effect.
{
"api_key": "<your_websocket_api_key>",
"slow_consumer_policy": "adaptive",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "quote"}
]
}Every accepted subscription message replaces the complete subscription set and selects the delivery policy again. Include slow_consumer_policy: "adaptive" on replacement requests to keep adaptive delivery enabled. A rejected replacement leaves both the previous subscriptions and their delivery policy unchanged.
Subscription Parameters
Each subscription object in the array can include these parameters:
Required Parameters
code- Symbol identifier (e.g.,"NASDAQ:AAPL","BINANCE:BTCUSDT")
Data Type Selection
type-"quote","series", or"both"
Be explicit: An omitted or unrecognized type currently behaves as "both" for compatibility. Always send a supported value so a typo cannot create an unintended quote and series subscription.
Series Data Parameters (when type="series")
An explicit type: "series" defaults to one-minute bars when bar settings are omitted. type: "both" defaults its series leg to one-day bars. Send bar_type and bar_interval explicitly when the timeframe matters.
bar_type- Time interval:"tick","second","minute","hour","day","week", or"month"bar_interval- Number of units per bar. Allowed values depend onbar_type:"second":1, 5, 10, 15, 30, 45"tick":1, 10, 100, 1000"minute": 1–1,440"hour": 1–24"day": 1–365;"week": 1–52;"month": 1–12
currency- Optional. Convert price values to the given currency (e.g.,"USD","EUR"). Unknown codes are rejected.unit- Optional. Override the instrument unit for display (e.g.,"BTC"). Unknown codes are rejected.extended- Non-futures only. Include extended hours (default: true) - details below24h- Use the 24-hour session on a supported exchange (default: false). It takes precedence overextended- details belowsplit- Non-futures only. Apply split adjustment (default: true) - details belowdadj- Non-futures only. Apply dividend adjustment (default: false). Ignored whensplitisfalse- details belowbadj- Futures only. Apply back-adjustment for continuous futures contracts (default: true) - details belowsettlement- Futures only. Use settlement price as close (default: false) - details belowmax_dp- Number of initial historical data points to receive on connect/reconnect (1-30,000). Default: 1. - details below
Example Subscription Message:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "max_dp": 100},
{"code": "NASDAQ:TSLA", "type": "quote"}
]
}Modifying Subscriptions
You can update your subscriptions without disconnecting from the WebSocket.
Send a new subscription message with your complete desired list. The server replaces all previous subscriptions with the new ones.
Complete Replacement
Each new subscription message completely replaces your current subscriptions. Include all symbols you want to continue receiving data for.
Example - Updating Subscriptions:
To change from the previous example to track both minute bars and quotes for AAPL:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "both", "bar_type": "minute", "bar_interval": 1, "max_dp": 100}
]
}NASDAQ:TSLA is removed because it is absent from the replacement. If the new streams cannot be activated, the server can keep the previous set active and return subscription_unavailable with subscriptions_unchanged: true. Follow that error's action; do not assume the update took effect.
Subscription Rules & Limits
Follow these important guidelines when managing your subscriptions:
Required Authentication
Every subscription message must include your valid api_key.
Rate Limiting
Avoid sending more than 120 subscription messages per 3 hours. The server does not drop your messages when you exceed this — instead, it adds small progressive delays to request processing. Frequent reconnections from the same account are throttled the same way to prevent reconnection storms.
Note: Throttling only affects new messages and reconnections you initiate. Your existing data feed continues uninterrupted, and data you receive has no rate limit.
Empty Subscriptions
You cannot send an empty subscriptions array. To stop all data, disconnect the WebSocket and reconnect when needed.
Multiple Symbols
Subscribe to multiple symbols in one message by adding multiple objects to the subscriptions array (subject to your plan limits).
How Symbol Quota Is Counted
- Each unique series-only symbol uses one plan unit.
- Up to 10 unique quote-only symbols use one plan unit.
- Each
bothsymbol uses one plan unit and activates two stream legs. - Duplicate stream requests are deduplicated before quota is calculated.
Send Updates Sequentially
Wait for subscription_status or an error before sending the next replacement. A socket permits at most eight pending data requests; exceeding that bound closes the socket with too_many_pending_requests.
Newsfeed Subscription & Filters
For all news, send only api_key. You may also filter by exact source, related symbol, and terms contained in the article content:
{
"api_key": "<your_websocket_api_key>",
"source": "Reuters",
"related_symbols": "NASDAQ:AAPL, MSFT",
"content": "earnings acquisition"
}sourceis a case-insensitive exact match.related_symbolsaccepts comma- or space-separated symbols. A symbol without an exchange prefix, such asAAPL, matches that ticker across exchanges.contentaccepts comma- or space-separated, case-insensitive terms; an item matches when its content contains at least one term.- Different filter fields are combined, so an item must satisfy every populated field.
Sending another valid newsfeed request replaces the filters, emits an operation: "update" status, and replays up to 10 recent items matching the new filters before continuing with live items.
3. Response Data Formats
The server sends real-time data updates as JSON messages. The format varies based on your subscription type.
For complete field descriptions, see the corresponding REST API documentation: /symbols/:symbol/series, /symbols/quotes, or /newsfeed.
Status Messages
The top-level event field identifies market-data updates and connection progress without conflicting with the nested trade-direction type field. Consider the request ready only after subscription_status reports status: "active".
{
"event": "subscription_status",
"status": "active",
"operation": "initial",
"subscription_count": 1
}A market-data request may emit validation errors for individual legs and then an active status for the valid remainder. Use subscription_count to confirm how many stream legs were accepted. If no valid legs remain, the server sends no_valid_symbols and does not send an active status for that request.
Dispatch successful market data using event: "quote_update" or event: "series_update". These discriminators are additive: all existing quote and series fields remain present, so clients that currently recognize messages by shape continue to work while migrating.
If adaptive delivery was requested and the client falls behind, the server sends a delivery status before the compacted market snapshot:
{
"event": "delivery_status",
"mode": "conflated",
"dropped_updates": 42,
"max_rate_hz": 1
}dropped_updates counts intermediate updates replaced in that delivery; it is not cumulative. max_rate_hz appears while delivery is rate-limited and can progress through 1, 2, and 4 Hz as the connection proves healthy. Sparse quote fields are merged, while live series delivery retains only the newest complete one-bar update for each series. A field omitted by that newest bar is not copied from an older update. Initial multi-bar history and unknown message shapes are not conflated and are delivered when bounded capacity remains. Status and error messages keep their order and receive reserved queue capacity. If a non-conflatable message cannot fit safely, the server still closes the socket as a slow consumer.
{
"event": "delivery_status",
"mode": "realtime",
"dropped_updates": 0
}The realtime status means normal full-frequency delivery has resumed. A brief isolated slowdown normally uses only a catch-up snapshot and returns immediately; repeated or sustained pressure uses the lower-frequency probe sequence.
Series Data (type: "series")
Series data provides OHLCV (Open, High, Low, Close, Volume) bar information and tick data.
Real-time Updates
Regardless of your chosen bar_type and bar_interval, you receive updates whenever the close price or volume changes within the current bar period.
Example: With bar_type: "hour", you get real-time updates throughout the hour, not just once per hour.
Update Frequency by Bar Type
Data delivery varies based on your bar_type subscription:
bar_type: "tick") - Data pushed for every individual tradebar_type: "second") - Data pushed only when close price or volume changesbar_type: "minute" or above) - Data pushed when price/volume changes AND when new bar periods start{
"event": "series_update",
"code": "NASDAQ:AAPL",
"bar_end": 1733432399.0,
"last_update": 1733432399820,
"bar_type": "1m",
"series": [
{
"time": 1733432340.0,
"open": 242.89,
"high": 243.09,
"low": 242.82,
"close": 243.08,
"volume": 533779.0
}
]
}Quote Data (type: "quote")
Quote data provides real-time market information including current prices, trading volume, and bid/ask spreads.
This data type is ideal for monitoring current market conditions and building trading interfaces.
Each WebSocket message is one direct quote object. Quote updates are sparse: fields that are not present in an update may be omitted, so merge updates by code when maintaining a current quote snapshot.
{
"event": "quote_update",
"code": "NASDAQ:AAPL",
"status": "PRE",
"lp_time": 1757061117.0,
"volume": 47549429.0,
"last_price": 239.42,
"change_percent": -0.15,
"change": -0.36,
"ask": 239.47,
"bid": 239.42,
"ask_size": 2.0,
"bid_size": 1.0,
"prev_close_price": 238.47,
"open_price": 238.45,
"low_price": 236.74,
"high_price": 239.8999,
"market_cap": 3558428648118.0,
"currency_code": "USD",
"delay_seconds": 0
}Understanding delay_seconds
The delay_seconds field indicates the data delay in seconds:
0- Real-time with no artificial delay900- Data is delayed by 900 seconds-1- End-of-day (EOD)
Newsfeed Data
News items are sent as direct JSON objects. published_at is Unix epoch time in seconds; the other fields are omitted when unavailable.
{
"link": "https://example.com/news/article",
"source": "Reuters",
"title": "Example company reports quarterly results",
"content": "The company reported its latest quarterly results...",
"published_at": 1757061265,
"related_symbols": ["NASDAQ:AAPL"]
}4. Additional Parameters
The WebSocket API supports several additional parameters for fine-tuning your data feeds and customizing the response format.
Extended Market Hours (extended)
The extended parameter applies to many US and Global stock markets.
extended: true(default) - Includes pre-market and after-hours trading data where available. For US equities, this covers 4:00 AM - 9:30 AM ET (pre-market) and 4:00 PM - 8:00 PM ET (after-hours).extended: false- Only includes regular trading hours data.- Note: Not all markets support extended hours trading. Setting
extended: truefor markets without extended hours will return only regular session data. - Futures subscriptions always include the available extended session.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "extended": false}
]
}24-Hour Session (24h)
Set 24h: true on quote, series, or both subscriptions to request the 24-hour session. Only NYSE, NASDAQ, AMEX, and CBOE are supported. Other exchanges ignore the parameter.
24hdefaults tofalse.- When
24h: trueis effective, it takes precedence overextended.
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "both", "24h": true}
]
}Split Adjustment (split)
For non-futures instruments, controls whether price history is adjusted for stock splits. This flag is ignored for futures symbols.
split: true(default) - Historical prices are adjusted for splits.split: false- Returns unadjusted prices. When set tofalse,dadjis forced tofalseas well — dividend adjustment requires split adjustment.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "day", "bar_interval": 1, "split": false}
]
}Dividend Adjustment (dadj)
For non-futures instruments, you can request dividend-adjusted price series. This flag is ignored for futures symbols.
dadj: true- Applies dividend adjustments to all price values. Requiressplit: true(the default).dadj: false(default) - Shows prices without dividend adjustment.
Note: If you send split: false together with dadj: true, the server forces dadj back to false.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "day", "bar_interval": 1, "dadj": true}
]
}Back-Adjustment (badj)
For continuous futures contracts (e.g., ES1!, NQ1!), you can request back-adjusted price series to remove price gaps between contract rollovers. This flag is ignored for non-futures symbols.
badj: true(default) - Applies back-adjustment to smooth price gaps at contract rollovers.badj: false- Shows unadjusted prices with natural contract rollover gaps.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "CME_MINI:ES1!", "type": "series", "bar_type": "day", "bar_interval": 1, "badj": false}
]
}Settlement Price (settlement)
For futures contracts, controls whether historical data uses settlement prices for the close value. This flag is ignored for non-futures symbols.
settlement: true- Uses settlement prices as the close price where applicable.settlement: false(default) - Uses the regular close price (last traded price).
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "CME_MINI:ES1!", "type": "series", "bar_type": "day", "bar_interval": 1, "settlement": true}
]
}Initial Data Points (max_dp)
Control how many historical data points you receive when first connecting or reconnecting to a symbol. The max_dp parameter accepts a number from 1 to 30,000. If not specified, defaults to 1 (only the current bar).
When you connect (or reconnect), the first series data message contains the most recent max_dp bars in the series property, followed by real-time updates with single bars.
- Plans with at least 10 symbol units: up to 30,000 data points.
- Other Plans: up to 1,000 data points.
- If you specify a value higher than your plan allows, it will be automatically clamped to your plan's maximum.
Example:
{
"api_key": "<your_websocket_api_key>",
"subscriptions": [
{"code": "NASDAQ:AAPL", "type": "series", "bar_type": "minute", "bar_interval": 1, "max_dp": 500}
]
}5. Maintaining Connection & Best Practices
Re-subscribe and Wait for Activation
On Every Open
Send the complete authentication/subscription request after each successful WebSocket open. Do not mark the connection ready merely because the upgrade or request validation succeeded. Wait for subscription_status with status: "active", and keep processing errors while activation is pending. Use a finite activation deadline (the example uses five minutes) so an open-but-never-active client cannot wait forever.
Control Ping/Pong
Both endpoints send a WebSocket control Ping every 10 seconds. Your client must return a control Pong before the next deadline or the server closes the socket with heartbeat_timeout. Mature WebSocket libraries reply automatically; keep that behavior enabled and avoid blocking the library's I/O loop.
Optional Application Ping
You may send the text message ping and expect the text response pong. This is optional and does not replace control Pong handling. If you use it for application-level checks, once every 20–30 seconds is sufficient.
Market-Data Server Time
After a /live subscription is active, the server sends a JSON timestamp about every 15 seconds. /newsfeed does not send this message.
Message format:
{"server_time": 1741397070281}The value is Unix epoch time in milliseconds. Use it to estimate delivery lag, allowing for clock skew. Do not use the absence of market updates as a liveness signal because markets can be inactive.
Read Continuously
Keep the socket read loop lightweight. Queue data for separate processing if parsing, storage, or callbacks may be slow. A client that stops reading can fill its bounded outbound queue and be closed with slow_consumer. Market-data clients that can tolerate skipped intermediate updates may opt into adaptive delivery, but must still read continuously: a non-writable socket or a non-conflatable overflow is closed.
Reconnection Best Practices
Network issues, server restarts, and transient errors will occasionally close your connection. How your client reacts determines whether your data flow recovers cleanly or whether you trigger server-side throttling.
Use Exponential Backoff with Jitter
Start near 500 ms, double after each consecutive failure, add random jitter, and cap the delay around 30 seconds. Reset the attempt counter only after the subscription is active and the connection remains healthy for a stability window. Set a finite retry budget, such as 10 attempts, then surface the failure to your application. Fast reconnect loops trigger progressive server-side delays and can make recovery slower.
| Server instruction | Client behavior |
|---|---|
action: "wait_and_retry" | Keep the socket open. For processing/reconnect throttles, the current request resumes automatically after delay_seconds. For an HTTP 429, wait at least Retry-After before opening a new socket. |
action: "retry_request" | Keep the socket and current subscriptions. Retry the replacement with bounded exponential backoff and a finite attempt limit; subscriptions_unchanged confirms the old set remains active. |
action: "reconnect" | Close or confirm closure of the old socket, then reconnect with backoff and send the complete subscription again. |
action: "reconnect_after_fixing_consumer" | Stop the reconnect loop. Restore or replace the stalled data consumer first, then reconnect with backoff. |
retryable: false | Follow the specific action—fix the request, update credentials, close another connection, or contact support—before attempting the same operation again. |
Recommended Reconnection Flow
- Read any structured error and close event; choose behavior from its action.
- Close or confirm closure of the old socket, then clear its timers, retry tasks, and listeners.
- Wait for the larger of the server-provided delay and your jittered exponential backoff.
- Open one new connection and send the complete subscription request once.
- Reset the attempt counter after
subscription_status: activeand a healthy stability window, not merely after the socket opens.
6. Error Handling
When possible, the server sends a JSON error before rejecting a request or closing the connection. Your client must still handle a close without an error message because the socket may already be unavailable.
WebSocket errors use one common envelope. Keep the established error value for program logic, follow action for recovery, and include the opaque reference_id when contacting support.
{
"error": "invalid_message",
"message": "The message must be a valid JSON subscription request.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "fix_request"
}Error Fields
| Field | Description |
|---|---|
error | Stable machine-readable error code. Existing values are preserved across message improvements. |
message | Safe, user-facing explanation. Do not parse this field for program logic. |
reference_id | Opaque identifier to include in a support request. It does not identify a symbol, session, or server. |
retryable | Whether retrying is appropriate after following the requested action. Do not immediately retry every error marked true. |
action | Recommended recovery, such as fix_request, update_credentials, wait_and_retry, or reconnect. Subscription updates can also use retry_request. |
| Optional fields | Some errors add symbol, code, details, delay_seconds, retry_after_seconds, or subscriptions_unchanged. |
Common Examples
An invalid or outdated dashboard WebSocket API key:
{
"error": "invalid_apikey",
"message": "The API key is invalid or no longer current. Check the current WebSocket API key in your dashboard.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "update_credentials"
}A temporary subscription failure that requires a reconnect:
{
"error": "subscription_unavailable",
"message": "One or more requested subscriptions are temporarily unavailable. Please reconnect. If this persists, contact support with the reference ID.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": true,
"action": "reconnect"
}A validation error can include the affected symbol while the socket stays open:
{
"error": "invalid_bar_interval",
"message": "Invalid bar_interval for second interval. Allowed values: 1, 5, 10, 15, 30, 45",
"symbol": "NASDAQ:AAPL",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": false,
"action": "fix_request"
}Error Codes
error | Endpoint | Meaning | Recovery |
|---|---|---|---|
invalid_message, invalid_subscriptions | Both; invalid_subscriptions is market data only | The JSON request or market-data subscriptions field is malformed. | Fix and resend on the same socket. |
missing_api_key, invalid_apikey | Both | The API key is missing, invalid, or no longer current. | Update credentials; socket stays open. |
invalid_request | Market data | The WebSocket credential has expired. | Update credentials; socket stays open. |
unsupported_message_type | Both | A binary message was used where a text subscription request is required. | Send text JSON on the same socket. |
websocket_access_required, subscription_expired | Both; subscription_expired is market data only | The account lacks WebSocket access or its market-data subscription expired. | Upgrade or renew before retrying. |
unsupported | Market data | The requested subscription source is not supported by this endpoint. | Fix the request; socket stays open. |
invalid_symbol, invalid_bar_interval, invalid_currency_code, invalid_unit_code | Market data | One subscription leg failed validation. Other valid legs may still be accepted. | Fix the affected symbol and resend if needed. |
no_valid_symbols, subscription_limit_exceeded | Market data | No valid symbols remain or the request exceeds the plan limit. Limit errors add requested and maximum counts under details. | Fix the request; socket stays open. |
quote_error, series_error | Market data | A requested symbol or series could not be activated. The requested code is included in code. | Follow the message and correct the request. |
subscription_unavailable | Both | A requested or active subscription could not be maintained. An update may add subscriptions_unchanged: true. | Follow action; the server may keep an unchanged update open or close and request a reconnect. |
processing_throttled, reconnect_throttled | Both | The server delayed the request because activity or reconnections are too frequent. The response includes delay_seconds. | Wait; the current request resumes automatically. |
max_connection_reached | Both | The account reached its concurrent connection limit. details includes current and maximum counts. | The rejected socket is closed. |
connection_evicted | Market data | A newer connection replaced this older connection. | Use the new connection; the old socket is closed. |
too_many_pending_requests | Both | Too many subscription requests are concurrently pending on one socket. | The socket is closed; reduce request concurrency. |
heartbeat_timeout, initial_request_timeout | Both | A control pong or valid initial request was not received before its deadline. | The socket is closed; fix handling and reconnect. |
slow_consumer | Both | The client did not read realtime messages quickly enough. Adaptive market-data delivery could not safely compact the remaining backlog, or was not enabled. | The socket is closed; read continuously first. |
internal_error | Market data | The request task could not complete. | Do not retry blindly. Contact support with the reference ID if it persists. |
HTTP 429 Before Upgrade
Excessive connection attempts can be rejected before the WebSocket upgrade. In that case, handle the HTTP 429 Too Many Requests response and its Retry-After header instead of waiting for a WebSocket message.
{
"error": "rate_limited",
"message": "Too many connection attempts were received from this network. Wait 2 seconds before trying again and review your reconnect logic.",
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"retryable": true,
"action": "wait_and_retry",
"retry_after_seconds": 2
}- Use
error,retryable, andactionfor decisions; treatmessageas display text. - When
actionrequests a reconnect, use exponential backoff and stop retrying after a reasonable client-side limit. - Store the
reference_idwith your own request context for support diagnostics.
7. Code Examples
Start with the short example to verify your connection and message handling. For a long-running client, use the advanced example's bounded processing queue, activation deadline, structured error handling, and jittered reconnect limit. The websockets library responds to server control Pings automatically.
Before You Start
Install websockets>=14 and set INSIGHTSENTRY_WEBSOCKET_API_KEY to your current WebSocket API key. Paid subscribers use the API key shown in their website dashboard.
Quick Start
This learning example connects once, waits for activation, and keeps reading messages. It logs the first data message's summary; add your application processing after choosing an appropriate queue and backpressure policy. Use the advanced example for automatic recovery.
# pip install "websockets>=14"
import asyncio
import json
import logging
import os
import websockets
URI = "wss://realtime.insightsentry.com/live"
ACTIVATION_TIMEOUT_SECONDS = 5 * 60
REQUEST = {
"api_key": os.environ["INSIGHTSENTRY_WEBSOCKET_API_KEY"],
"subscriptions": [
{
"code": "NASDAQ:AAPL",
"type": "both",
"bar_type": "minute",
"bar_interval": 1,
"max_dp": 100,
},
],
}
def message_summary(message):
if message.get("event") == "series_update":
return f"series update for {message.get('code', 'unknown symbol')}"
if "published_at" in message:
return "news item"
if message.get("event") == "quote_update":
return f"quote update for {message.get('code', 'unknown symbol')}"
return None
async def main():
activated = False
reported_first_data = False
activation_deadline = asyncio.get_running_loop().time() + ACTIVATION_TIMEOUT_SECONDS
async with websockets.connect(
URI,
open_timeout=10,
close_timeout=5,
) as websocket:
await websocket.send(json.dumps(REQUEST))
while True:
timeout = None
if not activated:
timeout = max(
0.0,
activation_deadline - asyncio.get_running_loop().time(),
)
try:
raw_message = await asyncio.wait_for(websocket.recv(), timeout=timeout)
except asyncio.TimeoutError as error:
raise RuntimeError("Subscription did not become active in time") from error
if raw_message == "pong":
continue
try:
message = json.loads(raw_message)
except json.JSONDecodeError:
logging.warning("Ignored an unexpected non-JSON WebSocket message")
continue
if error_code := message.get("error"):
logging.warning(
"WebSocket error=%s action=%s reference_id=%s",
error_code,
message.get("action"),
message.get("reference_id"),
)
if message.get("action") == "wait_and_retry":
continue
return
if (
message.get("event") == "subscription_status"
and message.get("status") == "active"
):
activated = True
logging.info("Subscription active")
continue
summary = message_summary(message)
if summary is not None and not reported_first_data:
logging.info("Received %s", summary)
reported_first_data = True
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())Advanced Reconnect Client
This version keeps socket reads separate from application work. Its queue is deliberately bounded: if the consumer falls behind, the client stops instead of reconnecting into the same unresolved problem. Keep processing asynchronous or offload blocking calls so they cannot stall the socket task. The example also retains reference IDs from both WebSocket errors and HTTP upgrade failures.
# pip install "websockets>=14"
import asyncio
import json
import logging
import os
import random
import time
import websockets
from websockets.exceptions import ConnectionClosed, InvalidStatus
URI = "wss://realtime.insightsentry.com/live"
API_KEY = os.environ["INSIGHTSENTRY_WEBSOCKET_API_KEY"]
REQUEST = {
"api_key": API_KEY,
"subscriptions": [
{
"code": "NASDAQ:AAPL",
"type": "both",
"bar_type": "minute",
"bar_interval": 1,
"max_dp": 100,
},
],
}
MAX_ATTEMPTS = 10
BASE_DELAY_SECONDS = 0.5
MAX_DELAY_SECONDS = 30.0
STABLE_AFTER_SECONDS = 60.0
ACTIVATION_TIMEOUT_SECONDS = 5 * 60
DATA_QUEUE_CAPACITY = 1024
PARTIAL_VALIDATION_ERRORS = {
"invalid_symbol",
"invalid_bar_interval",
"invalid_currency_code",
"invalid_unit_code",
}
class StopRetrying(Exception):
pass
class ReconnectRequested(Exception):
def __init__(self, delay_seconds: float, stable: bool):
self.delay_seconds = delay_seconds
self.stable = stable
def number(value, default=0.0):
try:
return max(0.0, float(value))
except (TypeError, ValueError):
return default
def is_stable(activated_at):
return activated_at is not None and time.monotonic() - activated_at >= STABLE_AFTER_SECONDS
async def resend_after(websocket, delay_seconds):
await asyncio.sleep(delay_seconds)
await websocket.send(json.dumps(REQUEST))
def enqueue_data(data_queue, message):
if (
message.get("event") not in ("quote_update", "series_update")
and "published_at" not in message
):
return
try:
data_queue.put_nowait(message)
except asyncio.QueueFull as error:
raise StopRetrying(
"local data queue is full; fix the consumer before reconnecting"
) from error
async def consume_data(data_queue):
processed_messages = 0
while True:
message = await data_queue.get()
try:
# Replace this counter with your storage or processing call.
# Await async work here, or use asyncio.to_thread for blocking work.
processed_messages += 1
if processed_messages == 1 or processed_messages % 1000 == 0:
logging.info("Processed %d data messages", processed_messages)
finally:
data_queue.task_done()
async def connect_once(data_queue):
activated_at = None
retry_task = None
request_retry_attempt = 0
request_needs_fix = False
activation_deadline = time.monotonic() + ACTIVATION_TIMEOUT_SECONDS
async with websockets.connect(
URI,
open_timeout=10,
close_timeout=5,
) as websocket:
await websocket.send(json.dumps(REQUEST))
try:
while True:
timeout = None
if activated_at is None:
timeout = max(0.0, activation_deadline - time.monotonic())
try:
raw_message = await asyncio.wait_for(websocket.recv(), timeout=timeout)
except asyncio.TimeoutError:
if request_needs_fix:
raise StopRetrying("activation failed because the request must be fixed")
raise ReconnectRequested(0.0, False)
if raw_message == "pong":
continue
try:
message = json.loads(raw_message)
except json.JSONDecodeError:
logging.warning("Ignored an unexpected non-JSON WebSocket message")
continue
if error_code := message.get("error"):
action = message.get("action")
reference_id = message.get("reference_id")
logging.warning(
"WebSocket error=%s action=%s reference_id=%s",
error_code,
action,
reference_id,
)
if action == "wait_and_retry":
# The current upgraded request resumes automatically.
continue
if action == "retry_request":
request_retry_attempt += 1
if request_retry_attempt >= MAX_ATTEMPTS:
raise StopRetrying(
f"request retry limit reached; reference_id={reference_id}"
)
if retry_task is None or retry_task.done():
if retry_task is not None:
await asyncio.gather(retry_task, return_exceptions=True)
cap = min(
BASE_DELAY_SECONDS * (2 ** (request_retry_attempt - 1)),
MAX_DELAY_SECONDS,
)
delay = max(
number(message.get("delay_seconds")),
random.uniform(cap / 2, cap),
)
retry_task = asyncio.create_task(resend_after(websocket, delay))
continue
if action == "reconnect":
delay = number(message.get("retry_after_seconds"))
raise ReconnectRequested(delay, is_stable(activated_at))
if action == "reconnect_after_fixing_consumer":
raise StopRetrying(
"consumer must be fixed before reconnecting; "
f"reference_id={reference_id}"
)
if action == "fix_request" and error_code in PARTIAL_VALIDATION_ERRORS:
# Other valid legs may still produce an active status.
request_needs_fix = True
continue
if not message.get("retryable", False):
raise StopRetrying(
f"{error_code}; reference_id={reference_id}; action={action}"
)
raise StopRetrying(
f"unsupported retry action={action}; reference_id={reference_id}"
)
if (
message.get("event") == "subscription_status"
and message.get("status") == "active"
):
if activated_at is None:
activated_at = time.monotonic()
if retry_task is not None:
retry_task.cancel()
await asyncio.gather(retry_task, return_exceptions=True)
retry_task = None
request_retry_attempt = 0
request_needs_fix = False
logging.info(
"Subscription active (%s)",
message.get("operation", "unknown"),
)
continue
enqueue_data(data_queue, message)
except ConnectionClosed as closed:
received_close = closed.rcvd
logging.info(
"WebSocket closed code=%s reason=%s",
received_close.code if received_close is not None else None,
received_close.reason if received_close is not None else None,
)
finally:
if retry_task is not None:
retry_task.cancel()
await asyncio.gather(retry_task, return_exceptions=True)
return is_stable(activated_at)
def http_status(error):
response = getattr(error, "response", None)
return getattr(response, "status_code", None)
def http_error_payload(error):
response = getattr(error, "response", None)
body = getattr(response, "body", None)
if not body:
return {}
try:
payload = json.loads(body)
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
return {}
return payload if isinstance(payload, dict) else {}
def retry_after(error):
response = getattr(error, "response", None)
headers = getattr(response, "headers", {}) or {}
return number(headers.get("Retry-After"))
async def run_connections(data_queue):
attempt = 0
while attempt < MAX_ATTEMPTS:
stable = False
server_delay = 0.0
try:
stable = await connect_once(data_queue)
except StopRetrying:
raise
except ReconnectRequested as requested:
stable = requested.stable
server_delay = requested.delay_seconds
except InvalidStatus as error:
status = http_status(error)
payload = http_error_payload(error)
reference_id = payload.get("reference_id")
logging.warning(
"WebSocket upgrade rejected HTTP %s error=%s reference_id=%s",
status,
payload.get("error"),
reference_id,
)
if status != 429:
raise StopRetrying(
f"WebSocket upgrade failed with HTTP {status}; "
f"reference_id={reference_id}"
)
server_delay = max(
retry_after(error),
number(payload.get("retry_after_seconds")),
)
except Exception:
logging.exception("WebSocket connection failed")
if stable:
attempt = 0
attempt += 1
if attempt >= MAX_ATTEMPTS:
break
cap = min(BASE_DELAY_SECONDS * (2 ** (attempt - 1)), MAX_DELAY_SECONDS)
client_delay = random.uniform(cap / 2, cap)
delay = max(server_delay, client_delay)
logging.info("Reconnecting in %.2f seconds (attempt %d)", delay, attempt + 1)
await asyncio.sleep(delay)
raise RuntimeError("WebSocket reconnect limit reached")
async def run():
data_queue = asyncio.Queue(maxsize=DATA_QUEUE_CAPACITY)
consumer_task = asyncio.create_task(consume_data(data_queue))
connection_task = asyncio.create_task(run_connections(data_queue))
try:
done, _ = await asyncio.wait(
{consumer_task, connection_task},
return_when=asyncio.FIRST_COMPLETED,
)
if connection_task in done:
await connection_task
raise RuntimeError("Connection loop stopped unexpectedly")
await consumer_task
raise RuntimeError("Data consumer stopped unexpectedly")
finally:
for task in (consumer_task, connection_task):
task.cancel()
await asyncio.gather(consumer_task, connection_task, return_exceptions=True)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(run())Using the Same Loop for Newsfeed
Replace the URI and request in either example with the values below. News items have a published_at field instead of a market-data code field.
URI = "wss://realtime.insightsentry.com/newsfeed"
REQUEST = {
"api_key": API_KEY,
"related_symbols": "NASDAQ:AAPL, MSFT",
"content": "earnings acquisition",
}8. FAQ
Q: Why does my WebSocket disconnect frequently?
Check the structured error first. heartbeat_timeout usually means the client's I/O loop could not send a control Pong; slow_consumer means it did not read messages fast enough. Keep I/O separate from heavy processing. If your market-data application prefers fresh snapshots over every intermediate update, enable slow_consumer_policy: "adaptive" and handle delivery_status events. On /live, a consistently large positive difference between your current epoch time in milliseconds and server_time can indicate delayed client handling, network delay, or clock skew. A negative difference usually indicates clock skew, not a blocked thread.
Q: The request passed validation, so why am I receiving no data?
Validation is not activation. Wait for subscription_status with status: "active" and handle any error received first. Once active, quiet markets, unchanged quotes, or restrictive newsfeed filters can legitimately produce no data for a period. If activation never arrives and the socket closes without an error payload, reconnect with backoff and retain the close details for diagnosis.
Q: Why are some one-second bars missing?
A one-second bar is sent only when its close price or volume changes. No message is produced for an unchanged second. If your application requires a continuous timeline, fill missing intervals from the previous bar according to your own normalization rules.