Polyester publishes each Pair's order book as aggregated price levels: one quantity per price per
side. Read a snapshot with GET /v1/orderbook/{symbol} or OrderbookService.GetOrderBook,
then keep it current with the public order book update channel. Both are public and need no
authentication.
Snapshots
GET https://api.testnet.polyester.com/v1/orderbook/BTC-USDT?depth=5{
"symbol": "BTC-USDT",
"bookSeq": "835057",
"available": true,
"ts": 1793914668528,
"asks": [
["76090.29", "0.00131422"],
["76090.3", "0.82087261"],
["76105.52", "0.84519947"],
["76120.74", "0.76896305"],
["76135.97", "0.80528809"]
],
"bids": [
["76059.87", "0.84582963"],
["76044.66", "0.82029386"],
["76029.45", "0.79617429"],
["76014.25", "1.21666489"],
["75999.05", "1.06646314"]
]
}Each level is a [price, quantity] pair of decimal strings. bookSeq is a decimal string and ts is the snapshot time in Unix milliseconds.
POST https://api.testnet.polyester.com/orderbook.v1.OrderbookService/GetOrderBook
Content-Type: application/json
{"symbolId": 1, "depth": "DEPTH_5"}{
"symbolId": 1,
"bookSeq": "835057",
"bids": [
{ "priceTicks": "76059870000000", "qtyScaled": "84582963" },
{ "priceTicks": "76044660000000", "qtyScaled": "82029386" }
],
"asks": [
{ "priceTicks": "76090290000000", "qtyScaled": "131422" },
{ "priceTicks": "76090300000000", "qtyScaled": "82087261" }
],
"ts": "2026-11-05T21:37:48.528466493Z",
"available": true
}The same snapshot as the REST tab, shortened to two levels per side. priceTicks uses a fixed scale
of 9 and qtyScaled the Pair's baseQuantityScale; see Scaled integers.
Bids are best first, highest price first. Asks are best first, lowest price first. Levels are not
aggregated into wider price buckets; group them yourself for display, for example with the Pair's marketdata.orderbookPriceBuckets from GetSpotConfig.
Depth
depth is the number of levels per side. REST takes the number; ConnectRPC takes the Depth enum.
REST depth | ConnectRPC depth |
|---|---|
1 | DEPTH_1 |
5, 10, 20 | DEPTH_5, DEPTH_10, DEPTH_20 |
50 (default) | DEPTH_50 or DEPTH_UNSPECIFIED |
100, 200, 500 | DEPTH_100, DEPTH_200, DEPTH_500 |
1000 | DEPTH_1000 |
Any other REST value is rejected with HTTP 400 and code INVALID_DEPTH. An unknown symbol returns
HTTP 404 with UNKNOWN_SYMBOL; symbols are case-sensitive. On ConnectRPC, an unknown symbolId returns HTTP 404 with not_found, and an unknown Depth name HTTP 400 with invalid_argument.
When the book is unavailable
available is false while a Pair has no book yet, for example right after it opens, or after the
book was invalidated. Its levels are then unknown, not empty. This is WLD-USDT, a disabled Pair
without a listing time:
{ "symbol": "WLD-USDT", "bookSeq": "0", "available": false, "ts": 0, "asks": [], "bids": [] }On REST, an unavailable snapshot still carries every field: ts is 0, bookSeq is "0", and
both sides are empty arrays. Read available before you use the levels or ts, and before you
treat an empty side as no liquidity, then request the snapshot again later. On ConnectRPC an
unavailable snapshot carries only symbolId, because ConnectRPC JSON omits default values, and its ts is unset.
With available: true, an empty side really is empty. BTC-USDC, a disabled Pair that is listed,
answered with available: true and no levels on either side.
Book sequence
bookSeq is the Pair's book sequence: it increases as the book changes, and it can also advance
when no level changed. Snapshots and updates carry
it so you can tell whether an update follows the state you hold.
- An update covers the range from
bookSeqStarttobookSeqEnd. Each update on a channel starts where the previous one ended: itsbookSeqStartequals the previous update'sbookSeqEnd. - The sequence advances by more than one between updates. Never expect
bookSeqEndto bebookSeqStart + 1. - After a reset, the sequence can restart at a lower value or stay unchanged. A reset always replaces your book, whatever its sequence.
Realtime updates
Subscribe to one depth tier of one Pair. The JSON channel carries decimal strings; the Protobuf
channel carries orderbook.v1.OrderBookDelta with scaled integers.
public:spot:orderbook:deltas:depth:<depth>:<symbolId>:json
public:spot:orderbook:deltas:depth:<depth>:<symbolId>:proto<depth> is one of the published tiers 1, 20, 50, 200, or 500. A subscription to another
depth is accepted but receives nothing. Smaller tiers wait less before publishing, though a larger
tier can send more updates because more of its levels change. Both channel names
need the encoding suffix. See Realtime channels for how to connect.
These are two consecutive updates sent on public:spot:orderbook:deltas:depth:20:1:json:
{"symbolId":1,"bookSeqStart":811343,"bookSeqEnd":811361,"reset":false,"ts":1793914066430,"bids":[["75749.82","0"],["75585.15","1.78487547"]]}
{"symbolId":1,"bookSeqStart":811361,"bookSeqEnd":811368,"reset":false,"ts":1793914066597,"bids":[["75786.57","0"],["75562.48","1.85352414"]]}| Field | Meaning |
|---|---|
bookSeqStart, bookSeqEnd | Sequence range of the update. JSON numbers here, unlike the snapshot's string bookSeq. bookSeqStart can be 0 on a reset |
reset | true: clear your book for this tier, then install the levels in this update |
bids, asks | [price, quantity] changes. The quantity replaces the level's quantity; "0" removes the level. A side without changes is omitted |
ts | Best-effort time of the update, in Unix milliseconds. Protobuf carries a google.protobuf.Timestamp |
Levels within an update are not sorted. Each update describes the top <depth> levels per side, so
a level that leaves the top <depth> arrives with quantity 0 even if it still rests deeper in the
book, and a level that moves into the top arrives with its quantity. In the first update above, 75749.82 left the top 20 bids and 75585.15 entered.
A reset carries the whole tier. This one was sent on public:spot:orderbook:deltas:depth:1:1:json:
{
"symbolId": 1,
"bookSeqStart": 0,
"bookSeqEnd": 817439,
"reset": true,
"ts": 1793914219342,
"bids": [["75983.86", "0.01314094"]],
"asks": [["76014.26", "0.85159909"]]
}Maintain a local book
This is the same pattern as other major venues: buffer updates, fetch a snapshot, discard what the snapshot already contains, then apply updates in sequence.
Subscribe and buffer
Subscribe to the depth tier you want, for example depth:50, and buffer the updates you receive.
Subscribe with recovery enabled; see Realtime channels.
Fetch a snapshot at the same depth
Request GetOrderBook with the same depth as the tier. If available is false, wait and request
it again. Install its levels and remember its bookSeq as the last applied sequence.
Drop what the snapshot contains
Discard buffered updates whose bookSeqEnd is at or below the snapshot's bookSeq.
Apply updates in sequence
Apply the next update only if its bookSeqStart equals your last applied sequence. Replace or
remove each level it lists, then store its bookSeqEnd as the last applied sequence. Ignore an
update whose bookSeqEnd is at or below it; you already hold that state.
Apply resets at any time
When an update has reset: true, clear both sides, install its levels, and store its bookSeqEnd,
whatever your last applied sequence was. Resets are sent when a tier starts publishing, after the
Pair's book is rebuilt, and after a publication could not be delivered. Testnet also sends one on
every tier about every 30 seconds, so expect them in normal operation.
Resync on a gap
If a non-reset update starts at any other sequence, your book has missed changes. Discard it, keep
buffering, and repeat from the snapshot step. A snapshot can also fall inside an update's range,
most often on depth:1, so a resync can take more than one attempt. Do the same when the subscription reports that missed
publications could not be recovered.
// Per tier: price string -> quantity string, for each side.
function apply(book, update) {
if (update.reset) {
book.bids.clear();
book.asks.clear();
} else if (update.bookSeqEnd <= book.seq) {
return "ignore";
} else if (update.bookSeqStart !== book.seq) {
return "resync";
}
for (const [price, qty] of update.bids ?? []) setLevel(book.bids, price, qty);
for (const [price, qty] of update.asks ?? []) setLevel(book.asks, price, qty);
book.seq = update.bookSeqEnd;
return "applied";
}
function setLevel(side, price, qty) {
if (Number(qty) === 0) side.delete(price);
else side.set(price, qty);
}Compare prices and quantities as exact decimals in production code; the Number(qty) === 0 check
above only tests for zero. Sort a side when you read it, not on every update.
Choose a tier
Use the smallest tier that covers what you need: 1 for the best bid and ask, 20 or 50 for a
trading ladder, 200 or 500 for depth charts. Snapshots support every depth up to 1000, but
realtime updates stop at 500. To show fewer levels than a tier carries, keep the full tier locally
and display the top of it.
Rate limits
See Rate limits. Prefer the realtime channel over polling snapshots; request a snapshot only to start or resync a book. The full request and response schema is in the API reference.