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)
pip install h1news # sync + async client, stream helper, typed errors 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:
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:
| Filter | Values |
|---|---|
tickers | Any symbols from the SEC universe — ["NVDA", "AAPL"] |
categories / exclude_categories | markets earnings macro world commodities forex crypto halts filings regulatory general |
sources / exclude_sources | Exact names from GET /v1/sources, e.g. "Federal Reserve", "SEC EDGAR 8-K" |
types / exclude_types | article press_release filing social |
languages | ISO-639-1: en pt es de fr it nl |
regions | us 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:
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:
# 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:
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
# 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
- Halt sniper — alert on every halt with the headlines behind it.
- Alpaca risk control — flatten a paper position on a negative primary-source headline.
- A Slack channel per category via n8n or Make.
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.