WebSocket Quickstart
Real-time FinancialJuice news headlines and economic-calendar events over a plain WebSocket. No SDK required — any WebSocket client in any language works.
1. Get an API key
Sign in to this portal with your FinancialJuice account and generate a key on the API Keys page. The key is shown once at creation — store it like a password. You can create as many keys as you like (one per app or environment is typical); your plan's connection limit applies across all of them combined.
2. Connect
wss://stream.financialjuice.com/v1/stream
Authenticate with your API key in one of three ways:
| Method | Example | Notes |
|---|---|---|
X-API-Key header | X-API-Key: fj_xxx | Preferred — keys stay out of access logs |
Authorization header | Authorization: Bearer fj_xxx | Equivalent to the above |
| Query string | ?apikey=fj_xxx | For browsers and quick tests (browser WebSockets can't set headers) |
Browser / JavaScript (with auto-reconnect)
Deploys and network blips happen — always wrap the connection in a reconnect loop. Use the close code to decide: configuration problems (4001/4003) won't fix themselves, everything else is worth retrying with capped backoff.
let attempt = 0;
function connect() {
const ws = new WebSocket('wss://stream.financialjuice.com/v1/stream?apikey=fj_xxx');
ws.onopen = () => { attempt = 0; }; // reset backoff once we're in
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch (msg.type) {
case 'hello': // sent once on connect
console.log('connected, channels:', msg.channels, 'delay:', msg.delay_seconds + 's');
break;
case 'news': // msg.event = created | updated | deleted
console.log('news', msg.event, msg.data);
break;
case 'calendar': // msg.event = changed | deleted | bulk
console.log('calendar', msg.event, msg.data);
break;
case 'error': // rejection detail — the socket closes right after this
console.warn('server error:', msg.code, msg.message);
break;
case 'ping': // keepalive every 30s — safe to ignore
break;
}
};
ws.onclose = (e) => {
// 4001 = bad/missing key, 4003 = IP/origin not allowed — retrying won't
// help. 4030 = replaced by your own newer takeover connection — reconnecting
// would make your clients evict each other forever.
if (e.code === 4001 || e.code === 4003 || e.code === 4030) {
console.error('rejected by server:', e.code, e.reason);
return;
}
// Anything else — normal drop, deploy, or 4029 (connection limit, which
// clears when another of your connections closes): exponential backoff
// capped at 30s, with jitter so a fleet of clients doesn't stampede.
attempt++;
const delay = Math.min(30000, 1000 * 2 ** Math.min(attempt, 5)) + Math.random() * 1000;
console.log(`reconnecting in ${Math.round(delay / 1000)}s (attempt ${attempt})`);
setTimeout(connect, delay);
};
}
connect();
Node.js
Same pattern; the key goes in a header instead of the query string.
// npm install ws
const WebSocket = require('ws');
let attempt = 0;
function connect() {
const ws = new WebSocket('wss://stream.financialjuice.com/v1/stream', {
headers: { 'X-API-Key': 'fj_xxx' }
});
ws.on('open', () => { attempt = 0; });
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'news') console.log(msg.event, msg.data);
if (msg.type === 'error') console.warn('server error:', msg.code, msg.message);
});
ws.on('close', (code, reason) => {
if (code === 4001 || code === 4003 || code === 4030) {
console.error('rejected by server:', code, reason.toString());
return; // fix the key / allowlist first (or you were deliberately replaced)
}
attempt++;
const delay = Math.min(30000, 1000 * 2 ** Math.min(attempt, 5)) + Math.random() * 1000;
setTimeout(connect, delay);
});
ws.on('error', () => ws.close());
}
connect();
Python
# pip install websockets
import asyncio, json, random, websockets
URL = "wss://stream.financialjuice.com/v1/stream"
async def run():
attempt = 0
while True:
try:
# websockets < 13 uses extra_headers= instead of additional_headers=
async with websockets.connect(URL, additional_headers={"X-API-Key": "fj_xxx"}) as ws:
attempt = 0
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "news":
print(msg["event"], msg["data"])
elif msg["type"] == "error":
print("server error:", msg["code"], msg["message"])
except websockets.ConnectionClosed as e:
if e.rcvd and e.rcvd.code in (4001, 4003, 4030):
print("rejected:", e.rcvd.code, e.rcvd.reason)
return # fix the key / allowlist first
except Exception:
pass # network blip — fall through to the backoff
attempt += 1
await asyncio.sleep(min(30, 2 ** min(attempt, 5)) + random.random())
asyncio.run(run())
C#
// .NET — no package needed, System.Net.WebSockets is built in
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
var attempt = 0;
while (true)
{
using var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("X-API-Key", "fj_xxx");
try
{
await ws.ConnectAsync(new Uri("wss://stream.financialjuice.com/v1/stream"), CancellationToken.None);
attempt = 0;
var buffer = new byte[64 * 1024];
while (ws.State == WebSocketState.Open)
{
using var frame = new MemoryStream();
WebSocketReceiveResult r;
do { r = await ws.ReceiveAsync(buffer, CancellationToken.None); frame.Write(buffer, 0, r.Count); }
while (!r.EndOfMessage);
if (r.MessageType == WebSocketMessageType.Close) break;
using var msg = JsonDocument.Parse(Encoding.UTF8.GetString(frame.ToArray()));
var type = msg.RootElement.GetProperty("type").GetString();
if (type == "news") Console.WriteLine(msg.RootElement.ToString());
}
if ((int?)ws.CloseStatus is 4001 or 4003 or 4030)
{
Console.WriteLine($"rejected: {(int?)ws.CloseStatus} {ws.CloseStatusDescription}");
return; // fix the key / allowlist first
}
}
catch { /* network blip — fall through to the backoff */ }
attempt++;
await Task.Delay(TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, Math.Min(attempt, 5)))));
}
3. Message format
Every frame is a JSON object: { "type": ..., "event": ..., "data": ... }
type | event | data |
|---|---|---|
hello | — | { channels: [...], delay_seconds: N, ts: ... } — once per connect. delay_seconds reflects your plan. |
news | created | Array of news items (usually one) — see the news object below |
news | updated | Array with the edited item, same shape |
news | deleted | Numeric id of the removed item |
calendar | changed | A calendar event (new and updated both) |
calendar | deleted | Numeric id of the removed event |
calendar | bulk | Array of calendar events |
ping | — | { ts: ... } every 30 seconds; no reply needed |
error | — | { code, message } — sent once just before the server closes the socket (see Errors below) |
The news object
{
"type": "news",
"event": "created",
"data": [
{
"newsId": 4501234,
"datePublished": "2026-06-11T08:42:07Z", // always UTC
"title": "ECB's Lagarde: Rate path remains data dependent",
"description": "",
"labels": ["ECB", "EUR"],
"link": "https://www.financialjuice.com/..." // external source URL when one exists
}
]
}
4. Plans & limits
| Plan | Concurrent connections | Feed |
|---|---|---|
| Free | 1 | Delayed by 10 minutes |
| Standard | 5 | Real-time |
The connection limit is per account, across all your keys. A connection over the limit is rejected with an error frame and close code 4029 (see Errors below). To upgrade, contact us.
Connection takeover
If your client crashes or restarts, its old connection can hold your slot for up to ~90 seconds before it times out. Add takeover=true to the connection URL and, when you're at your limit, your oldest connection is closed (close code 4030) to make room — instead of the new one being rejected with 4029:
wss://stream.financialjuice.com/v1/stream?apikey=fj_xxx&takeover=true
Never auto-reconnect on 4030 — that close means another of your own connections deliberately replaced this one. Reconnecting would start an eviction tug-of-war between your clients.
5. Key restrictions (optional)
On the API Keys page you can lock each key down:
- Allowed IPs — comma-separated addresses or CIDR blocks (e.g.
198.51.100.0/24). Connections from anywhere else are rejected with close code4003. - Allowed origins — restricts which websites may use the key from a browser. Server-side clients are unaffected (they send no origin), so use the IP allowlist for those.
6. Connection lifecycle
- There is no replay: messages published while you were disconnected are not re-sent. Reconnect promptly — the next message is rarely more than seconds away.
- Reconnect with exponential backoff and jitter (both examples above do this). Don't retry
4001/4003rejections — they won't change until you fix the key or its allowlist. - Idle connections are kept alive by the server's 30s
pingframe — you don't need to send anything, ever.
Errors
A rejected connection is accepted, sent a single { "type": "error", "code": ..., "message": ... } frame, and then closed with a matching close code — so the reason is visible from any client, including browsers (which can't see HTTP-level rejections on WebSockets). Read it in onmessage, or from event.code / event.reason in onclose:
| Close code | error.code | Meaning | Retry? |
|---|---|---|---|
4001 | missing_key / invalid_key | Missing, unknown, or deactivated API key | No — fix the key first |
4003 | ip_not_allowed / origin_not_allowed | Key valid, but your IP or origin isn't on the key's allowlist | No — adjust the key's settings |
4029 | connection_limit | Your plan's concurrent-connection limit is already in use | Yes, with backoff — it clears when one of your connections drops |
4030 | connection_replaced | One of your own connections used takeover=true and replaced this one | No — deliberate replacement; reconnecting starts an eviction loop |