Browse all documentation

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:

Subscribe message
{
  "action": "subscribe",
  "commodities": [
    {
      "commodity": "XAU",
      "currency": "USD"
    }
  ],
  "fx": [
    {
      "base": "USD",
      "currencies": [
        "EUR",
        "JPY"
      ]
    }
  ]
}
  • commodities: up to 20 commodity and currency pairs supported by GET /v1/commodities/spot/{commodity}/{currency}.
  • fx: up to 5 base currencies. currencies is 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.

MessageContents or cadence
readySent when the connection opens. Subscribe within 5 seconds.
subscribedThe subscription is accepted.
commodityspot holds the body of GET /v1/commodities/spot/{commodity}/{currency}.
fxrate holds one item of rates from GET /v1/fx/rates/{base}, with base and market_session.
heartbeatEvery 30 seconds, with as_of.
pongThe reply to {"action":"ping"}.
errorContains an error code; the connection then closes.
Commodity price message
{
  "type": "commodity",
  "spot": {
    "symbol": "XAU-USD",
    "price": "4195.86",
    "currency": "USD",
    "unit": "troy_ounce",
    "timestamp": "2026-10-11T06:25:00Z",
    "stale": false
  }
}
FX rate message
{
  "type": "fx",
  "base": "USD",
  "market_session": "open",
  "rate": {
    "currency": "EUR",
    "rate": "0.8924",
    "timestamp": "2026-10-09T00:00:00Z",
    "stale": false
  }
}

Spot prices reference · FX rates reference

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.

CodeMeaning
unauthorizedThe API key or ticket is missing or invalid.
plan_upgrade_requiredThe stream requires a Basic or Pro plan.
stream_limitThe plan's concurrent stream limit has been reached.
quota_unavailableThe monthly quota is used up or cannot be checked.
subscribe_requiredNo subscribe message was received within 5 seconds of the ready message.
resubscribe_requiredA second subscribe message was sent. Reconnect to change the subscription.
invalid_commandThe message is not a subscribe or ping action.
invalid_frameThe message is invalid JSON or exceeds the subscription limits.
commodity_not_supportedA commodity in the subscribe message is not served.
currency_not_supportedA currency in the subscribe message is not served.
revokedThe API key was revoked or its connection lease was lost.
slow_consumerThe client stopped reading messages for 5 seconds.
command_limitThe client sent more than 30 frames on one connection.
unavailableThe stream is temporarily unavailable. Reconnect later.