Live · WebSocket streaming.
One connection, many subscriptions. Connect with your API key in the query string, then send ops. Every server frame carries an f discriminator: branch on f first, the rest of the keys depend on it.
Connect & authenticate
Pass your API key in the ?key= query parameter. Browsers can't set custom headers on the WS handshake, so the query parameter is the only path. The server replies immediately with an ack op:auth that echoes your tier and the limits attached to it.
wscat -c "wss://stream.sifting.io/ws/v1?key=$KEY"{ "f": "ack", "op": "auth", "tier": "free", "max_conn": 1, "max_subs": 5, "active_conn": 1}// If you connected without ?key=, send auth as your first message:{ "op": "auth", "key": "sft_..." }// You have ~5s before the server closes the connection with auth_timeout.Client → server ops
Send any of these as a JSON text frame. The server replies with { "f":"ack" } on success, or { "f":"error" } with a stable code on failure.
authFirst message; skip if you used ?key= in the URL.
{ "op": "auth", "key": "sft_..." }subscribeAdd channels to the stream. Multiple subscribes on one connection are fine.
{ "op": "subscribe", "product": "cex", "symbols": ["BTCUSD", "ETHUSD"] }unsubscribeStop receiving the listed symbols.
{ "op": "unsubscribe", "product": "cex", "symbols": ["BTCUSD"] }pingApplication-level keepalive. Server replies with { f:"pong" }.
{ "op": "ping" }
Products
Pass one of these as the product field when you subscribe.
- cex
- Centralised crypto venues (top global spot venues)
- dex
- On-chain swaps. Symbols are chain-prefixed: eth:WETH-USDC
- fx
- Forex pairs (EURUSD, GBPUSD, …)
- us
- US equities (AAPL, NVDA, …)
- tvl
- Pool TVL deltas. Same chain:PAIR shape as dex.
Server → client frames
Every server frame begins with f. Branch on f first; the remaining keys depend on it.
f = "ack"Your op succeeded. Echoes back the op and any relevant fields.
example 1{ "f":"ack", "op":"auth", "tier":"pro", "max_conn":10, "max_subs":1000, "active_conn":1 }example 2{ "f":"ack", "op":"subscribe", "product":"cex", "symbols":["BTCUSD"] }example 3{ "f":"ack", "op":"unsubscribe", "product":"cex", "symbols":["BTCUSD"] }f = "pong"Reply to your ping.
frame · pong{ "f":"pong" }f = "tick"Price stream for cex / dex / fx / us. class reflects the venue type. DEX ticks include a chain field (e.g. "chain":"eth").
frame · tick{ "f": "tick", "class": "fx", "s": "EURUSD", "p": 1.16934, "P": 85790, "b": 1.16925, "B": 510300, "a": 1.16943, "A": 585025, "t": 1778019852426}Field key
f = "tick"Descriptionf = "tick"- s
- symbol
- p / P
- last trade price / size
- b / B
- best bid price / size
- a / A
- best ask price / size
- t
- timestamp · int64 (Unix epoch ms)
- class
- venue type: cex | dex | fx | us
- chain
- DEX only: eth | base | arbitrum | bsc | polygon
f = "tvl"TVL stream for the tvl product. Same chain:PAIR addressing as DEX.
frame · tvl{ "f": "tvl", "chain": "eth", "s": "WETH-USDC", "usd": 12345678.9, "r0": 1234.5, "r1": 5678.9, "n": 42, "t": 1778019852426}Field key
f = "tvl"Descriptionf = "tvl"- s
- pair (TOKEN0-TOKEN1)
- usd
- total USD value locked across pools
- r0 / r1
- summed reserves of token0 / token1
- n
- number of pools contributing
- t
- timestamp · int64 (Unix epoch ms)
f = "error"Your op failed. code is stable and machine-readable; switch on it. The connection stays open. Fix the op and try again.
frame · error{ "f":"error", "code":"max_subscriptions", "message":"...", "limit":1000 }
Snapshot on subscribe
When you subscribe, the server emits one snapshot tick (or tvl) per channel, the last known value from the engine's snapshot cache, BEFORE the live stream starts. The shape is identical to live frames, so your handler doesn't need to distinguish.
→ { "op":"subscribe", "product":"fx", "symbols":["EURUSD"] }← { "f":"tick", ...EURUSD snapshot from cache... }← { "f":"ack", "op":"subscribe", "product":"fx", "symbols":["EURUSD"] }← { "f":"tick", ...live... }← { "f":"tick", ...live... }Idle timeout & keepalive
If the server sees no client frames for 90 seconds, it closes the connection with WebSocket close code 1000 and reason "idle timeout". Send { "op": "ping" } at least every 60 seconds, or rely on your subscriptions producing inbound traffic. Note that inbound traffic does not reset the idle timer, only client → server frames do.
Disconnected (code: 1000, reason: "idle timeout")Error codes
Errors arrive as { "f":"error", "code":"...", "message":"..." }. The connection stays open. Fix the op and try again. Switch on code; the message is for humans.
bad_opUnknown op or malformed JSON.
auth_requiredTried to subscribe before authenticating.
auth_timeoutDidn't send auth within the handshake window.
auth_failedKey invalid, revoked, or not entitled.
unknown_productproduct wasn't one of cex | dex | fx | us | tvl.
max_connectionsTier connection cap reached. Body carries limit.
max_subscriptionsTier subscription cap reached. Body carries limit.
internalServer-side error. Reconnect with backoff.
Pair with REST
Most clients use REST for history and configuration, WebSocket for the live tape. See the API reference for snapshots and historical bars.