Streaming
Stream live market and account data with the SDK's WsClient, on one connection.
WsClient is the SDK's third class, alongside Info and Exchange. It carries nine push feeds over a single WebSocket connection and delivers frames on a background thread, so nothing here asks you to write async.
Reach for it when polling will not do. /info reads default to one per second per client IP (configurable per integration), which cannot drive a quoting loop; the socket pushes instead of costing a read.
1. Connect
from native_core import Info
info = Info("https://api.native.org")
ws = info.ws(on_reconnect=lambda: print("resubscribed")) # reuses the loaded market table
ws.connect()Info.ws() and Exchange.ws() hand the client the market table they already loaded, so "ETH/USDT" resolves without another request. WsClient(base_url) works standalone; a client that only ever subscribes by market id makes no HTTP request at all.
For a node you run yourself, pass the WebSocket URL directly (WsClient("ws://localhost:8081")) — the derivation from an HTTP base URL holds only for the public endpoints.
2. Subscribe
Nine feeds: three by market, one global, five by address.
subscribe_trades(market)
subscribe_user_fills(address)
subscribe_l2_book(market)
subscribe_order_updates(address)
subscribe_bbo(market)
subscribe_open_orders(address)
subscribe_spot_state(address)
subscribe_spot_credit_state(address)
Plus one global feed with no topic argument: subscribe_all_mids().
Each returns a Subscription you can pass to unsubscribe(...). Both calls block until the server answers and raise SubscriptionError if it refuses — a silent stream is almost always a refused subscription, so the SDK makes that loud rather than letting it hide.
Ten subscriptions per connection, and one connection per client IP. Multiplex everything onto the one socket; see rate limits for the full table.
3. Consume
Two ways, and you can mix them on one client.
Callbacks run on the reading thread, so they must return quickly. Use them for cheap work like tracking top of book.
stream() yields frames from every subscription created without a callback. It buffers for you, so it is the right choice when handling a message takes real work.
Each stream() item is the full {"channel", "data"} envelope, because the channel is what tells the feeds apart. The buffer holds queue_maxsize frames (10,000 by default); a consumer slower than the feed overflows it and the oldest frames are dropped rather than stalling the reader. ws.dropped_messages counts them — check it, do not assume it is zero.
React on bbo, not on l2Book: the book is a throttled snapshot at 500 ms, while bbo pushes on every change to the top of book.
4. What survives a reconnect, and what does not
reconnect is on by default: the client reopens the socket and restores every subscription, then fires on_reconnect. That is where to re-read anything that gapped.
l2Book, allMids, openOrders, spotState, spotCreditState
Complete state every frame, so the next frame repairs any gap. Nothing to backfill
bbo
Complete state, but only pushed when the top of book changes. On a quiet market a resubscribe yields nothing at all, so read Info.l2_book once if you need top of book immediately
userFills
The first frame after subscribing replays your most recent 100 fills, so an ordinary resubscribe backfills itself
trades, orderUpdates
No replay. A gap is a gap
Frames can be lost without a disconnect. The three event feeds (trades, userFills, orderUpdates) must stay ordered, so a slow consumer is disconnected from those. The six snapshot feeds are conflated instead — you silently skip intermediate frames and stay connected, which costs resolution rather than correctness.
Separately, when the server's broadcast falls behind it drops that gap's event frames outright and re-pushes only the snapshot feeds. There is no disconnect and no on_reconnect, and userFills does not replay the way it does on a resubscribe. Anything that needs complete fills must poll Info.recent_fills and reconcile by tid.
Deduplicate fills by (user, tid), not by tid alone: both sides of a trade receive the same tid. And serialize tid as a string before handing it to a browser — the value exceeds JavaScript's safe integer range, which collapses two fills into one silently.
5. Trade on the same socket
Optional, and Exchange.order over HTTP is still the recommended write path. When you want orders on the connection you already hold:
build_order runs every local check; sign_action signs without sending and consumes a nonce, so the body is single-use. What comes back is an ordinary /trade response, read exactly as over HTTP: accepted is a verdict on the transaction, so check is_order_failed or take next_action. See Accepted is not placed.
A write over the socket reports less than the same write over HTTP. When the API answers with anything other than 2xx it discards the trade response, so submission_status, tx_hash and the response envelope are simply not there. post_action raises instead: ClientError for a refusal that cannot have executed (bad signature or nonce, or a 429), SubmissionUncertain for anything that might still land. Both uncertain cases carry the order handles — reconcile with reconcile_by_cloid, never resubmit. A write here is never retried automatically, not even on a rate limit.
ws.post_info(payload) runs an /info read over the same connection. It shares the one per-IP read budget with HTTP, so it is convenience, not extra quota.
Field names differ from HTTP
The socket speaks the protocol's own vocabulary and the SDK passes it through rather than inventing a third one. One pair is a trap:
available / locked (Info.user_balances)
total / hold (spotState), where total is available plus locked
price / quantity / order_count
px / sz / n
Reading total where you used to read available overstates your free balance whenever an order is resting. The full mapping is in the WebSocket reference.
Runnable example
examples/ws_feeds.py ships in the source distribution and needs no key:
Next
API ReferenceWebSocketLast updated