What you need
- A canonical ID such as
syn_0123456789abcdeffrom the Synthetic Books definition workflow. - Kairos API credentials. The same credential triple creates definitions and authenticates the stream; no second Synthetic Books secret is issued. Reading an existing ID also works on the bounded anonymous tier in an environment where it is enabled.
- The public WebSocket control schema and synthetic book schema.
1. Craft the formula
A definition is a weighted sum:POST /v1/synthetics. This request creates a
50-level decomposed book for A - B:
weight as a decimal string so values such as
"0.333333333" are represented exactly.
Choose an output mode based on what your client needs:
For example:
expires_at_ms with
POST /v1/synthetics/subscriptions/{subscription_id}/refresh. When the caller
no longer needs the definition, release its handle with
DELETE /v1/synthetics/subscriptions/{subscription_id}. Send the same three
API-key headers on both calls. A lease lasts 10 minutes; refreshing every five
minutes leaves room for a retry. The account may hold at most 25 active leases,
and definition creation is limited to 30 requests per minute by default.
Only the account that created a lease can refresh, release, or inspect it. An
expired lease cannot be revived: create the definition again and use the newly
returned subscription UUID.
2. Compile the protobuf schemas
Download the control schemas and the synthetic schema, preserving their directory paths. For Python:generated to PYTHONPATH, or package the generated modules with your
application.
3. Connect and authenticate
Programmatic clients should send all three API-key headers during the WebSocket upgrade:Sec-WebSocket-Protocol. For details and
the anonymous public protocol, see Market Data Stream authentication.
For example, a Python client using an enabled anonymous tier can open the same
endpoint with:
4. Subscribe to the synthetic ID
Send a binarySubscribeRequest using the ordinary control-message envelope:
syn_ followed by 16 lowercase hexadecimal characters. The
synthetic topic must be named explicitly; an empty topic list does not select
it. An accepted request produces SubscribedResponse (0x14). A cached
snapshot may arrive immediately, so dispatch every frame by tag instead of
waiting for the acknowledgment before reading data.
Choose how much of the book you need
One synthetic ID can be consumed at three weights. Name one or more of these topics intopics; a request may list several, and a later request for the
same ID adds to what the connection already holds.
synthetic.snapshot and synthetic.quote require provider: "synthetic".
A connection that holds both synthetic and synthetic.snapshot for one ID
receives each snapshot once.
Keep the snapshot with the highest ownership_epoch, then the highest
synthetic_sequence: the one sent on subscribe and a live one can arrive in
either order. Between snapshots a synthetic.snapshot subscriber holds the last
full state, which can be behind the live book by the deltas published since. A
snapshot is published on the first materialization, on every status change, and
then periodically.
5. Decode snapshots and deltas
Synthetic data frames have a two-byte header:0x51 is reserved for synthetic status notices and is not sent on this stream.
Control frames such as SubscribedResponse still use their one-byte header.
Ignore unknown tags and unsupported versions so future additions do not break
the connection.
This complete Python client subscribes with API-key credentials and maintains a
price-keyed local book:
6. Apply deltas safely
A snapshot replaces the entire local book. A delta applies only when:- Its
ownership_epochequals the snapshot epoch you currently hold. - Its
previous_sequenceequals your currentsynthetic_sequence. - Its
synthetic_sequenceis greater than the current sequence.
synthetic_sequence is less
than or equal to the sequence already applied. A replay can briefly overlap
with live delivery, so receiving the same delta twice is not a sequence gap.
Recover only when a newer delta does not connect to the current sequence, or
when a higher ownership epoch arrives without its snapshot.
Each SyntheticLevelChange is addressed by side and price_nano. A zero
qty_nano removes that price; a positive quantity replaces the complete level.
In decomposed mode, replace the recipes with the recipes on the change as well.
InputVersionChange.leg_index addresses the corresponding leg in the canonical
definition echoed by the snapshot. Apply those sparse changes if your client
displays or validates source lineage.
Gotcha: a failed check has no tolerance — resync fully. If any sequence
or epoch check fails, discard the local book and send the same
SubscribeRequest again. That requests a cached snapshot and its contiguous
deltas without consuming another subscription slot. If no complete replay is
available, wait for the next snapshot. Never apply a delta to an old
baseline.previous_sequence does not chain onto the frame the gateway holds is
dropped rather than forwarded, as is a delta from a lower ownership_epoch
and any duplicate redelivery. So a gap upstream shows up as silence, not as a
broken chain, and it ends at the next snapshot — the producer snapshots every
64 deltas, and the gateway’s replay buffer holds at most 256. While the chain
is broken, a re-subscribe replays nothing at all until that snapshot
re-anchors it; a snapshot plus a chain with a hole would leave you
confidently wrong.
The gateway drops frames that fail validation before they ever reach you:
a mismatched synthetic_id, a zero synthetic_sequence, a
price_unit_exponent / qty_unit_exponent other than 9, an empty
publisher_instance_id, a non-monotonic or crossed ladder, a non-positive
quantity on a snapshot level, a negative qty_nano on a change, or a change
whose side is neither BOOK_SIDE_BID nor BOOK_SIDE_ASK.
7. Decide whether the quote is usable
Only treat levels as executable whenstatus is
MATERIALIZATION_STATUS_EXECUTABLE. Empty ladders with NO_LIQUIDITY, and all
non-live statuses, are not zero-priced opportunities.
For execution-aware clients:
- Reject or revalidate recipes after
valid_until_ms. - Check
max_input_age_msandcross_venue_skew_msagainst your own policy. - Read the source action on each recipe leg; do not infer it from the sign of the formula at execution time.
- Include fees, balances, allowances, and partial-fill risk before submitting source orders.
8. Read the top of the book only
Subscribe withtopics: ["synthetic.quote"] to receive one small frame
instead of the ladder:
Each level carries
price_nano, qty_nano, and one recipes entry per venue
resting at that level, with the venue name, its token, the action, and the
venue’s own price_nano and qty_nano. On a union_l2 book the level price
already includes that venue’s taker fee, and the recipe price is the raw price
the venue shows. Recipe weight_nano, source_seq, and source_epoch are
not set on a quote.
A quote is sent when you subscribe, when the best bid or best ask changes in
price, quantity, or venue, when status changes, and with every snapshot of
the book. Changes deeper in the book send nothing.
Apply these rules:
- Keep the quote with the highest
ownership_epoch, then the highestsynthetic_sequence, and drop anything older. The quote sent on subscribe and a live quote can arrive in either order. - A quote with
MATERIALIZATION_STATUS_UNSPECIFIEDand no sides is a withdrawal: the server lost track of the book, because of a sequence gap or a rejected frame. It carries the sequence of the quote it withdraws. Clear the quote you hold. Nothing is sent on subscribe during that time. - Only treat the prices as executable when
statusisMATERIALIZATION_STATUS_EXECUTABLE. - On a
union_l2book the best bid can be above the best ask. The two sides rest on different venues, so the cross is a real cross-venue quote. - The next snapshot produces a fresh quote.
9. Unsubscribe and reconnect
To stop the stream, send:contract_id and provider: "synthetic". The server responds
with UnsubscribedResponse (0x15). This drops every topic the connection
holds for that ID.
To drop some synthetic topics and keep the rest, add the topic names as
repeated string field number 4 of the same request:
Errors and close codes
Subscribe rejections arrive asErrorResponse (0x17) with
action: "subscribe". The ones you are most likely to hit here are
synthetic requires a canonical synthetic id (syn_ followed by 16 hex digits),
synthetic.snapshot and synthetic.quote require provider synthetic,
Subscription limit exceeded: max N subscriptions per connection, and
Subscription limit exceeded: too many active subscriptions for this account.
The connection stays open — only the offending request is dropped.
The close codes you will see on this stream:
The full catalogue of
ErrorResponse messages, upgrade-time rejections, and
every close code is on
Market Data Stream → Errors & Disconnection.
Stream limits
Formula limits are listed in the Synthetic Books guide. The WebSocket limits apply independently:
One synthetic ID counts as one subscription however many of the three topics
the connection holds for it. Re-subscribing to the same provider and ID, or
adding another topic to it, does not consume another subscription slot.
For all connection limits, error frames, close codes, and reconnect behavior,
see Market Data Stream.

