Docs / WebSocket stream

Gold Price WebSocket API

Real-time gold, silver, and copper spot prices over a persistent connection. Subscribe to XAU-USD-SPOT, XAG-USD-SPOT, and HG-USD-SPOT on one socket; ticks arrive as they clear the spot oracle feed. The stream carries live ticks plus the physical channel. It does not push OHLC bars; poll /v1/bars/latest for the freshest bar.

REALTIME PRO · $80/MOSSE ALSO AVAILABLE · PRO $30
Current capability map

WebSocket and SSE carry live ticks; they do not emit OHLC bars. Subscribe to XAU-USD-SPOT, XAG-USD-SPOT, or HG-USD-SPOT after authentication. For OHLCV bars, use /v1/bars with 1m, 5m, 15m, 30m, 1h, 2h, 4h, or 1d. Plan access, connection limits, and history windows are separate entitlements.

The SSE route uses one symbol query parameter per connection. Its supported live spot symbols areXAU-USD-SPOT, XAG-USD-SPOT, andHG-USD-SPOT when the selected live feed is enabled. HG spot is the LSE USD-per-pound product; HG futures settlement is a separate symbol.

The older native browser EventSource snippet further down shows headers that the browser API cannot send. Use the workingserver proxy example instead.

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, silver, and copper (XAU-USD-SPOT, XAG-USD-SPOT, and HG-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, and HG-USD-SPOT. Default: XAU-USD-SPOT.

Event shape

data: {"symbol":"XAU-USD-SPOT","price":"3312.45","bid":"3312.40","ask":"3312.50","computed_at":"2026-06-21T14:22:01Z"}
data: {"symbol":"XAG-USD-SPOT","price":"32.18","bid":"32.17","ask":"32.19","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, bid?, ask?, 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.

SSE — browser proxy

Native browser EventSource cannot set an Authorization header. Keep the API key on your server and proxy the stream to the browser. This Express example assumes your app has already mounted Passport session middleware; its viewer session check must pass before the paid stream is opened. The server route adds the bearer header from a secret environment variable and forwards the upstreamtext/event-stream body. The browser then opens/api/gold-stream with no key in its code or URL.

// Node.js 22+ · npm install express@5 · mount your Passport session middleware first
import express from "express";
const key = process.env.GOLDPRICE_API_KEY;
if (!key) throw new Error("GOLDPRICE_API_KEY is required");
const app = express();
app.get("/api/gold-stream", async (req, res) => {
  if (!req.isAuthenticated?.()) return res.sendStatus(401);
  const controller = new AbortController();
  res.on("close", () => controller.abort());
  const upstream = await fetch(
    "https://api.goldprice.dev/v1/prices/stream?symbol=XAU-USD-SPOT",
    { headers: { Authorization: "Bearer " + key }, signal: controller.signal },
  );
  if (!upstream.ok) return res.status(upstream.status).send(await upstream.text());
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  if (!upstream.body) return res.end();
  try {
    for await (const chunk of upstream.body) res.write(Buffer.from(chunk));
  } catch (error) {
    if (!controller.signal.aborted) console.error("Gold stream failed", error);
  }
  res.end();
});
app.listen(3000);

The browser connects to that local route:

const es = new EventSource("/api/gold-stream");
es.onmessage = (event) => {
  const tick = JSON.parse(event.data);
  console.log(tick.symbol, tick.price, tick.computed_at);
};
es.onerror = () => console.error("Gold stream disconnected");

WebSocket stream (Realtime Pro · $80)

The WebSocket feed multiplexes multiple metals over one persistent connection. It earns its place over SSE the moment you need XAU, XAG, and HG spot simultaneously. One socket carries only the symbols you choose, and you can unsubscribe without closing it. Max 10 concurrent connections per Realtime Pro key.

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

Full-duplex WebSocket. Authentication happens on the first client frame (no HTTP bearer header, because 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 sentIgnore
pongReply to client pingNone
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","bid":"3312.40","ask":"3312.50","computed_at":"2026-06-21T14:22:01Z"}

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

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

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

Tick shape

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

  • symbol: XAU-USD-SPOT, XAG-USD-SPOT, or HG-USD-SPOT
  • price: decimal string, USD/troy oz for XAU/XAG or USD/pound for HG. Rounded to nearest with half-up ties: 2 decimal places for XAU, 3 for XAG, 4 for HG. Never a JS number.
  • bid / ask: optional decimal strings from the same selected observation as price. Each side is omitted when unavailable; clients must not synthesize it. Sides use the same rounding as price; very narrow spreads may round to zero, and the rounded price may differ from the mean of the rounded sides.
  • conf: optional decimal-string confidence interval. It is omitted when the selected source does not publish one.
  • 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 because 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 Pro tier.
invalid_symbolOne or more symbols in a subscribe frame are not recognised.
too_many_connectionsPer-key concurrency cap reached (10 for Realtime Pro). 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 Pro 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 with no Next.js proxy.

Live playgroundIdle
0
Connections
0
Subscriptions
0
Ticks received
Symbols
Send frame
SymbolPrice (USD/oz)ConfidenceUpdated
XAU-USD-SPOT

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

Event log
No events yet.

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 and receive newline-delimited events. It works well for browsers that need one symbol without managing a socket. WebSocket (wss://api.goldprice.dev/v1/stream, Realtime Pro $80) gives you a full-duplex connection where you choose the symbols, unsubscribe mid-session, and ping the server. One socket can carry multiple metals with less overhead than 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, so you can ignore it. 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 Pro ($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 Pro ($80) plan details