CreateInstrumentStateChangeSubscription, which does not carry settlement values, and had to rebuild on CreatePositionChangeSubscription. Start at the decision table below, then read the section for the stream it sends you to.
participantId and the account’s provisionedAccount from the kyc.approved webhook
(Firms, participants and accounts), and generated stubs
from your pinned proto bundle (Protos and SDKs).Choose a stream
The two questions you arrived with
“Which stream tells me a market I hold resolved?”CreatePositionChangeSubscription. It is resumable and position-driven, so you only hear about markets you actually hold, and a disconnect does not lose the event. → Position change
“Which stream carries settlement values?” The market data stream, or the GET /v1/orderbook/{symbol} read. Not CreateInstrumentStateChangeSubscription. That subscription returns the updated Instrument and does not include the settlement result fields. We told a partner the opposite and then corrected it — if you were given the earlier answer, it was wrong, and code written against it will never see a settlement value. → Detect that a market resolved
The five streams
When to use drop copy
Use it for fills, commissions and trade capture. It is the only stream with a populatedresume_token, so it is the only one where you can close the gap over a disconnect exactly. commission_notional_collected arrives here, and decoding it wrong is how a partner turned a 1.00 — de-scale it per Money on the wire. → Drop copy
When to use position change
Use it to learn that something happened on a market a user of yours holds. Because it is driven by positions rather than by symbols, you do not subscribe to instruments and you do not hit the 1000-instrument market-data cap. It tells you that a position changed; it is not the source of the settlement price. → Position changeWhen to use instrument state change
Use it for lifecycle transitions and nothing else. It is the one subscription that does not needx-participant-id — it needs read:instruments only. It is not resumable, so anything that transitions while you are disconnected is lost and must be recovered with a read. → Instrument state changes
When to use balance ledger
Use it for cash movement on a single account. It is per-account and every open one counts against the 20-streams-per-firm cap, so you cannot run one per participant. The firm-level ledger stream is “coming soon” with no date, which leaves no scalable push channel for cash orRESOLUTION entries for an ISV at scale. → Balance ledger
When to use market data
Use it for the book, BBO, stats and settlement values. It has no resume field at all: on reconnect you take a fresh snapshot and re-derive state. It is capped at 1000 instruments per stream.Caps, with their scopes
max_streams_per_firm: 20, which is the same number from the other side, and stream_message_rate_limit: 50,000 / 60 s.
More than 20,000 instruments does not fit
1000 instruments per stream × 20 streams per firm = 20,000 instruments, and that is before you spend any of the 20 on drop copy, position change or balance ledger. One partner ran 7,372 subscriptions before being told the cap and had to re-architect in production. Do the arithmetic for your instrument universe before you write the subscriber — Reconnect and recover gaps works it through and says plainly where it stops fitting.Drop copy
Drop copy is your source of record for fills and commissions, and the only stream whoseresume_token is populated, so it is the only one where a disconnect costs you nothing.
x-participant-id and will 403. Drop copy is
account-scoped, so the call metadata needs both authorization and x-participant-id. If you
copied the current example from /trader-guide/streaming-apis, add the header — the snippets below
include it. The same defect is in the published balance-ledger example.Subscribe and process reports
Persist theresume_token as you go and pass it back on the next subscribe. It is roughly 576 bytes when populated — store it as opaque bytes, do not parse it, and do not assume a length.
int64 fields serialize as strings in JSON — "500" above is a number on the wire, not a string value.
Order states you will see
OrderState is NEW(1) accepted and resting, PARTIALLY_FILLED(2), FILLED(3), CANCELED(4), REJECTED(7), EXPIRED(9). There is no PENDING order state.
On the ISV surface, EXPIRED is the most common outcome you will see: CreateVendorOrder is fill-or-kill only, and an FOK that never crossed comes back EXPIRED with no reason, by design. One partner reported that ~99% of their failed orders showed only EXPIRED. That is expected, not a fault. See Order outcomes.
Global Rate Limit Exceeded arrives here as an execution-report rejection, not as an HTTP error or a gRPC status. If you only watch gRPC statuses you will not see it.
Decoding the fee off a report
commission_notional_collected is scaled by priceScale × fractionalQtyScale, read per instrument. Read it at priceScale alone and a 1.00 — that mistake exceeded a real customer’s entire prefund and suppressed their refund.
The formula, the per-instrument scale lookup and the worked example are on one page: Money on the wire. Do not re-derive them here.
There is no settlement_fee. The only fees are the trading fees in the published Fee Schedule — see Settlement fields.
Using the resume token
Persist it with the work it covers
Pass it on the next subscribe
resume_token on CreateDropCopySubscriptionRequest. With no
token you get a fresh subscription, not a replay.Dedupe anyway
clord_id plus the execution’s
own identifier; do not assume a resumed stream starts exactly one message past your token.Position change
CreatePositionChangeSubscription is the stream that tells you something happened on a market one of your users holds, and it is the one to build resolution detection on.
Why this stream and not instrument state change
It is resumable.resume_token is populated, so a disconnect does not silently drop the one event you care about. CreateInstrumentStateChangeSubscription declares a resume_token and never populates it — anything that transitions while you are disconnected is gone.
It is position-driven. You do not enumerate symbols, so you never hit the 1000-instruments-per-stream market-data cap, and you only hear about markets you actually hold. A partner who built settlement detection on instrument state change was eventually redirected here, and that redirect de-escalated the ticket.
It does not carry the settlement price. This stream is the trigger; the value comes from the market data stream or GET /v1/orderbook/{symbol}.
x-participant-id as well as authorization. The published streaming examples build metadata
with authorization alone and 403.Subscribe and handle deltas
CreatePositionSubscription has no proto definition anywhere — do not assume the two names are
interchangeable. Composing it with the settlement read
A position change on a market that has passed its expiration is a trigger, not a settlement price. Get the price from the market data stream or the orderbook read.Take the symbol off the position change
Read the settlement fields for that symbol
GET /v1/orderbook/{symbol} or the market data stream carries settlement_px,
settlement_preliminary, settlement_price_calculation_method,
settlement_price_calculation_text and settlement_set_time. Neither x-participant-id nor an
account is needed on /v1/orderbook/*.De-scale settlementPx with priceScale
settlementPriceScale is reserved and reads 0. Winning contracts settle at 0.00 — see Settlement fields.Gate before you act
settlement_preliminary: false does not mean “resolved to an outcome”. A non-binary
settlementPx on a non-resolved instrument is a mark, and there is no positive resolved
indicator. Read Detect that a market resolved before
you write any payout logic.Preprod does not resolve markets
Markets do not resolve in preprod. Instruments reachINSTRUMENT_STATE_EXPIRED in large batches — 363, 455, 544 and 549 in a single session — with none resolving. One partner asked six times over 40 days to test a settlement flow and never could. Build and unit-test your handler against captured messages; do not plan on an end-to-end preprod resolution.
Instrument state changes
CreateInstrumentStateChangeSubscription carries lifecycle transitions on instruments and nothing else.
This section needs less than the rest of the page: a minted access token with the read:instruments scope, and your generated stubs. No participant ID and no provisioned account.
This is the one subscription with no participant header
CreateInstrumentStateChangeSubscription needs read:instruments only. Do not send x-participant-id expecting it to matter — the subscription is not account-scoped, and every other stream on this list is. That asymmetry is why partners copy a working instrument-state subscriber, point it at drop copy, and get a 403.
Subscribe to instrument state changes
InstrumentState has 9 values
InstrumentState has 9 values. Code a default branch: /concepts/market-data publishes a lossy 5-state subset, and a partner coding to that subset will miss states it never mentions.
EXPIRED on an instrument is not resolution. Preprod instruments reach INSTRUMENT_STATE_EXPIRED in batches of 363, 455, 544 and 549 in a single session with none resolving, so a state transition tells you the instrument stopped trading and nothing about the outcome.
Recovering what you missed
There is no replay. After any disconnect, re-read state for the symbols you care about rather than assuming the stream caught up.Re-subscribe immediately
Re-read instrument state for your symbols
ListInstruments is 6/min per firm and
ListSymbols is 6/min per firm, so a full re-read is not something you can do per reconnect
at scale — track a working set.Do not use this stream to fill settlement gaps
Balance ledger
CreateBalanceLedgerSubscription streams ledger entries for a single account in two phases: a replay of existing entries, then live entries as they are written.
x-participant-id and will 403. This surface is
account-scoped: the metadata needs authorization and x-participant-id. The same defect is
in the published drop-copy example.Replay, then live
Replay phase
Live phase
Reconnect
resume_token here to close a gap precisely. See
Reconnect and recover gaps.Subscribe to the ledger
CreateCashMovement proto shipped with a wrong field number once, so a name that looks right is
not evidence.The entry-type allowlist
LedgerEntryType is an allowlist. Twelve internal types are suppressed — NETTING, GIVE_UP, INTEREST and SETTLEMENT_FEE among them — and requesting one returns Aborted / 409. Handle Aborted as a request defect on your side, not as a transport failure to retry: retrying an unentitled type gets the same answer forever.
The types you will actually consume as an ISV are ORDER_EXECUTION and RESOLUTION.
An ORDER_EXECUTION entry folds commission into a single net entry. Whether the RESOLUTION entry is gross with COMMISSION deducted separately, or net, is unanswered — do not assume settlement behaves like an execution. See Settlement fields.
Interim pattern until the firm-level stream exists
Reserve streams for the accounts where latency matters, and poll for the rest.Budget your 20 streams explicitly
Use position change as your resolution trigger, not the ledger
CreatePositionChangeSubscription is resumable and position-driven, so one stream tells you
something happened on any market any of your users hold. That is the scalable signal; the ledger
is the after-the-fact record. See Position change.Poll the ledger read for cash, within the REST budget
429, which carries Retry-After.Derive spendable cash from the newest entry
GetAccountBalance returns InvalidArgument: invalid account for a participant clearing
account while succeeding for a firm account, so partners take the newest ledger entry’s
afterBalance instead. Use GetFundingAccountBalance for the pool — that one is the pool’s
source of truth. See Funding.Reconcile on a schedule, not on every event