Real-time stock news over WebSocket in Python

Twelve lines of Python get you a push feed of ticker-tagged, sentiment-scored headlines that survives disconnects. Here is the SDK version, the raw websockets version, and how to backfill gaps.

Updated 2026-09-20 · 7 min read · every example runs against the live API

Almost every affordable financial-news source is pull-based: you poll, you page, you deduplicate. The H1 News API polls 130+ sources for you — the wires, SEC EDGAR and Nasdaq halts every 20 seconds — and fans the result out over one WebSocket. This guide connects to it from Python.

1. With the SDK (recommended)

install
pip install h1news        # sync + async client, stream helper, typed errors
stream.py
import asyncio
from h1news import AsyncH1News

async def main():
    async with AsyncH1News("sk_...") as news:
        async for a in news.stream(tickers=["NVDA", "AAPL"], categories=["earnings", "halts"]):
            s = a["sentiment"]
            print(f"{a['published_at'][11:19]}  {s['label']:8} {s['score']:+.2f}  "
                  f"{','.join(a['tickers'])}  {a['source']}: {a['title']}")

asyncio.run(main())

Run it and the feed starts within a second:

output
14:47:02  positive +0.71  AAPL  Google News: Analyst Actions: Evercore ISI Raises Apple (NASDAQ: AAPL) Price Target To $380
14:52:41  neutral  +0.00  NVDA  SEC EDGAR 4: 4 - NVIDIA CORP (0001045810) (Issuer)
15:50:04  neutral  +0.00  UZX   Nasdaq Trade Halts: Trading halt: UZX — Linkage Global Inc (NASDAQ, T1: news pending)
16:01:13  negative -0.58  NVDA  Google News: Markets: Nvidia Supplier Warns of Slower Shipments Into Year-End

stream() does three things a bare socket does not: it reconnects with exponential backoff when the connection drops (redeploys, network blips, Cloud Run's ~60-minute connection cap), it replays whatever was published during the gap through /v1/news?since_id= before resuming, and it sends an app-level ping every 30 seconds so idle connections stay up. Plan and auth rejections raise PlanLimitError and AuthError instead of looping.

2. Filters

Filtering is server-side; you receive only what you subscribed to. Every filter takes a list:

FilterValues
tickersAny symbols from the SEC universe — ["NVDA", "AAPL"]
categories / exclude_categoriesmarkets earnings macro world commodities forex crypto halts filings regulatory general
sources / exclude_sourcesExact names from GET /v1/sources, e.g. "Federal Reserve", "SEC EDGAR 8-K"
types / exclude_typesarticle press_release filing social
languagesISO-639-1: en pt es de fr it nl
regionsus uk eu jp br mx latam ca au in cn asia global

Leave tickers out to receive everything that matches the other filters — categories=["halts"] alone is a complete halt alerter.

3. Without the SDK

The protocol is plain JSON frames over a standard WebSocket, so any client works. Filters go on the query string at connect time, or in a subscribe frame later:

raw websockets
import asyncio, json, websockets

URL = ("wss://api.heliusone.com/v1/stream?api_key=sk_..."
       "&tickers=NVDA,AAPL&categories=earnings,halts")

async def main():
    async with websockets.connect(URL) as ws:
        # change filters any time without reconnecting
        await ws.send(json.dumps({"action": "subscribe", "tickers": ["NVDA", "AAPL", "TSLA"]}))
        async for frame in ws:
            msg = json.loads(frame)
            if "ack" in msg or "pong" in msg:
                continue            # subscribe acks and keepalive pongs
            print(msg["sentiment"]["label"], msg["title"])

asyncio.run(main())

Three frame types come back: articles (the same JSON shape as /v1/news results), {"ack": "subscribed", …} after a subscribe, and {"pong": true} after a {"action": "ping"}. Close codes: 1008 means the key was invalid; 4403 means the plan has no stream, and a JSON frame explains before the close.

4. Never miss an article

Ids are monotonic, so a reconnect is a REST call away:

backfill after a disconnect
# After a disconnect, replay everything you missed — ids are gapless
curl -H 'X-API-Key: sk_...' \
  'https://api.heliusone.com/v1/news?since_id=598112&sort=oldest&limit=200&ticker=NVDA'

Store the highest id you have processed, backfill from it on every reconnect, then resume reading the socket. The SDK's stream() keeps that id internally.

5. On the Basic plan: poll with since_id

No stream, but the same articles arrive at one call per poll, and only new ones:

near-real-time on Basic
from h1news import H1News

news = H1News("sk_...")
for a in news.poll("NVDA", interval=30):          # ~2,900 calls/month per poller
    print("new:", a["sentiment"]["label"], a["title"])

6. In a browser, without exposing your key

stream tokens
# on your server (Pro key stays here)
tok = news.stream_token()      # {"token": "...", "expires_in": 300, "ws_url": "wss://api.heliusone.com/v1/stream"}

# in the browser
# new WebSocket(`${ws_url}?token=${token}&tickers=NVDA`)

Tokens live a few minutes and are checked only at connect time; mint a fresh one before each reconnect. The TypeScript SDK's streamWithToken() handles the refresh.

What to build first

Questions

Do I need the Pro plan for the WebSocket?
Yes. The stream is the Pro plan's defining feature. Basic keys get every REST endpoint and can poll with since_id, which returns only articles newer than the last id you saw — near-real-time at one call per poll.
What is the latency from publication to the stream?
Sources on the 20-second tier — the press-release wires, Nasdaq halts, SEC EDGAR and five central banks — are polled every 20 seconds, so a release reaches the socket within about 20 to 30 seconds of publication. Every article carries both published_at and ingested_at so you can measure it on your own feed.
How do I avoid missing articles when the connection drops?
Article ids are gapless for this purpose: remember the highest id you have seen and, after reconnecting, call GET /v1/news?since_id=<id>&sort=oldest before consuming the socket again. The Python and TypeScript SDKs do this automatically.
Can a browser connect directly?
Yes, with a short-lived token from POST /v1/stream/token minted on your server. The browser connects to the ws_url with ?token= and never sees your API key.

Publisher and product names on this page are trademarks of their respective owners and are used only to identify sources and compared services; no affiliation, sponsorship or endorsement is implied.

Try it on your own keys

Basic is $19.99/mo, Pro $49.99/mo; both start with a 5-day, 100-call free trial. Card required, cancel anytime.