sifting/io
Streaming · v1

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.

connect · wscat
wscat -c "wss://stream.sifting.io/ws/v1?key=$KEY"
first frame · server → client
{  "f": "ack",  "op": "auth",  "tier": "free",  "max_conn": 1,  "max_subs": 5,  "active_conn": 1}
alternate · auth as a message
// 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.

  • auth

    First message; skip if you used ?key= in the URL.

    { "op": "auth", "key": "sft_..." }
  • subscribe

    Add channels to the stream. Multiple subscribes on one connection are fine.

    { "op": "subscribe", "product": "cex", "symbols": ["BTCUSD", "ETHUSD"] }
  • unsubscribe

    Stop receiving the listed symbols.

    { "op": "unsubscribe", "product": "cex", "symbols": ["BTCUSD"] }
  • ping

    Application-level keepalive. Server replies with { f:"pong" }.

    { "op": "ping" }

Products

Pass one of these as the product field when you subscribe.

Products
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"
    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"
    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.

sequence
 { "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.

example close · client → server idle
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_op

    Unknown op or malformed JSON.

  • auth_required

    Tried to subscribe before authenticating.

  • auth_timeout

    Didn't send auth within the handshake window.

  • auth_failed

    Key invalid, revoked, or not entitled.

  • unknown_product

    product wasn't one of cex | dex | fx | us | tvl.

  • max_connections

    Tier connection cap reached. Body carries limit.

  • max_subscriptions

    Tier subscription cap reached. Body carries limit.

  • internal

    Server-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.