Skip to content

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 /everything filters supported — language, category, sentiment, entities, and more
  • Field selection with fl parameter to reduce payload size

Endpoint

http
GET https://api.apitube.io/v1/news/stream

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 server returns HTTP 401.

Quick Start

cURL

bash
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)

javascript
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

javascript
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

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

text
event: article
id: 123456
data: {"id":123456,"title":"...","source":{...},...}
  • event: article — event type
  • id: — article ID (used for reconnection)
  • data: — full article JSON in the same format as the /everything endpoint

error — Server Error

text
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

text
: heartbeat

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

bash
curl -N -H "X-API-Key: YOUR_API_KEY" \
  "https://api.apitube.io/v1/news/stream?language.code=en"

By category:

bash
curl -N -H "X-API-Key: YOUR_API_KEY" \
  "https://api.apitube.io/v1/news/stream?category.id=medtop:13000000"

By source:

bash
curl -N -H "X-API-Key: YOUR_API_KEY" \
  "https://api.apitube.io/v1/news/stream?source.domain=theverge.com"

Multiple filters:

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

bash
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_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 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:

    bash
    curl -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:

    bash
    curl -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:

  1. Connect and receive articles — each has a unique id:
  2. Connection drops (network issue, server restart, etc.)
  3. Reconnect with the last received ID:
bash
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

ParameterDescription
api_keyAPI key (alternative to X-API-Key header)
flField selection (e.g., id,title,source.domain)
stream.max_ageRolling 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 filterslanguage.code, category, source.domain, sentiment.overall.polarity, title, entity.name, and more

Technical Details

PropertyValue
Poll intervalEvery 30 seconds
Heartbeat intervalEvery 30 seconds
Articles per poll cycleUp to 50
Delivery orderEach poll batch delivered by published_at (oldest first); cursor advances by ingestion id, so no gaps or repeats
Freshness windowOptional stream.max_age (minutes) drops backfill before delivery and billing
Billing1 credit per delivered article
ReconnectionVia Last-Event-ID header
Response headerX-Accel-Buffering: no

Concurrent Connections

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

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

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:

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

http
GET https://api.apitube.io/v1/news/stream/sessions

Returns every SSE session currently open under your account:

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

http
DELETE https://api.apitube.io/v1/news/stream/sessions

Force-closes your active SSE sessions and returns the number closed. Each closed client receives an error event with code ER0364 and the connection ends:

json
{ "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
bash
# 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:

  1. When you connect, the server checks your balance. If you have no credits and no paid balance, the connection is rejected with HTTP 402 and error code ER0176.
  2. While the stream is active, 1 credit is deducted per article sent to you.
  3. If your balance runs out mid-stream, the server sends an error event with code ER0301 ("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:

json
{
    "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):

bash
curl -N -H "X-API-Key: invalid" \
  "https://api.apitube.io/v1/news/stream"
json
{
    "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:

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

text
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

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