A cryptocurrency API has to answer a question that sounds simple: what is BTCUSD worth right now? Ask three large trading venues at the same instant and you'll get three different numbers. Venues disagree by a few basis points in calm markets and by much more during volatility, thin liquidity, or an outage at one of them. Any app that shows a crypto price, values a portfolio, or fires alerts has to decide which number to trust. SiftingIO's answer is aggregation: one consensus price per symbol, computed across multiple independent venues, delivered in real time over WebSocket and historically over REST, under one API key.
Why an aggregated cryptocurrency API beats a single-venue price#
Most crypto price feeds pass through whatever one venue reports. SiftingIO's crypto feed works differently. Every published price is a volume- and reputation-weighted median across venues, computed by a four-stage pipeline documented publicly on the data methodology page.
The pipeline validates first, so stale observations and hard outliers never reach the estimator. Surviving observations are scored with median absolute deviation, a statistic that flags a venue printing away from its peers. Each venue also carries a reputation, updated continuously, with detection for frozen feeds: the failure mode where a venue keeps sending messages while its price stops moving against a moving market. The final aggregation weights each venue by real traded volume times that reputation, so the venues actually carrying size dominate the output, and the published number reflects where volume genuinely traded rather than where one quiet order book happened to sit.
A median is a deliberate choice. A volume-weighted average can be dragged by one bad print; a median can't. A majority of venues would have to err in the same direction before the published price is wrong. That bound is honest in both directions: the estimator survives a minority of bad feeds, and no median survives a coordinated majority. The output is a synthetic reference value for research, dashboards, alerting, and cross-checking the price your execution venue shows you. It does not represent executable depth on any single venue, and SiftingIO is a data provider, so nothing in the feed routes orders.
Quality is visible rather than silent. Every tick carries three timestamps (source, ingest, publish) plus an explicit quality flag, so a consumer can tell a Normal print from a Degraded one instead of guessing.
Historical crypto data: OHLCV bars back to 2013#
The same aggregation feeds the historical side. GET /v1/hist/crypto/{symbol}/bars returns OHLCV bars at intervals from 1m up to 1mo, with selected series beginning as early as 2013 and 15+ years of history across the catalog. Depth is plan-gated: one month on the free tier, one year on Builder, full available history on Pro and above (see pricing).
The endpoint requires gzip. Without an Accept-Encoding: gzip header it returns 406 with the error code gzip_required, so use curl's --compressed flag:
curl --compressed -H "X-API-Key: $SIFTING_KEY" \
"https://api.sifting.io/v1/hist/crypto/BTCUSD/bars?interval=1h&start=2026-08-01&end=2026-08-31"
A response page looks like this, where v is base-asset volume with fractions preserved and t is Unix epoch milliseconds in UTC:
{
"data": [
{ "t": 1785542400000, "o": 109201.4, "h": 109480.0, "l": 109115.2, "c": 109322.7, "v": 84.5031 }
],
"meta": { "symbol": "BTCUSD", "interval": "1h", "as_of": "2026-08-31T09:12:44Z", "next_cursor": "MTc4NTU0NjAwMQ" }
}
start is required (YYYY-MM-DD or RFC 3339), limit defaults to 1000 bars per page with a maximum of 5000, and pagination is cursor-based: keep requesting with cursor=meta.next_cursor until it comes back null. The full parameter table is in the historical crypto docs.
Real-time crypto prices over WebSocket#
For live data, connect to wss://stream.sifting.io/ws/v1?key=$SIFTING_KEY and subscribe to the cex product:
{ "op": "subscribe", "product": "cex", "symbols": ["BTCUSD", "ETHUSD"] }
The server acknowledges, immediately replays the last cached value for each symbol so your UI never starts blank, then streams live updates:
{ "f": "ack", "op": "subscribe", "product": "cex", "symbols": ["BTCUSD", "ETHUSD"] }
{ "f": "tick", "class": "cex", "s": "BTCUSD", "p": 109322.7, "P": 0.0140,
"b": 109321.9, "B": 0.42, "a": 109323.6, "A": 0.35, "t": 1787654321420 }
Frames discriminate on f. In a tick, p and P are last trade price and size, b/B and a/A are best bid and ask with sizes, and t is epoch milliseconds. The product page states sub-100ms median API delivery, and the consensus-price refresh cadence scales by tier, from 1 Hz on Free up to 10 Hz on Ultra. Those are fair-price update rates. The feed is a reference layer and makes no execution-latency promise.
If a persistent socket is more than you need, poll the REST snapshot instead:
curl -H "X-API-Key: $SIFTING_KEY" "https://api.sifting.io/v1/last/quote/crypto/BTCUSD"
It returns { "s": "BTCUSD", "b": "109321.9", "B": "0.42", "a": "109323.6", "A": "0.35", "t": 1787654321000 }. Details are in the live quote docs, and there's a full walkthrough in the earlier post on building a correct quote client.
Common pitfalls#
Three things trip up first integrations.
Gzip on heavy endpoints. The /v1/hist/* bar endpoints refuse to serve uncompressed responses. A plain fetch or requests.get usually negotiates gzip automatically, but a hand-rolled HTTP client that strips Accept-Encoding sees 406 gzip_required, and it reads like a server bug. It isn't. Add the header.
Quote prices are strings. /v1/last/quote returns bid and ask as JSON strings to preserve precision. JavaScript that computes quote.a - quote.b gets away with it through implicit coercion until a template string renders "109323.6" somewhere in the UI. Cast explicitly on ingest. Related: a 503 stale_snapshot response means the cached quote is older than the freshness threshold (default 5s); the body includes last_t and server_now, so treat it as a data-quality signal. Retrying in a tight loop won't make the data fresher.
The idle timer only counts your frames. The server closes a WebSocket after 90 seconds without a client frame, and inbound ticks don't reset that timer. A quiet subscriber receiving thousands of ticks per minute still gets disconnected. Send { "op": "ping" } at least every 60 seconds and reconnect with backoff on close.
Trying it against your current feed#
Paid tiers carry uptime SLAs (99.5% on Builder, 99.9% on Pro and above). Those figures describe availability; data quality is handled separately, through the per-tick flags and timestamps described above. The free tier includes 10,000 REST calls a month, one WebSocket connection with five symbol subscriptions, one month of history, and no credit card. That's enough to wire BTCUSD end to end and judge the consensus feed against whatever your app trusts today.



