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.
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).
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, andHG-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"}
: heartbeatResponse codes
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);
};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.
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
| type | When | Key fields |
|---|---|---|
welcome | Auth accepted | tier, limits.max_symbols |
subscribed | Subscribe confirmed | symbols[] |
tick | New price available | symbol, price, conf, computed_at |
heartbeat | ~20 s when no tick sent | Ignore |
pong | Reply to client ping | None |
error | Protocol / auth violation | code, 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, orHG-USD-SPOTprice: 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 asprice. 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 aZsuffix (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
| code | Meaning |
|---|---|
unauthenticated | First frame was not an auth frame, or was received too late. |
plan_gated | API key is valid but is not on the Realtime Pro tier. |
invalid_symbol | One or more symbols in a subscribe frame are not recognised. |
too_many_connections | Per-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.
FAQ
What is the difference between SSE and WebSocket?
Do I need to reply to heartbeat frames?
What close code should I watch for?
Which plan do I need?
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