SSE Stream
The SSE (Server-Sent Events) Stream endpoint delivers news articles in real time as APITube discovers and indexes them. Instead of polling the API repeatedly, open a single persistent connection and receive articles automatically.
What "real time" means here: articles are pushed as soon as APITube finishes ingesting them — not at the exact moment their publisher released them. See Delivery Order and Freshness for how this affects article age and how to keep only fresh articles.
What is SSE Stream?
SSE Stream is a real-time news delivery channel built on the Server-Sent Events standard. You open one HTTP connection and the server pushes new articles to you as they appear — no repeated requests needed.
Use cases:
- Live news feeds and 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, no repeated requests needed
- Uses standard SSE protocol — works in any language
- Automatic reconnection with no data loss via
Last-Event-ID - All
/everythingfilters supported — language, category, sentiment, entities, and more - Field selection with
flparameter to reduce payload size
Endpoint
GET https://api.apitube.io/v1/news/streamAuthentication
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 server returns HTTP 401.
Quick Start
cURL
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?language.code=en"The -N flag disables buffering so events appear in real time.
JavaScript (Browser)
const es = new EventSource(
'https://api.apitube.io/v1/news/stream?api_key=YOUR_API_KEY&language.code=en'
);
es.addEventListener('article', (e) => {
const article = JSON.parse(e.data);
console.log(`[${e.lastEventId}]`, article.title);
});
es.addEventListener('error', (e) => {
if (e.data) {
console.error('Server error:', JSON.parse(e.data));
}
});
// To disconnect:
// es.close();Node.js
import EventSource from 'eventsource';
const es = new EventSource(
'https://api.apitube.io/v1/news/stream?language.code=en',
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
es.addEventListener('article', (e) => {
const article = JSON.parse(e.data);
console.log(`[${e.lastEventId}]`, article.title);
});
es.addEventListener('error', (e) => {
if (e.data) {
console.error('Server error:', JSON.parse(e.data));
}
});TIP
The eventsource npm package supports custom headers, which makes it the recommended choice for Node.js.
Python
import requests
url = 'https://api.apitube.io/v1/news/stream'
headers = {'X-API-Key': 'YOUR_API_KEY'}
params = {'language.code': 'en'}
with requests.get(url, headers=headers, params=params, stream=True) as r:
for line in r.iter_lines(decode_unicode=True):
if line:
print(line)SSE Event Format
The stream uses standard SSE format with three event types:
article — New Article
event: article
id: 123456
data: {"id":123456,"title":"...","source":{...},...}event: article— event typeid:— article ID (used for reconnection)data:— full article JSON in the same format as the /everything endpoint
error — Server Error
event: error
data: {"code":"ER0301","message":"Balance exhausted"}Sent when an error occurs (e.g., balance exhausted). The connection closes after an error event.
Heartbeat
: heartbeatAn SSE comment sent every 30 seconds as a keep-alive signal. This is not a named event — it simply prevents the connection from timing out.
Filtering
All filters available on the /everything endpoint work with SSE Stream:
By language:
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?language.code=en"By category:
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?category.id=medtop:13000000"By source:
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?source.domain=theverge.com"Multiple filters:
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?language.code=en&category.id=medtop:13000000&sentiment.overall.polarity=positive"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:
curl -N -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream?language.code=en&fl=id,title,source.domain"The data: payload will contain only the specified fields.
Delivery Order and Freshness
The stream advances through articles by an internal ingestion cursor (the order APITube indexes them) so that no article is ever skipped or delivered twice. Within each poll, the batch is then delivered ordered by published_at (oldest first). "Real time" means as soon as APITube discovers and processes an article, which is not necessarily the moment its publisher released it — so an article's published_at can already be some time in the past when it reaches you.
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 or infrequently-updated sources can lag by much longer.
Dropping stale (backfill) articles
You can stop old articles from being delivered — and therefore from being billed — in two ways:
stream.max_age— a rolling freshness window, in minutes. Each poll only delivers articles published within the last N minutes; anything older is dropped before delivery and before billing. Best for a continuous pipeline that only wants live articles:bashcurl -N -H "X-API-Key: YOUR_API_KEY" \ "https://api.apitube.io/v1/news/stream?language.code=en&stream.max_age=15"An invalid value (non-integer or less than 1) is rejected on connect with
400 ER0363.published_at.start— a fixed cutoff timestamp. Drops anything published before that instant:bashcurl -N -H "X-API-Key: YOUR_API_KEY" \ "https://api.apitube.io/v1/news/stream?language.code=en&published_at.start=2026-07-03T10:00:00"
To bias the stream toward higher-authority sources, filter by source rank with source.rank.opr.min (Open PageRank) or is_premium_source. The stream sorts each delivered batch by publish time, but does not globally re-order the whole stream by source priority — it can only filter low-rank sources out. See Parameters for these filters.
Reconnection with Last-Event-ID
SSE has built-in reconnection support. If the connection drops, the client can resume from where it left off using the Last-Event-ID header:
- Connect and receive articles — each has a unique
id: - Connection drops (network issue, server restart, etc.)
- Reconnect with the last received ID:
curl -N -H "X-API-Key: YOUR_API_KEY" \
-H "Last-Event-ID: 123456" \
"https://api.apitube.io/v1/news/stream?language.code=en"The server will immediately deliver any articles indexed after ID 123456, then continue streaming new ones.
TIP
The browser EventSource API handles reconnection and Last-Event-ID automatically. If the connection drops, the browser will reconnect and resume from the last received event — no extra code needed.
Parameters
| Parameter | Description |
|---|---|
api_key | API key (alternative to X-API-Key header) |
fl | Field selection (e.g., id,title,source.domain) |
stream.max_age | Rolling freshness window in minutes — deliver only articles published within the last N minutes; older ones are dropped before delivery and billing. Invalid value → 400 ER0363 |
All /everything filters | language.code, category, source.domain, sentiment.overall.polarity, title, entity.name, and more |
Technical Details
| Property | Value |
|---|---|
| Poll interval | Every 30 seconds |
| Heartbeat interval | Every 30 seconds |
| Articles per poll cycle | Up to 50 |
| Delivery order | Each poll batch delivered by published_at (oldest first); cursor advances by ingestion id, so no gaps or repeats |
| Freshness window | Optional stream.max_age (minutes) drops backfill before delivery and billing |
| Billing | 1 credit per delivered article |
| Reconnection | Via Last-Event-ID header |
| Response header | X-Accel-Buffering: no |
Concurrent Connections
Your plan limits how many SSE connections you can keep open at the same time:
| Plan | Max concurrent SSE connections |
|---|---|
| Free | 0 (not available) |
| Basic | 2 |
| Professional | 10 |
| Corporate | 50 |
SSE Stream is a paid feature — the Free plan gets 0 connections. A free-plan key is rejected on connect with ER0360. Upgrade to Basic or higher to open a stream.
Opening more connections than your plan allows returns HTTP 429 with error code ER0360:
{
"status": "not_ok",
"errors": [
{
"status": 429,
"code": "ER0360",
"message": "Maximum SSE connections (10) reached for your plan."
}
]
}A connection's slot is freed when its TCP connection closes cleanly. If a client process is killed without closing the socket (for example a hard SIGKILL, or a half-open connection behind a proxy), the slot may take longer to be reclaimed. If you hit ER0360 after a restart even though no client is running, you have two ways to release stuck slots immediately: call the session-management endpoint below (recommended for scheduled restarts), or rotate your API key — either drops the affected sessions on the next poll cycle.
Zombie connections
SSE has no application-level ping/pong — liveness relies on the OS-level TCP keepalive (enabled at ~30-second intervals) plus a backpressure watchdog. How fast a dead or half-open client releases its slot depends on whether its feed is active:
- Active feed (articles flowing): if the client stops reading, the server's outbound buffer backs up. Once it grows past ~1 MB for a few consecutive checks (about 15 seconds), the server force-closes the socket and frees the slot automatically.
- Silent feed (no matching articles): backpressure cannot detect a dead client, because nothing is being written except periodic heartbeats. The slot then lingers until TCP keepalive notices the peer is gone, the 6-hour max lifetime (
ER0362) is reached, the API key is revoked (ER0361), or you close the session with the terminate endpoint (ER0364).
The silent-feed case is why a slot can appear "stuck" after an unclean shutdown. Don't wait for it to time out — call DELETE /v1/news/stream/sessions or rotate the key to reclaim it immediately.
Managing Active Sessions
Two endpoints let you inspect and force-close your own active SSE sessions — useful for a 24/7 pipeline that restarts on a schedule and needs to clear leftover ("zombie") slots without rotating the API key. Neither endpoint is billed.
List active sessions
GET https://api.apitube.io/v1/news/stream/sessionsReturns every SSE session currently open under your account:
{
"count": 2,
"sessions": [
{ "session_id": "req-a1b2c3", "api_key_id": 42, "started_at": "2026-07-03T10:11:04.512Z" },
{ "session_id": "req-d4e5f6", "api_key_id": 42, "started_at": "2026-07-03T10:11:05.973Z" }
]
}Terminate sessions
DELETE https://api.apitube.io/v1/news/stream/sessionsForce-closes your active SSE sessions and returns the number closed. Each closed client receives an error event with code ER0364 and the connection ends:
{ "terminated": 2 }Optional query filters narrow the scope:
?session_id=<id>— close only that one session (id from the list endpoint)?api_key_id=<id>— close only sessions opened by that specific key
# Before a scheduled restart: drop every open SSE session for the account
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" \
"https://api.apitube.io/v1/news/stream/sessions"Both endpoints authenticate like any other request and require the stream scope if your key is scope-restricted. They act only on sessions belonging to the calling key's account.
Billing
Each article delivered through the stream costs 1 credit. Credits are deducted in real time as articles arrive — not when you open the connection.
How it works:
- When you connect, the server checks your balance. If you have no credits and no paid balance, the connection is rejected with HTTP
402and error codeER0176. - While the stream is active, 1 credit is deducted per article sent to you.
- If your balance runs out mid-stream, the server sends an
errorevent with codeER0301("Balance exhausted") and closes the connection.
Credit source priority:
- If you have available plan credits (points), they are used first.
- If plan credits are exhausted and you have a positive paid balance, the paid balance is used instead.
The stream connection responds with the standard SSE headers only (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no). It does not return billing state in response headers. To check your remaining plan credits and paid balance, call GET /v1/balance separately.
TIP
The stream does not charge for heartbeats or the connection itself — only for actual articles delivered. If no articles match your filters, no credits are spent.
Deduplication and parallel streams
There is no server-side deduplication between your own parallel streams. If you open 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. This is the usual reason a dashboard credit total is higher than a client-side count that has already de-duplicated stories — you are billed per delivery, per connection, at send time, before any de-duplication you do on your side.
Article-level reprints are also not reliably de-duplicated: the same story republished by several sources arrives as separate articles, each billed. Run a single stream where possible, split filters so streams don't overlap, and de-duplicate on your side by article id or href.
Error Handling
Insufficient Balance
If your account has no credits and no paid balance, the server rejects the connection with HTTP 402:
{
"status": "not_ok",
"request_id": "fecf47d8-c90c-4c48-bdf0-bc34f6d41415",
"errors": [
{
"status": 402,
"code": "ER0176",
"message": "You have no SSE points on your account.",
"links": {
"about": "https://docs.apitube.io/platform/news-api/http-response-codes"
},
"timestamp": "2025-05-11T17:02:46Z"
}
]
}Top up your balance or wait for plan credits to renew before reconnecting.
Invalid or Missing API Key
Returns a standard HTTP 401 JSON error response (not SSE):
curl -N -H "X-API-Key: invalid" \
"https://api.apitube.io/v1/news/stream"{
"status": "not_ok",
"request_id": "fecf47d8-c90c-4c48-bdf0-bc34f6d41415",
"errors": [
{
"status": 401,
"code": "ER0202",
"message": "Invalid API key.",
"links": {
"about": "https://docs.apitube.io/platform/news-api/http-response-codes"
},
"timestamp": "2025-05-11T17:02:46Z"
}
]
}A missing API key returns the same envelope with code ER0201 (API key is required. Provide it via X-API-Key header or api_key query parameter.).
Balance Exhausted
If your balance runs out during an active stream, the server sends an error event and closes the connection:
event: error
data: {"code":"ER0301","message":"Balance exhausted"}Session Terminated
If a session is force-closed via the terminate endpoint (or by an API key rotation), the client receives an error event and the connection ends:
event: error
data: {"code":"ER0364","message":"SSE session terminated by API"}Reconnect to resume streaming; pass Last-Event-ID to continue from the last article you received.
Next Steps
- Learn about all available Parameters
- Explore common workflow examples
- 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