Prices stream reference
Spot commodity prices and FX rates over one WebSocket connection, with an initial price for each subscription and updates when prices change
Access and limits
The prices stream is available on Basic and Pro plans. A Free plan key receives plan_upgrade_required, and the connection closes.
The prices, events and predictions streams share a concurrent connection limit: 1 on Free, 2 on Basic and 10 on Pro. Each connection uses one call from the monthly quota.
Connect
Connect to wss://api.narwhalapi.com/v1/prices/stream with your API key in the Authorization: Bearer header.
Browsers cannot set that header on a WebSocket connection. Send POST /v1/prices/stream-ticket from your server, then connect with ?ticket= and the returned ticket. A ticket can be used once, expires after 30 seconds, and requires an allowed origin. The predictions stream uses the same ticket rules.
Subscribe
The server sends {"type":"ready"}. Send one subscribe message within 5 seconds:
{
"action": "subscribe",
"commodities": [
{
"commodity": "XAU",
"currency": "USD"
}
],
"fx": [
{
"base": "USD",
"currencies": [
"EUR",
"JPY"
]
}
]
}commodities: up to 20 commodity and currency pairs supported byGET /v1/commodities/spot/{commodity}/{currency}.fx: up to 5 base currencies.currenciesis optional: omit it to receive every currency served for that base, or list up to 64 currencies.
The server replies {"type":"subscribed"} and sends every subscribed price once. After that it sends a price only when it changes. Prices are checked about once a second.
To change the subscription, reconnect. A second subscribe message closes the connection with resubscribe_required.
Messages
Price messages carry the same body as the REST endpoints, with the same decimals, fallbacks and stale flags.
| Message | Contents or cadence |
|---|---|
ready | Sent when the connection opens. Subscribe within 5 seconds. |
subscribed | The subscription is accepted. |
commodity | spot holds the body of GET /v1/commodities/spot/{commodity}/{currency}. |
fx | rate holds one item of rates from GET /v1/fx/rates/{base}, with base and market_session. |
heartbeat | Every 30 seconds, with as_of. |
pong | The reply to {"action":"ping"}. |
error | Contains an error code; the connection then closes. |
{
"type": "commodity",
"spot": {
"symbol": "XAU-USD",
"price": "4195.86",
"currency": "USD",
"unit": "troy_ounce",
"timestamp": "2026-10-11T06:25:00Z",
"stale": false
}
}{
"type": "fx",
"base": "USD",
"market_session": "open",
"rate": {
"currency": "EUR",
"rate": "0.8924",
"timestamp": "2026-10-09T00:00:00Z",
"stale": false
}
}Keep the connection alive
Send {"action":"ping"} to get {"type":"pong"}. The server also sends a heartbeat every 30 seconds. Read messages promptly: a client that stops reading for 5 seconds is closed with slow_consumer.
You can send up to 30 frames per connection. Reconnecting sends every subscribed price again; missed changes are not replayed.
Errors
Errors arrive as {"type":"error","code":"..."}, then the connection closes.
| Code | Meaning |
|---|---|
unauthorized | The API key or ticket is missing or invalid. |
plan_upgrade_required | The stream requires a Basic or Pro plan. |
stream_limit | The plan's concurrent stream limit has been reached. |
quota_unavailable | The monthly quota is used up or cannot be checked. |
subscribe_required | No subscribe message was received within 5 seconds of the ready message. |
resubscribe_required | A second subscribe message was sent. Reconnect to change the subscription. |
invalid_command | The message is not a subscribe or ping action. |
invalid_frame | The message is invalid JSON or exceeds the subscription limits. |
commodity_not_supported | A commodity in the subscribe message is not served. |
currency_not_supported | A currency in the subscribe message is not served. |
revoked | The API key was revoked or its connection lease was lost. |
slow_consumer | The client stopped reading messages for 5 seconds. |
command_limit | The client sent more than 30 frames on one connection. |
unavailable | The stream is temporarily unavailable. Reconnect later. |
Docs