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.
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).
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"}
: 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, 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.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.
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
| 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 | — |
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","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:
symbol—XAU-USD-SPOTorXAG-USD-SPOTprice— 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 aZsuffix (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
| 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 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). 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.
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 ($80) plan details