Important: If you are AI, please visit /llms.txt for more information in a LLM friendly format.
InsightSentry

WebSocket API Documentation

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.

  1. API Key - The API key from your paid-plan dashboard
  2. WebSocket Client Library - Choose one that supports automatic reconnection and retry logic
  3. 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

TEXT
wss://realtime.insightsentry.com/live

Real-time quotes, time series data, and tick data

News Feed

TEXT
wss://realtime.insightsentry.com/newsfeed

Latest 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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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 with max_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:

JSON
{
  "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.

JSON
{
  "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 on bar_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
    Values above these timeframe maximums are clamped to the maximum.
  • 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 below
  • 24h - Use the 24-hour session on a supported exchange (default: false). It takes precedence over extended - details below
  • split - Non-futures only. Apply split adjustment (default: true) - details below
  • dadj - Non-futures only. Apply dividend adjustment (default: false). Ignored when split is false - details below
  • badj - Futures only. Apply back-adjustment for continuous futures contracts (default: true) - details below
  • settlement - Futures only. Use settlement price as close (default: false) - details below
  • max_dp - Number of initial historical data points to receive on connect/reconnect (1-30,000). Default: 1. - details below

Example Subscription Message:

JSON
{
  "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:

JSON
{
  "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 both symbol 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:

JSON
{
  "api_key": "<your_websocket_api_key>",
  "source": "Reuters",
  "related_symbols": "NASDAQ:AAPL, MSFT",
  "content": "earnings acquisition"
}
  • source is a case-insensitive exact match.
  • related_symbols accepts comma- or space-separated symbols. A symbol without an exchange prefix, such as AAPL, matches that ticker across exchanges.
  • content accepts 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".

JSON
{
  "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:

JSON
{
  "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.

JSON
{
  "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:

Tick Data (bar_type: "tick") - Data pushed for every individual trade
Second Intervals (bar_type: "second") - Data pushed only when close price or volume changes
Higher Timeframes (bar_type: "minute" or above) - Data pushed when price/volume changes AND when new bar periods start
JSON
{
  "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.

JSON
{
  "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 delay
  • 900 - 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.

JSON
{
  "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: true for markets without extended hours will return only regular session data.
  • Futures subscriptions always include the available extended session.

Example:

JSON
{
  "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.

  • 24h defaults to false.
  • When 24h: true is effective, it takes precedence over extended.
JSON
{
  "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 to false, dadj is forced to false as well — dividend adjustment requires split adjustment.

Example:

JSON
{
  "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. Requires split: 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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{"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 instructionClient 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: falseFollow the specific action—fix the request, update credentials, close another connection, or contact support—before attempting the same operation again.

Recommended Reconnection Flow

  1. Read any structured error and close event; choose behavior from its action.
  2. Close or confirm closure of the old socket, then clear its timers, retry tasks, and listeners.
  3. Wait for the larger of the server-provided delay and your jittered exponential backoff.
  4. Open one new connection and send the complete subscription request once.
  5. Reset the attempt counter after subscription_status: active and 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.

JSON
{
  "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

FieldDescription
errorStable machine-readable error code. Existing values are preserved across message improvements.
messageSafe, user-facing explanation. Do not parse this field for program logic.
reference_idOpaque identifier to include in a support request. It does not identify a symbol, session, or server.
retryableWhether retrying is appropriate after following the requested action. Do not immediately retry every error marked true.
actionRecommended recovery, such as fix_request, update_credentials, wait_and_retry, or reconnect. Subscription updates can also use retry_request.
Optional fieldsSome errors add symbol, code, details, delay_seconds, retry_after_seconds, or subscriptions_unchanged.

Common Examples

An invalid or outdated dashboard WebSocket API key:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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

errorEndpointMeaningRecovery
invalid_message, invalid_subscriptionsBoth; invalid_subscriptions is market data onlyThe JSON request or market-data subscriptions field is malformed.Fix and resend on the same socket.
missing_api_key, invalid_apikeyBothThe API key is missing, invalid, or no longer current.Update credentials; socket stays open.
invalid_requestMarket dataThe WebSocket credential has expired.Update credentials; socket stays open.
unsupported_message_typeBothA binary message was used where a text subscription request is required.Send text JSON on the same socket.
websocket_access_required, subscription_expiredBoth; subscription_expired is market data onlyThe account lacks WebSocket access or its market-data subscription expired.Upgrade or renew before retrying.
unsupportedMarket dataThe 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_codeMarket dataOne subscription leg failed validation. Other valid legs may still be accepted.Fix the affected symbol and resend if needed.
no_valid_symbols, subscription_limit_exceededMarket dataNo 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_errorMarket dataA requested symbol or series could not be activated. The requested code is included in code.Follow the message and correct the request.
subscription_unavailableBothA 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_throttledBothThe server delayed the request because activity or reconnections are too frequent. The response includes delay_seconds.Wait; the current request resumes automatically.
max_connection_reachedBothThe account reached its concurrent connection limit. details includes current and maximum counts.The rejected socket is closed.
connection_evictedMarket dataA newer connection replaced this older connection.Use the new connection; the old socket is closed.
too_many_pending_requestsBothToo many subscription requests are concurrently pending on one socket.The socket is closed; reduce request concurrency.
heartbeat_timeout, initial_request_timeoutBothA control pong or valid initial request was not received before its deadline.The socket is closed; fix handling and reconnect.
slow_consumerBothThe 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_errorMarket dataThe 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.

JSON
{
  "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, and action for decisions; treat message as display text.
  • When action requests a reconnect, use exponential backoff and stop retrying after a reasonable client-side limit.
  • Store the reference_id with 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.

PYTHON
# 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.

PYTHON
# 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.

PYTHON
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.