Aperiodic
Catalog
MarketsLive
Pricing
Get Started
API Documentation

Live WebSocket Feed

Every metric your plan covers, streamed as it is produced, on the same API key as the REST API. Included in every self-serve plan on sale, from Pro; Institutional can also take it over Kafka.
Endpoint
wss://stream.aperiodic.io/v1/stream
Auth
X-API-KEY header
Frames
JSON text
Delivery
At most once

On this page

  1. Quickstart
  2. Connecting
  3. Channels
  4. Messages
  5. Limits
  6. Errors and close codes
  7. Delivery and reconnecting
  8. Known limitations

Quickstart

You need a plan that streams and its API key, which is on Live Streams once you are signed in. Each example streams BTC from ohlcv.binance-futures.1m until you press Ctrl+C.

Python client · pip install "aperiodic[stream]"
import os

from aperiodic import stream

for message in stream(
    api_key=os.environ["APERIODIC_API_KEY"],
    dataset="ohlcv",
    exchange="binance-futures",
    interval="1m",
    symbols=["perpetual-BTC-USDT:USDT"],  # omit for every symbol on your plan
):
    print(message.channel, message.snapshot, message.data)

The Python client and the CLI reconnect and resubscribe on their own.

CLI
export APERIODIC_API_KEY='<your key>'
aperiodic stream ohlcv --exchange binance-futures --interval 1m --symbols perpetual-BTC-USDT:USDT

Any WebSocket client works. With wscat, type the second line once connected:

wscat
wscat -c "wss://stream.aperiodic.io/v1/stream" \
  -H "X-API-KEY: $APERIODIC_API_KEY"
> {"op":"subscribe","id":"c1","channels":[{"dataset":"ohlcv","exchange":"binance-futures","interval":"1m","symbols":["perpetual-BTC-USDT:USDT"]}]}
Python · pip install websockets
import asyncio, json, os

import websockets

async def main():
    async with websockets.connect(
        "wss://stream.aperiodic.io/v1/stream",
        additional_headers={"X-API-KEY": os.environ["APERIODIC_API_KEY"]},
    ) as ws:
        await ws.send(json.dumps({"op":"subscribe","id":"c1","channels":[{"dataset":"ohlcv","exchange":"binance-futures","interval":"1m","symbols":["perpetual-BTC-USDT:USDT"]}]}))
        async for message in ws:
            frame = json.loads(message)
            if "channel" in frame:
                print(frame["channel"], frame["data"])
            elif frame.get("op") in ("subscribed", "unsubscribed", "error"):
                print(frame)

asyncio.run(main())

Connecting

Open a WebSocket to wss://stream.aperiodic.io/v1/stream with your API key in the X-API-KEY header. The key is never accepted in the URL, where it would end up in logs and proxies. Browsers cannot set headers on a WebSocket, so the feed is for servers, notebooks and scripts, not for web pages.

The key is checked before the upgrade, so a refused connection gets an ordinary HTTP status with a JSON error message instead of a socket:

StatusMeaning
401No X-API-KEY header, or the key is not valid.
403The key is valid, but its plan does not stream. Every plan on sale does: upgrade to stream.
426The request was not a WebSocket upgrade.
429Your account already holds as many connections as its plan allows. Close one, or wait for a dropped one to expire (see Limits).
503Streaming is temporarily unavailable. Retry with backoff.

A new plan or a new key reaches the feed within 60 seconds. If a connection is refused right after you upgrade, wait a minute and try again.

Channels

A channel is one dataset on one exchange at one interval, named dataset.exchange.interval with the catalog's names: ohlcv.binance-futures.1m, open_interest.okx-perps.1m. Symbols are Atlas IDs, the same as in the REST API: perpetual-BTC-USDT:USDT. Rows arrive when each bar closes, so a 1h channel sends once an hour.

These datasets stream. Intervals 15s and 30s are Institutional only; every other plan gets the rest of each list.

DatasetExchangesIntervalsFrom
flow
Flow Metrics
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
trade_size
Trade Size
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
range
Price Range & Distribution
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
impact
Impact Metrics
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
updownticks
Up / Down Tick Metrics
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
run_structure
Trade Run Structure
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
returns
Returns & Volatility
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
slippage
Slippage
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
basis
Basis
binance-futures, okx-perps, hyperliquid-perps1m 5m 15m 30m 1h 4h 1dPro
funding
Funding
binance-futures, okx-perps, hyperliquid-perps1m 5m 15m 30m 1h 4h 1dPro
open_interest
Open Interest
binance-futures, okx-perps, hyperliquid-perps1m 5m 15m 30m 1h 4h 1dPro
derivative_price
Derivative Price
binance-futures, okx-perps, hyperliquid-perps1m 5m 15m 30m 1h 4h 1dPro
ohlcv
Candlesticks
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
vtwap
VWAP / TWAP
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
l1_price
L1 Price
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPro
l1_imbalance
L1 Imbalance
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPrime
l1_liquidity
L1 Liquidity
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPrime
l2_imbalance
L2 Order Book Imbalance
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPrime
l2_liquidity
L2 Order Book Liquidity
binance-futures, okx-perps, hyperliquid-perps15s 30s 1m 5m 15m 30m 1h 4h 1dPrime

Messages

Every frame is a JSON text frame. Your frames carry an op and an optional id matching ^[A-Za-z0-9_\-.:]{1,64}$, which the answer echoes.

subscribe

A channel is a name string, or an object with dataset, exchange, interval and an optional symbols list. Leave out symbols for every symbol, where your plan allows it. Subscribing to a channel you already have adds the new symbols to it. Put all your channels in one frame, up to 200, rather than one frame each.

subscribe
{"op":"subscribe","id":"c1","channels":[{"dataset":"ohlcv","exchange":"binance-futures","interval":"1m","symbols":["perpetual-BTC-USDT:USDT","perpetual-ETH-USDT:USDT"]},"open_interest.okx-perps.1m"]}

unsubscribe

Drop a whole channel, or some of its symbols. Symbols can only be dropped from a channel subscribed with a symbol list: to narrow an all-symbols subscription, unsubscribe the channel and subscribe again with the symbols you want.

unsubscribe
{"op":"unsubscribe","id":"u1","channels":["ohlcv.binance-futures.1m"]}
{"op":"unsubscribe","id":"u2","channels":[{"dataset":"ohlcv","exchange":"binance-futures","interval":"1m","symbols":["perpetual-ETH-USDT:USDT"]}]}

ping

{"op":"ping"} is answered with {"op":"pong"}, echoing the ping's id if it has one. You do not need to ping: the server sends heartbeats on an idle connection.

What the server sends

Data. One row, exactly as it was produced. Its fields are the dataset’s columns, listed on its catalog page.

Data
{"channel":"ohlcv.binance-futures.1m","data":{…}}

Snapshot. Right after a subscribe, on plans with snapshots: the latest row per symbol, so you need not wait for the next bar. Best-effort.

Snapshot
{"channel":"ohlcv.binance-futures.1m","snapshot":true,"data":{…}}

subscribed. The answer to a subscribe: what was granted, and why anything was refused. A frame can be partly granted.

subscribed
{"op":"subscribed","id":"c1","channels":["ohlcv.binance-futures.1m"],"rejected":[{"channel":"open_interest.okx-perps.1m","code":"not_entitled","message":"Dataset open_interest is not included in your plan"}]}

unsubscribed. The answer to an unsubscribe. Also sent unprompted, without an id, when a plan change removes channels.

unsubscribed
{"op":"unsubscribed","id":"u1","channels":["ohlcv.binance-futures.1m"],"rejected":[]}

pong. The answer to a ping, echoing its id if it had one.

pong
{"op":"pong","id":"p1"}

heartbeat. Sent when a connection has received nothing for 25 seconds. time is in epoch milliseconds.

heartbeat
{"op":"heartbeat","time":1791210600000}

error. A message that could not be applied, with a code from the table under Errors. The connection stays open.

error
{"op":"error","code":"limit_exceeded","message":"At most 10 control frames per second"}

Rows are keyed by exchange, symbol, interval, timestamp type and time. A row you receive twice carries the same key, so overwrite rather than append.

Limits

Limits per plan. Connections count across your whole account.

PlanConnectionsChannels per connectionSymbols per channelSnapshots
Pro550Up to 100, listedYes
Prime, Prime Commercial, Prime + Raw10200AllYes
Institutional25Up to 1,000AllYes

On a plan with a symbol limit, every subscribe has to list its symbols: one without symbols is refused with limit_exceeded.

On every plan, one connection may:

  • send 10 control messages (subscribe, unsubscribe, ping) a second, in bursts of up to 20. Messages over the limit get an error frame with code limit_exceeded and are not applied: pause and send them again. A connection is closed only if it stays over the limit for 10 seconds straight.
  • list at most 5,000 symbols across all its channels, and 1,000 in one channel.
  • send frames of up to 64 KB, with at most 200 channels each.

Close sockets cleanly so their slot frees at once. A connection that disappears without a close keeps its slot for up to 2 minutes, and a reconnect in that time can be refused with 429 if you are at your plan's limit.

Errors and close codes

A subscribe or unsubscribe that is partly refused lists each refused channel under rejected with one of these codes. The same codes appear in error frames.

CodeMeaning
not_entitledThe dataset or interval is not in your plan.
not_liveYour plan has the dataset, but it is not streamed at that interval. The message names the intervals it is streamed at.
unknown_channelThe channel is malformed, or the exchange does not carry that dataset.
limit_exceededA plan or connection limit would be passed. The message says which.
invalid_messageThe frame could not be applied: not JSON, a missing field, or unsubscribing from something you are not subscribed to.

The server closes a connection with one of these codes:

Close codeMeaning
4001Your plan lapsed, the key was rotated, or your entitlement could not be confirmed for 15 minutes past its expiry. An error frame with code auth_expired comes first. Reconnecting with the same key will be refused until the plan or key is fixed.
1008The connection kept sending control messages over the limit for 10 seconds straight.
1011The server lost the connection’s state. Reconnect and subscribe again.
1006Never sent by the server: your client reports it when the connection dropped without a close frame, for example on a network change or a server restart. Reconnect.

Delivery and reconnecting

  • At most once. Rows published while you are disconnected are not replayed. After a reconnect, fill the gap from the REST API.
  • Subscriptions belong to the connection. A new connection starts with none: subscribe again after every reconnect.
  • Detecting a dead connection. The server checks every 30 seconds and sends a heartbeat to any connection that has received nothing for 25, so a healthy connection hears from it at least every 55 seconds. Treat 85 seconds of silence as a dead connection.
  • Reconnect with backoff. Start at about a second, double up to a minute, and add jitter. On 401 or 403, or after close code 4001, stop and fix the key or plan.
  • Plan changes. A connection's grant is checked again every 15 minutes. After a downgrade, channels the new plan does not cover are removed with an unsubscribed frame. When a plan lapses or its key is rotated, the connection closes with 4001. An upgrade reaches open connections at the next check; reconnect to use it sooner.

Known limitations

  • No replay and no history: the feed only carries rows from the moment you subscribe. History is in the REST API.
  • Snapshots are best-effort. The feed snapshots only rows it still holds in memory, which it can lose at any time, so do not rely on one arriving.
  • No server-side check for slow readers. A client that falls behind is not cut off; its rows queue up and arrive late. Read the socket promptly and do heavy work elsewhere.
  • No browser connections, since browsers cannot send the X-API-KEY header. Connect from a server and relay to your pages.
  • Self-serve plans carry no uptime commitment. Institutional plans come with an SLA.
  • In the first seconds after a channel's first subscriber arrives, its rows can be missed while the feed starts consuming it.

Questions or a limit that does not fit your use: support@aperiodic.io.

Aperiodic

Built from hundreds of terabytes of raw data and distilled into point-in-time metrics you can pull as parquet files. Plus the raw trades, quotes and derivatives data behind them.

Use Cases
  • Improve your backtest
  • Find new alpha
  • Execute smarter
Catalog
  • Full Catalog
  • Live markets dashboard
  • Global Liquidity
  • Unusual activity
  • Order Flow
  • L1 — Top of Book
  • L2 — Order Book
  • Market Data
  • Derivatives
  • Raw Data
  • RWA
  • Atlas Security Master
Developers
  • API Docs
  • Live WebSocket Feed
  • Notebooks
  • For AI Agents
  • Changelog
  • Roadmap
  • llms.txt
Learn
  • Replays
  • Microstructure Guide
  • Architecture
  • Research Blog
  • FAQ
Compare
  • All vendors
  • vs. Tardis
  • vs. CoinGlass
  • vs. Kaiko
  • vs. Amberdata
Company
  • Pricing
  • Contact
  • Book a call
  • Terms of Service
  • Privacy Policy
  • Licensing
  • SLA

© Copyright 2026 Aperiodic. All Rights Reserved.

Aperiodic Limited · Registered in Ireland · Company No. 815273 ·
136 Capel Street, Dublin D01 T2C9, Ireland

Shown: minute closes of 847 perpetuals, 20:30–22:30 UTC on 10 Oct 2025, IOTX in orange · built with ohlcv 1m