Docs / WebSocket stream

WebSocket stream

Real-time gold and silver prices over a persistent connection. Subscribe to XAU-USD-SPOT and XAG-USD-SPOT on one socket; ticks arrive as they clear the spot oracle feed.

REALTIME · $80/MOSSE ALSO AVAILABLE · PRO $30

SSE stream (Pro · $30)

The SSE feed is the simpler of the two transports. It requires no handshake beyond the bearer token: open the URL and events arrive as newline-delimited JSON. It is currently available for gold and silver (XAU-USD-SPOT and XAG-USD-SPOT).

GET/v1/prices/streamBearer · Pro+

Server-Sent Events feed. The server pushes a JSON event each time a new oracle tick is available (typically 2–4 events/second in market hours). A 15-second heartbeat comment line (: heartbeat) keeps the connection alive through proxies. Max 3 concurrent connections per API key.

Query parameters

symbolsstring[]
Comma-separated. Supported: XAU-USD-SPOT, XAG-USD-SPOT. Default: XAU-USD-SPOT.

Event shape

data: {"symbol":"XAU-USD-SPOT","price":"3312.45","conf":"0.41","computed_at":"2026-06-21T14:22:01Z"}
data: {"symbol":"XAG-USD-SPOT","price":"32.18","conf":"0.02","computed_at":"2026-06-21T14:22:01Z"}
: heartbeat

Response codes

200 OK (stream open)401 unauthenticated403 plan_gated429 too_many_connections

SSE — browser example

const es = new EventSource(
  "https://api.goldprice.dev/v1/prices/stream?symbols=XAU-USD-SPOT,XAG-USD-SPOT",
  { headers: { Authorization: "Bearer ga_live_…" } }
);

es.onmessage = (e) => {
  const tick = JSON.parse(e.data);
  // { symbol, price, conf, computed_at }
  console.log(tick.symbol, tick.price);
};
Note
The browser EventSource API does not support custom headers. Use a server-side proxy (see quickstart) or pass the token as a query parameter if your security posture allows it.

WebSocket stream (Realtime · $80)

The WebSocket feed multiplexes multiple metals over one persistent connection. It earns its place over SSE the moment you need both XAU and XAG simultaneously: one socket, subscribe exactly to what you want, unsubscribe mid-session without closing. Max 10 concurrent connections per Realtime key.

WSwss://api.goldprice.dev/v1/streamAuth-on-connect · Realtime+

Full-duplex WebSocket. Authentication happens on the first client frame (no HTTP bearer header — middleware does not fire on WS upgrades). The server validates the key and responds with a welcome frame before it will honour any subscribe frame.

Client frames

All frames are JSON objects with an action key. The first frame on any connection must be auth; the server closes with code 4401 if it is absent or the key is invalid.

// 1. Required first frame — authenticate
{"action":"auth","api_key":"ga_live_…"}

// 2. Subscribe to one or more symbols
{"action":"subscribe","symbols":["XAU-USD-SPOT","XAG-USD-SPOT"]}

// 3. Unsubscribe (connection stays open)
{"action":"unsubscribe","symbols":["XAG-USD-SPOT"]}

// 4. Optional ping — server replies with pong
{"action":"ping"}

Server frames

typeWhenKey fields
welcomeAuth acceptedtier, limits.max_symbols
subscribedSubscribe confirmedsymbols[]
tickNew price availablesymbol, price, conf, computed_at
heartbeat~20 s when no tick sent— (ignore)
pongReply to client ping
errorProtocol / auth violationcode, message
// welcome
{"type":"welcome","tier":"realtime","limits":{"max_symbols":8}}

// subscribed
{"type":"subscribed","symbols":["XAU-USD-SPOT","XAG-USD-SPOT"]}

// tick
{"type":"tick","symbol":"XAU-USD-SPOT","price":"3312.45","conf":"0.41","computed_at":"2026-06-21T14:22:01Z"}

// heartbeat (no reply needed)
{"type":"heartbeat"}

// pong
{"type":"pong"}

// error
{"type":"error","code":"plan_gated","message":"Realtime tier required for WebSocket stream."}

Tick shape

The tick frame matches the SSE event shape so SDKs share a single parser:

  • symbolXAU-USD-SPOT or XAG-USD-SPOT
  • price — decimal string, USD/troy oz. Never a JS number (avoids float precision issues with gold at $3000+).
  • conf — oracle confidence interval, also a decimal string.
  • computed_at — ISO-8601 UTC timestamp, always with a Z suffix (e.g. 2026-06-21T14:22:01Z).

Oracle slot and feed ID are stripped — they are internal oracle identifiers and not included in the public tick surface.

Heartbeat

When no tick has been sent in ~20 seconds (configurable server-side), the server emits {"type":"heartbeat"}. This is a one-way application-layer keepalive — you do not need to reply. Its purpose is to let the server detect dead TCP connections: if the send fails, the slot is freed and the concurrency cap is released.

Pure-listener pattern is supported. Clients that send only the initial auth + subscribe and then receive silently are not disconnected.

Error codes

codeMeaning
unauthenticatedFirst frame was not an auth frame, or was received too late.
plan_gatedAPI key is valid but is not on the Realtime tier.
invalid_symbolOne or more symbols in a subscribe frame are not recognised.
too_many_connectionsPer-key concurrency cap reached (10 for Realtime). Close another connection first.

Close code 4401 is sent when the auth frame is missing or the key is invalid. Do not retry on 4401 — rotating to a valid key is required.

Code examples

Browser — WebSocket

const ws = new WebSocket("wss://api.goldprice.dev/v1/stream");

ws.onopen = () => {
  ws.send(JSON.stringify({ action: "auth", api_key: "ga_live_…" }));
};

ws.onmessage = (event) => {
  const frame = JSON.parse(event.data);

  if (frame.type === "welcome") {
    ws.send(JSON.stringify({
      action: "subscribe",
      symbols: ["XAU-USD-SPOT", "XAG-USD-SPOT"],
    }));
  }

  if (frame.type === "tick") {
    console.log(frame.symbol, frame.price, frame.computed_at);
  }

  // heartbeat: ignore; pong: ignore
};

ws.onclose = (event) => {
  if (event.code === 4401) {
    console.error("Auth failed — invalid API key");
  } else {
    // Reconnect with jitter for transient closes
    setTimeout(() => reconnect(), Math.random() * 4000);
  }
};

Python — websockets

import asyncio, json
import websockets

API_KEY = "ga_live_…"
URL = "wss://api.goldprice.dev/v1/stream"

async def stream():
    async with websockets.connect(URL) as ws:
        # Step 1: authenticate
        await ws.send(json.dumps({"action": "auth", "api_key": API_KEY}))

        async for raw in ws:
            frame = json.loads(raw)

            if frame["type"] == "welcome":
                # Step 2: subscribe
                await ws.send(json.dumps({
                    "action": "subscribe",
                    "symbols": ["XAU-USD-SPOT", "XAG-USD-SPOT"],
                }))

            elif frame["type"] == "tick":
                print(frame["symbol"], frame["price"], frame["computed_at"])

            elif frame["type"] == "heartbeat":
                pass  # no reply needed

asyncio.run(stream())

Live playground

Enter your Realtime API key below to connect directly to the production WebSocket endpoint. Ticks stream as they clear the spot oracle. The connection goes directly to wss://api.goldprice.dev/v1/stream — no Next.js proxy.

Live playgroundIdle
Symbols
SymbolPrice (USD/oz)ConfUpdated
XAU-USD-SPOT

Enter your Realtime API key and click Connect to stream live ticks.

FAQ

What is the difference between SSE and WebSocket?
SSE (GET /v1/prices/stream, Pro $30) is a simple server-push feed — open one HTTP connection, receive newline-delimited events. It is great for browsers that need one symbol and don't want to manage a socket. WebSocket (wss://api.goldprice.dev/v1/stream, Realtime $80) gives you a full-duplex connection: you subscribe to exactly the symbols you want, can unsubscribe mid-session, and ping the server. The real benefit of WebSocket is multiplexing — one socket, multiple metals, lower overhead than two parallel SSE connections.
Do I need to reply to heartbeat frames?
No. The server sends {"type":"heartbeat"} every ~20 seconds when no tick has been delivered in that window. It is a one-way application keepalive — you can ignore it completely. Pure-listener clients (no outbound frames except the initial auth + subscribe) are fully supported.
What close code should I watch for?
Close codes 4401 (auth failed / invalid key), 4403 (plan not entitled to the stream), and 4429 (connection limit reached) are terminal — the condition won't resolve on its own, so do not retry. All other abnormal closes (network loss, Fly machine restart) are transient and should be retried with jitter.
Which plan do I need?
SSE requires Pro ($30/mo, up to 3 concurrent streams/key). WebSocket requires Realtime ($80/mo, up to 10 concurrent connections/key). Both plans include REST access to all endpoints.

See also

  • API Reference — REST endpoints for polling use-cases
  • Quickstart — get your first price in ten minutes
  • Pricing — Pro ($30) and Realtime ($80) plan details