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:

MethodExampleNotes
X-API-Key headerX-API-Key: fj_xxxPreferred — keys stay out of access logs
Authorization headerAuthorization: Bearer fj_xxxEquivalent to the above
Query string?apikey=fj_xxxFor 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": ... }

typeeventdata
hello{ channels: [...], delay_seconds: N, ts: ... } — once per connect. delay_seconds reflects your plan.
newscreatedArray of news items (usually one) — see the news object below
newsupdatedArray with the edited item, same shape
newsdeletedNumeric id of the removed item
calendarchangedA calendar event (new and updated both)
calendardeletedNumeric id of the removed event
calendarbulkArray 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

PlanConcurrent connectionsFeed
Free1Delayed by 10 minutes
Standard5Real-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 code 4003.
  • 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/4003 rejections — they won't change until you fix the key or its allowlist.
  • Idle connections are kept alive by the server's 30s ping frame — 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 codeerror.codeMeaningRetry?
4001missing_key / invalid_keyMissing, unknown, or deactivated API keyNo — fix the key first
4003ip_not_allowed / origin_not_allowedKey valid, but your IP or origin isn't on the key's allowlistNo — adjust the key's settings
4029connection_limitYour plan's concurrent-connection limit is already in useYes, with backoff — it clears when one of your connections drops
4030connection_replacedOne of your own connections used takeover=true and replaced this oneNo — deliberate replacement; reconnecting starts an eviction loop