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
typefield (like Finnhub / Alpaca) - Connection acknowledgement — the server confirms the connection and the accepted subscription up front
- Full-duplex keep-alive — client
ping/ serverpong, plus server heartbeats - All
/everythingfilters supported — language, category, sentiment, entities, and more - Field selection with the
flparameter to reduce payload size takeoveroption to reclaim a slot after a restart without rotating your key
Endpoint
GET wss://api.apitube.io/v1/news/wsOne 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)
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)
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
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
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:
{"type":"connected","session_id":"req-a1b2c3","timestamp":1718800000000}
{"type":"subscribed","filters":{"language.code":"en"},"timestamp":1718800000000}connected— the connection is established and authenticated.session_ididentifies this session.subscribed— your subscription is accepted.filtersechoes 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:
{"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 withfl).
Control frames:
| Frame | When |
|---|---|
{"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:
{ "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_KEYBy category:
wss://api.apitube.io/v1/news/ws?category.id=medtop:13000000&api_key=YOUR_API_KEYBy source:
wss://api.apitube.io/v1/news/ws?source.domain=theverge.com&api_key=YOUR_API_KEYMultiple filters:
wss://api.apitube.io/v1/news/ws?language.code=en&category.id=medtop:13000000&sentiment.overall.polarity=positive&api_key=YOUR_API_KEYFor 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_KEYDelivery 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_atis the publisher's original timestamp, read from the article's own metadata. When a source provides no valid date,published_atfalls 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_atalready 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 browserWebSocketAPI and the Nodewslibrary) 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:
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
| Parameter | Description |
|---|---|
api_key | API key (alternative to X-API-Key header) |
takeover | true to evict your oldest connection instead of being rejected at the connection limit |
fl | Field selection (e.g., id,title,source.domain) |
All /everything filters | language.code, category, source.domain, sentiment.overall.polarity, title, entity.name, and more |
Technical Details
| Property | Value |
|---|---|
| Protocol | WebSocket (wss://), full-duplex |
| Poll interval | Every 10 seconds |
| Heartbeat interval | Every 30 seconds |
| Articles per poll cycle | Up to 50 |
| Max message size | 128 KB |
| Max connection lifetime | 6 hours (then reconnect) |
| Billing | 1 credit per delivered article |
| Reconnection | Manual (exponential backoff) |
Concurrent Connections
Your plan limits how many WebSocket connections you can keep open at the same time:
| Plan | Max concurrent WebSocket connections |
|---|---|
| Free | 0 (not available) |
| Basic | 2 |
| Professional | 10 |
| Corporate | 50 |
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:
{"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_KEYThis 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:
- When you connect, the server checks your balance. With no WebSocket credits and no paid balance, the connection sends an
errorframe with codeER0404and closes with code4002. - 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.
- If your balance runs out mid-stream, the server sends an
errorframe with codeER0402("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:
{"type":"error","code":"ER0402","message":"WebSocket points exhausted."}| Error code | Meaning | Close code |
|---|---|---|
ER0401 | Invalid filters | 4001 |
ER0402 | WebSocket credits exhausted mid-stream | 4002 |
ER0403 | Maximum WebSocket connections reached | 4003 |
ER0404 | No WebSocket credits at connection time | 4002 |
ER0361 | API key revoked | 4004 |
ER0362 | Connection lifetime (6h) exceeded — reconnect | 4005 |
ER0365 | Connection replaced by a newer one (takeover) | 4007 |
WebSocket close codes at a glance:
| Close code | Reason |
|---|---|
1000 | Normal close |
1008 | Unauthorized (invalid or missing API key) |
4001 | Invalid filters |
4002 | Balance exhausted / no credits |
4003 | Connection limit reached |
4004 | API key revoked |
4005 | Lifetime exceeded |
4007 | Taken 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 Stream | WebSocket Stream | |
|---|---|---|
| Protocol | HTTP (one-way) | WebSocket (full-duplex) |
| Message format | event: article + data: | {"type":"article","data":{...}} |
| Reconnection | Automatic via Last-Event-ID | Manual (exponential backoff) |
| Slot recovery | Session list/terminate endpoints | takeover option |
| Best for | Simple one-way reading, browser EventSource | Full-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
- Learn about all available Parameters
- Compare with the SSE Stream and Webhooks
- Check Authentication options
- See HTTP Response Codes for error handling
Support
If you encounter issues:
- Check the Technical FAQ
- Verify your API key on apitube.io
- Contact support through the APITube website