Skip to content

WebSocket Stream

The WebSocket Stream endpoint delivers news articles in real time over a single full-duplex connection. Instead of polling the API repeatedly, open one WebSocket, pass your filters, and receive matching articles automatically as APITube indexes them.

What "real time" means here: articles are pushed as soon as APITube finishes ingesting them, in indexing order — not at the exact moment their publisher released them. See Delivery Order and Freshness for how this affects article age.

What is WebSocket Stream?

WebSocket Stream is a real-time news delivery channel built on the WebSocket protocol. You open one persistent connection, and the server pushes new articles to you as they appear. Unlike SSE Stream (one-way HTTP), WebSocket is full-duplex — the same connection carries a lightweight message protocol both ways, so the client can send keep-alive pings and the server tags every frame with a type.

Use cases:

  • Live news feeds and trading/monitoring dashboards
  • Real-time monitoring for specific topics, entities, or sentiment
  • Breaking-news alerts
  • Streaming pipelines for data processing

Benefits:

  • Low-latency delivery — articles are pushed as soon as they are indexed
  • Typed message protocol — every frame is a JSON object with a type field (like Finnhub / Alpaca)
  • Connection acknowledgement — the server confirms the connection and the accepted subscription up front
  • Full-duplex keep-alive — client ping / server pong, plus server heartbeats
  • All /everything filters supported — language, category, sentiment, entities, and more
  • Field selection with the fl parameter to reduce payload size
  • takeover option to reclaim a slot after a restart without rotating your key

Endpoint

http
GET wss://api.apitube.io/v1/news/ws

One connection = one set of filters. To subscribe to different filter sets, open separate connections.

Authentication

Pass your API key in one of two ways:

  • Header (recommended): X-API-Key: YOUR_API_KEY
  • Query parameter: ?api_key=YOUR_API_KEY

Without a valid API key the connection is closed with WebSocket close code 1008 (Unauthorized). A scope-restricted key must include the websocket scope.

Quick Start

wscat (command line)

bash
npx wscat -c "wss://api.apitube.io/v1/news/ws?language.code=en&api_key=YOUR_API_KEY"

You will first receive connected and subscribed acknowledgement frames, then article frames as matching news is indexed.

JavaScript (Browser)

javascript
const ws = new WebSocket(
    'wss://api.apitube.io/v1/news/ws?api_key=YOUR_API_KEY&language.code=en'
);

ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);

    if (msg.type === 'error') {
        console.error(msg.code, msg.message);
        return;
    }

    if (msg.type === 'article') {
        const article = msg.data;
        console.log(article.title, article.source?.domain);
    }
};

ws.onclose = (event) => {
    console.log('Disconnected:', event.code, event.reason);
    // Reconnect with exponential backoff (see below)
};

Node.js

javascript
import WebSocket from 'ws';

const ws = new WebSocket('wss://api.apitube.io/v1/news/ws?language.code=en', {
    headers: { 'X-API-Key': 'YOUR_API_KEY' }
});

ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());

    if (msg.type === 'error') {
        console.error(msg.code, msg.message);
        return;
    }

    if (msg.type === 'article') {
        console.log(msg.data.title);
    }
});

ws.on('close', (code, reason) => {
    console.log('Closed:', code, reason.toString());
});

Python

python
import json
import websocket

def on_message(ws, message):
    msg = json.loads(message)

    if msg.get("type") == "error":
        print(f"Error: {msg['code']} - {msg['message']}")
        return

    if msg.get("type") == "article":
        article = msg["data"]
        print(f"{article['title']}{article.get('source', {}).get('domain')}")

ws = websocket.WebSocketApp(
    "wss://api.apitube.io/v1/news/ws?language.code=en",
    header={"X-API-Key": "YOUR_API_KEY"},
    on_message=on_message,
)

ws.run_forever()

Message Format

Every message is a JSON object with a type field. Read type first, then handle the frame.

Server → Client

Right after a successful connection, the server sends two acknowledgement frames:

json
{"type":"connected","session_id":"req-a1b2c3","timestamp":1718800000000}
{"type":"subscribed","filters":{"language.code":"en"},"timestamp":1718800000000}
  • connected — the connection is established and authenticated. session_id identifies this session.
  • subscribed — your subscription is accepted. filters echoes the query parameters you connected with (your API key is not echoed back).

Then, as articles are indexed, each one arrives in an article envelope:

json
{"type":"article","data":{"id":123456,"title":"...","source":{"domain":"reuters.com"},"published_at":"2026-07-03T...","language":{"code":"en"}}}
  • data — the full article JSON, in the same format as the /everything endpoint (or only the fields you requested with fl).

Control frames:

FrameWhen
{"type":"heartbeat","timestamp":...}Keep-alive, sent every 30 seconds
{"type":"pong","timestamp":...}Reply to a client ping
{"type":"error","code":"...","message":"..."}An error occurred (balance, limits, key revoked); the connection then closes

Client → Server

The client can send an application-level ping to keep the connection marked as alive and get a pong back:

json
{ "action": "ping" }

Any inbound message counts as a liveness signal. Messages other than ping are ignored (they do not change your subscription — filters are fixed for the life of the connection).

Filtering

All filters available on the /everything endpoint work with WebSocket Stream, passed as query parameters at connection time:

By language:

wss://api.apitube.io/v1/news/ws?language.code=en&api_key=YOUR_API_KEY

By category:

wss://api.apitube.io/v1/news/ws?category.id=medtop:13000000&api_key=YOUR_API_KEY

By source:

wss://api.apitube.io/v1/news/ws?source.domain=theverge.com&api_key=YOUR_API_KEY

Multiple filters:

wss://api.apitube.io/v1/news/ws?language.code=en&category.id=medtop:13000000&sentiment.overall.polarity=positive&api_key=YOUR_API_KEY

For the full list of available filters, see Parameters.

Field Selection

Use the fl parameter to receive only the fields you need, reducing payload size. The article frame's data object will then contain only the specified fields:

wss://api.apitube.io/v1/news/ws?language.code=en&fl=id,title,source.domain&api_key=YOUR_API_KEY

Delivery Order and Freshness

Articles are streamed in the order APITube indexes them, ordered by an internal ingestion ID — not by their published_at timestamp. "Real time" means as soon as APITube discovers and processes an article, which is not necessarily the moment its publisher released it.

  • published_at is the publisher's original timestamp, read from the article's own metadata. When a source provides no valid date, published_at falls back to the time APITube processed the article.
  • Delivery latency for a given article is the time APITube needs to discover it (feed/sitemap polling), fetch it, and run parsing and enrichment. This varies by source: frequently-polled feeds arrive within minutes, while slow-to-discover sources can lag much longer. An article can therefore reach the stream with a published_at already some time in the past.

To keep only recently-published articles, add a published_at.start filter. It is applied inside the stream and drops older articles before they are delivered — and therefore before they are billed. To bias the stream toward higher-authority sources, filter by source rank with source.rank.opr.min (Open PageRank). The stream cannot re-order articles by priority — ordering is always by indexing sequence — but it can filter out low-rank sources. See Parameters for both filters.

Keep-Alive

Two mechanisms keep the connection healthy:

  • Server heartbeat — every 30 seconds the server sends a {"type":"heartbeat"} frame and a protocol-level WebSocket ping. Standard clients (the browser WebSocket API and the Node ws library) reply to the protocol ping automatically.
  • Client ping — you may send {"action":"ping"} at any time and the server replies with {"type":"pong"}.

A client that stops responding to pings (a dead or half-open socket) is detected within one or two heartbeat intervals and its connection is closed, freeing the slot — even on a silent stream with no matching articles.

Reconnection

WebSocket Stream does not resume from a cursor — unlike SSE Stream, there is no Last-Event-ID equivalent. A fresh connection starts from the newest matching article forward, so articles indexed during a disconnect gap are not replayed. Implement reconnection on the client with exponential backoff:

javascript
let reconnectDelay = 1000;

function connect() {
    const ws = new WebSocket('wss://api.apitube.io/v1/news/ws?language.code=en&api_key=YOUR_API_KEY');

    ws.onopen = () => {
        reconnectDelay = 1000; // reset on success
    };

    ws.onclose = () => {
        setTimeout(connect, reconnectDelay);
        reconnectDelay = Math.min(reconnectDelay * 2, 30000);
    };

    ws.onmessage = (event) => {
        const msg = JSON.parse(event.data);
        if (msg.type === 'article') console.log(msg.data.title);
    };
}

connect();

If you must not miss articles across a gap, backfill with a normal /everything request for the missed window, then resume the stream.

Parameters

ParameterDescription
api_keyAPI key (alternative to X-API-Key header)
takeovertrue to evict your oldest connection instead of being rejected at the connection limit
flField selection (e.g., id,title,source.domain)
All /everything filterslanguage.code, category, source.domain, sentiment.overall.polarity, title, entity.name, and more

Technical Details

PropertyValue
ProtocolWebSocket (wss://), full-duplex
Poll intervalEvery 10 seconds
Heartbeat intervalEvery 30 seconds
Articles per poll cycleUp to 50
Max message size128 KB
Max connection lifetime6 hours (then reconnect)
Billing1 credit per delivered article
ReconnectionManual (exponential backoff)

Concurrent Connections

Your plan limits how many WebSocket connections you can keep open at the same time:

PlanMax concurrent WebSocket connections
Free0 (not available)
Basic2
Professional10
Corporate50

WebSocket Stream is a paid feature — the Free plan gets 0 connections. A free-plan key is rejected on connect with ER0403. Upgrade to Basic or higher (or use PAYG balance) to open a stream.

Opening more connections than your plan allows sends an error frame with code ER0403 and closes the connection with code 4003:

json
{"type":"error","code":"ER0403","message":"Maximum WebSocket connections (10) reached for your plan."}

Takeover

To reclaim a slot after a restart without rotating your API key, connect with ?takeover=true. Instead of being rejected at the limit, the new connection evicts your oldest connection. The evicted client receives an error frame with code ER0365 and is closed with code 4007:

wss://api.apitube.io/v1/news/ws?language.code=en&takeover=true&api_key=YOUR_API_KEY

This is the recommended way to clear a leftover ("zombie") slot when a client was killed without closing its socket cleanly. Rotating the API key also drops all of that key's sessions.

Billing

Each article delivered through the stream costs 1 credit. Credits are deducted in real time as articles arrive — never for the connection itself or for heartbeats.

How it works:

  1. When you connect, the server checks your balance. With no WebSocket credits and no paid balance, the connection sends an error frame with code ER0404 and closes with code 4002.
  2. While the stream is active, 1 credit is deducted per article sent to you — only after the article is confirmed delivered to an open socket.
  3. If your balance runs out mid-stream, the server sends an error frame with code ER0402 ("WebSocket points exhausted") and closes the connection.

Credit source priority:

  • Plan WebSocket credits (ws_points) are used first.
  • If those are exhausted and you have a positive paid balance, the paid balance is used instead.

Each connection is billed independently

There is no server-side deduplication between your own parallel streams. If you run several connections with overlapping filters, each connection delivers — and separately bills — the same article. N parallel streams that all match one article cost N credits for that article. Run a single stream where possible, or split filters so streams don't overlap.

Error Handling

Errors are delivered as an error frame, after which the connection closes:

json
{"type":"error","code":"ER0402","message":"WebSocket points exhausted."}
Error codeMeaningClose code
ER0401Invalid filters4001
ER0402WebSocket credits exhausted mid-stream4002
ER0403Maximum WebSocket connections reached4003
ER0404No WebSocket credits at connection time4002
ER0361API key revoked4004
ER0362Connection lifetime (6h) exceeded — reconnect4005
ER0365Connection replaced by a newer one (takeover)4007

WebSocket close codes at a glance:

Close codeReason
1000Normal close
1008Unauthorized (invalid or missing API key)
4001Invalid filters
4002Balance exhausted / no credits
4003Connection limit reached
4004API key revoked
4005Lifetime exceeded
4007Taken over (takeover)

SSE or WebSocket?

Both deliver the same articles in real time with the same filters and per-article billing. Choose by connection style:

SSE StreamWebSocket Stream
ProtocolHTTP (one-way)WebSocket (full-duplex)
Message formatevent: article + data:{"type":"article","data":{...}}
ReconnectionAutomatic via Last-Event-IDManual (exponential backoff)
Slot recoverySession list/terminate endpointstakeover option
Best forSimple one-way reading, browser EventSourceFull-duplex clients, ping/pong control

Prefer SSE when you want automatic resume and the simplest one-way read. Prefer WebSocket when you want a full-duplex connection with a typed message protocol. For push delivery to your own URL without holding a connection, see Webhooks.

Next Steps

Support

If you encounter issues:

  1. Check the Technical FAQ
  2. Verify your API key on apitube.io
  3. Contact support through the APITube website