sifting/io
Forex & Crypto
5 min readSiftingIO Team

Real-time FX and crypto quotes: REST snapshots and WebSocket streams

How to fetch live bid, ask, mid, and last-trade prices for EUR/USD and BTC/USDT through SiftingIO REST snapshots and WebSocket streams.

Real-time FX and crypto quotes: REST snapshots and WebSocket streams

A price ticker on a dashboard, a risk gauge on a treasury monitor, a fill-quality check inside a strategy loop. They all need the same thing: the current bid, ask, mid, and last-trade for a symbol, refreshed often enough to stay useful and not so often that the data feed becomes the bottleneck. For FX pairs like EUR/USD and crypto pairs like BTC/USDT, two delivery shapes cover the field: a REST snapshot when you want a single point-in-time read, and a WebSocket subscription when you want every quote change pushed as it happens.

This post walks through both, using one credential and one JSON shape across asset classes.

What a snapshot returns#

The REST snapshot endpoints answer one question: what is the latest quote for this symbol right now? They return the current bid, ask, mid (computed as the midpoint), and the most recent trade with its timestamp. They're the right tool when the calling code only needs a single read, a periodic refresh measured in seconds, or a recovery fallback when a streaming client has just connected and needs an initial state to render.

The FX snapshot follows the unified /v1/<asset_class>/<resource> pattern. To pull EUR/USD:

curl -H "Authorization: Bearer $SIFTING_KEY" \
  "https://api.sifting.io/v1/fx/quotes?symbol=EURUSD"

A typical response (verify exact fields against /docs):

{
  "symbol": "EURUSD",
  "asset_class": "fx",
  "ts": "2026-05-11T14:32:01.184Z",
  "bid": 1.0832,
  "ask": 1.0833,
  "mid": 1.08325,
  "last": 1.08325,
  "spread_bps": 0.9
}

The crypto snapshot uses the same shape with a different asset class segment:

curl -H "Authorization: Bearer $SIFTING_KEY" \
  "https://api.sifting.io/v1/crypto/quotes?symbol=BTCUSDT"
{
  "symbol": "BTCUSDT",
  "asset_class": "crypto",
  "venue": "aggregated",
  "ts": "2026-05-11T14:32:01.512Z",
  "bid": 63250.10,
  "ask": 63251.40,
  "mid": 63250.75,
  "last": 63251.00,
  "spread_bps": 2.1
}

The same field names mean a dashboard that already renders FX quotes will render crypto quotes without a parser branch. That's the unified-schema payoff: one Python TypedDict, one TypeScript interface, one set of column names in a dataframe.

A small Python helper that grabs both pairs and prints the mids:

import os, requests

def snapshot(asset, symbol):
    r = requests.get(
        f"https://api.sifting.io/v1/{asset}/quotes",
        params={"symbol": symbol},
        headers={"Authorization": f"Bearer {os.environ['SIFTING_KEY']}"},
        timeout=2.0,
    )
    r.raise_for_status()
    return r.json()

for asset, sym in [("fx", "EURUSD"), ("crypto", "BTCUSDT")]:
    q = snapshot(asset, sym)
    print(f"{q['symbol']:10s} bid={q['bid']} ask={q['ask']} mid={q['mid']}")

That covers the snapshot use case. Anything that needs to refresh faster than once every couple of seconds, though, should not be polling REST.

When to switch to the stream#

Polling a REST endpoint twice a second for a single symbol burns request budget without giving you fresh information between polls. FX majors and the busiest crypto pairs update many times per second. A polling loop either misses ticks (intervals longer than the tick rate) or hammers the rate limit (intervals shorter than it). The WebSocket stream removes both problems by pushing every quote change as it happens, over a single long-lived connection.

The streaming endpoints accept a comma-separated symbol list per asset class:

wss://stream.sifting.io/v1/fx?symbols=EURUSD,USDJPY,GBPUSD
wss://stream.sifting.io/v1/crypto?symbols=BTCUSDT,ETHUSDT,SOLUSDT

Authentication is the same bearer token, sent either in the connection header or as a ?token= query parameter when a header isn't reachable from the client. Each pushed message carries the same field shape as the REST snapshot, so a render function written for snapshot JSON works unmodified on stream JSON.

A minimal Node client that subscribes to two crypto pairs and prints each tick:

import WebSocket from "ws";

const ws = new WebSocket(
  "wss://stream.sifting.io/v1/crypto?symbols=BTCUSDT,ETHUSDT",
  { headers: { Authorization: `Bearer ${process.env.SIFTING_KEY}` } }
);

ws.on("open", () => console.log("connected"));
ws.on("message", (buf) => {
  const q = JSON.parse(buf.toString());
  console.log(`${q.symbol} ${q.bid}/${q.ask} last=${q.last}`);
});
ws.on("close", () => console.log("disconnected"));

Subscribing to FX is the same code with fx swapped for crypto and the symbol list adjusted. The two streams can run side by side in one process; the server multiplexes per-symbol delivery so an idle pair doesn't cost throughput.

A common hybrid pattern: open the WebSocket, then issue a REST snapshot for every subscribed symbol immediately. The snapshot fills the table while the first stream tick is still in flight, so the dashboard never shows an empty cell. After that, the stream takes over and snapshots are only used as a recovery tool on reconnect.

Common pitfalls#

Three issues come up repeatedly when wiring this kind of feed.

First, FX symbol formatting. Some APIs accept EUR/USD, some accept EURUSD, some accept EUR_USD. SiftingIO uses the unslashed six-character form (EURUSD, USDJPY, GBPUSD) for snapshots and streams. Passing EUR/USD returns a 400 with unknown symbol, which reads like a permissions or coverage issue but is purely formatting. Crypto pairs follow the venue convention (BTCUSDT, ETHUSDT, SOLUSDC), matching what the major centralized exchanges quote.

Second, treating the WebSocket like an HTTP request. A streaming connection lives for hours or days, and any network in between (load balancers, NAT, corporate proxies) may drop it silently. Production clients need a heartbeat (the server pings every 30 seconds; respond with pong, otherwise the connection is torn down) and an exponential-backoff reconnect (start at 1 second, cap at 60, with a small jitter). When reconnecting, pull a REST snapshot for every subscribed symbol to fill the gap that opened between disconnect and reconnect. Without that, the displayed quote can be many seconds stale and nothing in the message stream will reveal it.

Third, confusing the last-trade price for the executable mid. The last field is the most recent print. For an illiquid crypto pair, the last trade can be minutes old even though bid and ask are still tight and current. Using last as a valuation input is fine for a tape display but wrong for any portfolio mark or pre-trade check; the mid (or, more conservatively, the bid for a long position and the ask for a short) is the right number. The REST snapshot returns both, so the choice is explicit in the caller, not buried in the feed.

Rate limits and headers worth watching#

Every REST response carries the standard rate-limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. A polling loop that ignores them will eventually receive a 429 Too Many Requests instead of a quote, often at the worst possible moment. Reading X-RateLimit-Remaining and backing off when it drops below ten percent of the limit is cheap insurance. The WebSocket has no per-message budget, so any code path that needs sustained high-frequency updates belongs on the stream rather than on a tight snapshot loop.

Mid prices and last trades for FX and crypto sit behind the same auth and the same JSON shape. Snapshots for occasional reads, streams for continuous flow, and a snapshot-on-reconnect bridge between the two. That's the whole pattern.

Read the docs

Tagsfxcryptowebsocketrest-apireal-time-datamarket-dataquotesstreamingbid-askdeveloper-tutorial
All postsLast updated May 12, 2026
Keep reading

Related posts