Server-Sent Events

Server-Sent Events (SSE) deliver the same real-time rates, exchange rates, and tickers as our WebSockets, over a plain HTTP response that stays open. Use it when a WebSocket client is not available or not wanted: from the browser's built-in EventSource, from curl, or from any HTTP library that can read a response as it arrives.

An SSE stream is a regular GET request. The server answers with content-type: text/event-stream and then keeps writing one line per message for as long as the connection stays open. The subscriptions, the throttle, the message contents, and the plan requirements are identical to the corresponding WebSocket path, so you can switch between the two transports without changing what you receive.

Authentication

Send your API key in the x-access-token header, exactly as for the REST endpoints. This is the preferred method for every client that can set headers: curl, fetch, and server-side HTTP libraries.

curl -N https://api.coinranking.com/v2/real-time/sse/rates?list=all -H 'x-access-token: your-api-key'

The browser's built-in EventSource cannot set request headers. For that case only, the key is also accepted as a query parameter, just like on the WebSockets:

https://api.coinranking.com/v2/real-time/sse/rates?x-access-token=your-api-key

A key in the query string ends up in browser histories, access logs, and referrer headers, so use the header wherever you can. For a public website, keep the key on your server and proxy the stream through your own origin.

Connection Setup

Professional These streams require the professional plan or higher
Real-Time Dex DEX data in the tickers and exchange rates streams requires the Real-time DEX data add-on.

Open a stream by requesting one of the three paths. Each path mirrors the WebSocket path of the same name:

https://api.coinranking.com/v2/real-time/sse/rates

https://api.coinranking.com/v2/real-time/sse/exchange-rates

https://api.coinranking.com/v2/real-time/sse/tickers

  • Rates
    /sse/rates - Aggregated price and 24 hour volume per coin.
  • Exchange Rates
    /sse/exchange-rates - Price and 24 hour volume of a coin on a specific exchange.
  • Tickers
    /sse/tickers - Trading pair data per market, including base and quote volumes.

The stream is one-way. Unlike the WebSockets, there are no subscription messages: everything you want to receive is specified in the URL when you open the stream. To change your subscriptions, close the stream and open a new one with the new parameters.

API Usage

Opening a stream consumes one API request from your plan, however long the stream stays open. The messages themselves do not consume requests. Every reconnect opens a new stream and therefore consumes one request again, so avoid reconnect loops that retry without a delay.

URL Query Parameters

The parameters are the same as for the WebSocket path with the same name. Each path accepts exactly one subscription parameter per stream; you cannot mix them.

Path Subscription parameter (choose one)
/sse/rates currency-uuids[] or list=all
/sse/exchange-rates references[], exchange-uuids[], currency-uuids[], or list=all
/sse/tickers market-uuids[], exchange-uuids[], currency-uuids[], or list=all
Parameter Description
currency-uuids[] Array

Subscribe to specific coins. On /sse/rates you receive the aggregated rate of each coin; on /sse/exchange-rates and /sse/tickers you receive the data of each coin on every exchange that lists it.

You can find UUIDs for all our coins in the reference currency endpoint.

Note: Use array notation in the URL: currency-uuids[]=Qwsogvtv82FCd

Size range: 1-100

Example:
https://api.coinranking.com/v2/real-time/sse/rates?currency-uuids[]=Qwsogvtv82FCd&currency-uuids[]=razxDUgYGNAdQ
market-uuids[] Array

/sse/tickers only. Subscribe to specific trading pairs. You can find market UUIDs in our markets endpoint.

Note: Use array notation in the URL: market-uuids[]=dj23kKgETIie

Size range: 1-100

Example:
https://api.coinranking.com/v2/real-time/sse/tickers?market-uuids[]=dj23kKgETIie
exchange-uuids[] Array

/sse/exchange-rates and /sse/tickers only. Subscribe to everything on specific exchanges. You can find exchange UUIDs in our exchanges endpoint.

Note: Use array notation in the URL: exchange-uuids[]=-zdvbieRdZ&exchange-uuids[]=TjMe3QlK0

Size range: 1-100

Example:
https://api.coinranking.com/v2/real-time/sse/exchange-rates?exchange-uuids[]=-zdvbieRdZ
references[] Array

/sse/exchange-rates only. Subscribe to specific exchange-currency pairs. Each reference must follow the format exchangeUuid_currencyUuid.

Note: Use array notation in the URL: references[]=-zdvbieRdZ_Qwsogvtv82FCd

Size range: 1-100

Example:
https://api.coinranking.com/v2/real-time/sse/exchange-rates?references[]=-zdvbieRdZ_Qwsogvtv82FCd
list String

Subscribe to everything on the path instead of a selection. This is a high-volume stream, so make sure your client keeps up with it; a client that stops reading is disconnected.

On /sse/exchange-rates and /sse/tickers, list=all includes decentralized exchanges only with the Real-time DEX data add-on. Without the add-on the stream opens normally but carries centralized exchanges only.

Allowed values: all

Example:
https://api.coinranking.com/v2/real-time/sse/rates?list=all
throttle (optional) String

With the throttle parameter, you prevent getting overloaded with messages. Throttling works per coin on /sse/rates and per exchange-currency pair on the other paths: the same item is not pushed more than once within the specified duration.

Default value: 1s
Allowed values: 1s 10s

Example:
https://api.coinranking.com/v2/real-time/sse/rates?throttle=10s

Code examples

The fetch and Python examples read the status and error body of a rejected request before they start streaming. The EventSource example is the quickest way to try the stream from a browser, but it puts the API key in the page and cannot see why a request was rejected; see Using the stream from a website and Error handling in the browser before using it in an integration.

Stream Format

A successful request is answered with status 200, content-type: text/event-stream; charset=utf-8, cache-control: no-cache, no-transform, access-control-allow-origin: *, and the usual ratelimit-* headers. The body follows the standard Server-Sent Events format: lines grouped into messages, with a blank line ending each message. Lines end in \n today; a parser should accept \r\n as well, as the SSE specification allows either.

retry: 5000

data: {"type":"confirmation","topic":"rate","throttle":"1s","subscriptions":["Qwsogvtv82FCd"]}

data: {"currencyUuid":"Qwsogvtv82FCd","price":"77348.906368714524","volume":"19604392582.842590","type":"rate"}

:ping

  • retry: 5000 is sent once, at the start of the stream. It tells EventSource to wait five seconds before reconnecting when the stream ends.
  • data: lines each carry one JSON message: the same message the WebSocket would send as one frame. Messages have no event: name, so EventSource.onmessage receives all of them. The type field tells them apart.
  • :ping is a comment line sent every 25 seconds to keep the connection alive. Ignore it; EventSource does so automatically.

There are no id: lines, so Last-Event-ID replay is not supported: a reconnect resumes with the next update, not with what was missed. The stream carries updates only and does not send a snapshot on connect. If your client needs a starting value before the first update arrives, fetch it from the coins, exchange coin listings, or markets endpoints first.

Responses

The stream carries two kinds of messages:

Confirmation Messages

The first message on every stream confirms what you are subscribed to and with which throttle. It has the same shape as the WebSocket confirmation:

json
{ "type": "confirmation", "topic": "rate", "throttle": "10s", "subscriptions": ["Qwsogvtv82FCd", "razxDUgYGNAdQ"] }

The topic is rate, exchange-rate, or ticker, matching the path you opened.

Data Messages

After the confirmation, you receive a data message whenever new data is available for one of your subscriptions. The fields are identical to the WebSocket messages of the same path, plus a type field.

A rate from /sse/rates:

json
{ "type": "rate", "currencyUuid": "Qwsogvtv82FCd", "price": "125423.835504191964312353", "volume": "6270550403.287645846237386287" }

An exchange rate from /sse/exchange-rates:

json
{ "type": "exchange-rate", "exchangeUuid": "-zdvbieRdZ", "currencyUuid": "Qwsogvtv82FCd", "exchangeType": "cex", "price": "125423.835504191964312353", "volume": "6270550403.287645846237386287" }

A ticker from /sse/tickers:

json
{ "type": "ticker", "marketUuid": "dj23kKgETIie", "exchangeUuid": "-zdvbieRdZ", "baseUuid": "Qwsogvtv82FCd", "quoteUuid": "cpjRxjFYD", "exchangeType": "cex", "close": "124590.91", "price": "124590.91", "baseVolume": "24290.53392", "quoteVolume": "3036047751.1820064", "volume": "3036047751.1820064", "filters": [] }

Response Fields

Prices and volumes are sent as strings to preserve precision. As with the WebSockets, all prices and US dollar volumes are in US dollars; other reference currencies are not supported.

PropertyDescription
type String

The kind of message: confirmation, rate, exchange-rate, or ticker. Data messages on a stream all have the type matching its path.

The remaining fields are documented per path, and mean the same thing here:

  • Rates: currencyUuid, price, volume
  • Exchange Rates: exchangeUuid, currencyUuid, exchangeType, price, volume
  • Tickers: marketUuid, exchangeUuid, baseUuid, quoteUuid, exchangeType, price, close, baseVolume, quoteVolume, volume, filters

Errors

A request is validated in full before the stream starts. If anything is wrong, the response is a normal HTTP error with a JSON body instead of a stream, and nothing is streamed. The body has the same shape as every other error response of the API: a status (fail for 4xx, error for 5xx), a machine-readable code, and a human-readable message.

json
HTTP/1.1 400 Bad Request { "status": "fail", "code": "INVALID_THROTTLE", "message": "Throttle \"5s\" is not supported" }

Unknown UUIDs reject the whole request. Validation is all-or-nothing, the same as on the WebSockets: if any UUID in your subscription does not exist, the request is rejected and the message lists the UUIDs that could not be found. Remove or correct them and open the stream again.

json
HTTP/1.1 400 Bad Request { "status": "fail", "code": "UNKNOWN_CURRENCY_UUID", "message": "Could not find coins with the following UUIDs: notarealuuid" }

Match on code rather than on message; the messages may be reworded.

Status Code Meaning
400 INVALID_THROTTLE The throttle is not 1s or 10s.
400 INVALID_SUBSCRIPTION No subscription parameter was given, more than one was given, or a references[] entry is not in the exchangeUuid_currencyUuid format. Unlike the WebSockets, a stream without subscriptions is refused, because there is no way to subscribe after opening it.
400 TOO_MANY_UUIDS More than 100 UUIDs in one subscription.
400 UNKNOWN_CURRENCY_UUID
UNKNOWN_MARKET_UUID
UNKNOWN_EXCHANGE_UUID
One or more UUIDs do not exist. The whole request is rejected and the message lists the offending UUIDs.
401 UNAUTHORIZED The API key does not exist.
403 FORBIDDEN No API key was sent, or the key belongs to a plan without real-time access.
403 DEX_ADD_ON_REQUIRED DEX markets or exchanges were requested without the Real-time DEX data add-on.
404 NOT_FOUND Unknown path. Note that /v2/real-time/rates without /sse/ is the WebSocket path, which also answers 404 to a plain HTTP request.
429 RATE_LIMIT_EXCEEDED Your rate limit is exhausted. Wait for the window in the ratelimit-reset header before reconnecting.
500 INTERNAL_SERVER_ERROR Something went wrong on our side. The body is {"status":"error","code":"INTERNAL_SERVER_ERROR","message":"…"}, and the message carries a reference you can quote when contacting support.

Using the stream from a website

A page that calls the API directly has to put the key in the URL, where every visitor can read it, and it cannot see why a request was refused. Production sites avoid both by proxying the stream through their own origin: a small route on your server adds the key, opens the upstream stream, and pipes it back to the browser unchanged. The browser then uses EventSource against your own domain, so there is no key in the page and no cross-origin request.

JavaScript
// Node.js 18+. GET /stream/rates?currency-uuids[]=... on your server becomes // a Coinranking stream with the key added here, never in the browser. import http from 'node:http'; import { Readable } from 'node:stream'; http.createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost'); if (url.pathname !== '/stream/rates') return res.writeHead(404).end(); // When the tab goes away, end the upstream stream too. The response's close // event fires when the browser disconnects. const controller = new AbortController(); res.once('close', () => controller.abort()); const upstream = await fetch( `https://api.coinranking.com/v2/real-time/sse/rates${url.search}`, { headers: { 'x-access-token': process.env.COINRANKING_API_KEY }, signal: controller.signal } ).catch(() => null); if (!upstream) { // Either the tab closed before Coinranking answered, or Coinranking is unreachable if (!res.destroyed) res.writeHead(502).end(); return; } // A refused request (JSON, non-200) passes through as-is, so the browser can read it res.writeHead(upstream.status, { 'content-type': upstream.headers.get('content-type'), 'cache-control': 'no-cache, no-transform', }); Readable.fromWeb(upstream.body).on('error', () => res.end()).pipe(res); }).listen(3000);

Three things to keep in mind when you build this:

  • Do not buffer. Compression middleware and reverse proxies often hold the response until it ends, which for a stream is never. Disable compression for the route and, behind nginx, set proxy_buffering off.
  • Every browser connection is one upstream stream, and therefore one API request. For a site with many visitors, open one upstream stream per distinct subscription and broadcast its messages to all browsers that want it, instead of one upstream stream per tab.
  • Only forward the query parameters you intend to expose. The route above passes the query string through unchanged, which lets any visitor subscribe to anything your key allows. Allow-list the UUIDs or build the upstream URL yourself.

Because the proxy sees the real status and body, it can also turn a refusal into whatever your page finds easiest to handle, for example a single data: frame on a 200. That side-steps the EventSource limitation described in the next section.

Error handling in the browser

The browser's built-in EventSource cannot read the status code or the body of a response that is not a 200 stream. When a request is rejected, it fires a bare error event with readyState === EventSource.CLOSED and does not retry. From that event alone, your page cannot tell a wrong UUID from a wrong API key or an exhausted rate limit. A dropped connection looks different: readyState stays EventSource.CONNECTING and the browser reconnects by itself.

For real integrations, read the stream with fetch instead. Reading response.body and splitting it on blank lines takes a few lines, as shown in the JavaScript (fetch) tab above. It works in browsers as well as Node.js, Deno, and Bun, it exposes the status, code, and message of every rejected request, and it lets you send the API key in a header instead of the URL. If you prefer a library, @microsoft/fetch-event-source does the same with reconnects handled for you:

JavaScript
import { fetchEventSource } from '@microsoft/fetch-event-source'; await fetchEventSource('https://api.coinranking.com/v2/real-time/sse/rates?currency-uuids[]=Qwsogvtv82FCd', { headers: { 'x-access-token': 'your-api-key' }, async onopen(response) { if (response.ok) return; // Rejected before the stream started; throwing stops the retries const { code, message } = await response.json(); throw new Error(`HTTP ${response.status} ${code}: ${message}`); }, onmessage(event) { console.log(JSON.parse(event.data)); }, });

Diagnosing a rejected EventSource request

If you keep using EventSource, treat an error event with readyState === EventSource.CLOSED right after opening as a rejected request. Then do a single plain fetch of the same URL to read the status, code, and message. If that request unexpectedly succeeds, abort it right away so you do not leave a second stream open.

JavaScript
const url = 'https://api.coinranking.com/v2/real-time/sse/rates?x-access-token=your-api-key&currency-uuids[]=Qwsogvtv82FCd'; const source = new EventSource(url); source.onmessage = (event) => { console.log(JSON.parse(event.data)); }; source.onerror = async () => { // CONNECTING means the connection dropped; the browser retries by itself if (source.readyState !== EventSource.CLOSED) return; // CLOSED means the request was rejected. EventSource hides the reason, so // fetch the same URL once to read the status and the error body. const controller = new AbortController(); const response = await fetch(url, { signal: controller.signal }); if (response.ok) { controller.abort(); // It succeeded this time: do not leave the stream open return; } const { code, message } = await response.json(); console.error(`HTTP ${response.status} ${code}: ${message}`); };

Reconnecting

There is no maximum stream duration. A stream ends in three cases: the real-time service is deployed or restarted, which ends every open stream cleanly; you close it; or your client stops reading. In the last case the server terminates the stream once two consecutive :ping heartbeats are left unread, roughly a minute, so a consumer that cannot keep up with a list=all stream is cut off rather than buffered without bound. Idle timeouts on the way do not apply as long as the heartbeats flow.

The subscriptions live in the URL, so reconnecting with the same URL restores everything; there is no state to rebuild. EventSource reconnects on its own after the five seconds announced by retry: 5000. Other clients should reconnect after a similar short delay. Remember that each reconnect consumes one API request, so do not retry a rejected request without fixing its cause, and back off when the response is a 429.