Skip to main content
Five gRPC streams matter to an ISV, and picking the wrong one is a multi-week mistake: one partner built settlement detection on 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.
Before this page: a minted access token (Authentication), the 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).
API reference: Drop copy · Balance ledger · Market data · Proto reference. Opens on the public documentation site in a new tab. Where it disagrees with this page, this page is authoritative for the partner surface.

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

Each of the four sections after this one is the detail behind one row of that table.

When to use drop copy

Use it for fills, commissions and trade capture. It is the only stream with a populated resume_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 0.01feeinto0.01 fee into 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 change

When to use instrument state change

Use it for lifecycle transitions and nothing else. It is the one subscription that does not need x-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 or RESOLUTION 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

Cluster configuration carries max_streams_per_firm: 20, which is the same number from the other side, and stream_message_rate_limit: 50,000 / 60 s.
No separate preprod values are published for any of these caps. Build against the numbers above in both environments.
The concurrent-stream limit is 20 per firm, not 10. A figure of 10 is in circulation and is wrong. Size your fleet against 20; sizing against 10 wastes half your budget.

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 whose resume_token is populated, so it is the only one where a disconnect costs you nothing.
The published drop-copy example omits 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 the resume_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.
One execution report, as JSON, with the fields you branch on:
int64 fields serialize as strings in JSON — "500" above is a number on the wire, not a string value.
Confirm the report field spellings against your own bundle before you branch on them. The proto bundle is an unversioned, anonymous zip with no changelog and no checksum, so neither side can tell which build you hold.

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 0.01feebecomes0.01 fee becomes 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

1

Persist it with the work it covers

Write the token in the same transaction as the report it covers. If you store the token first and crash, you have acknowledged a report you did not process.
2

Pass it on the next subscribe

Send the last persisted token as resume_token on CreateDropCopySubscriptionRequest. With no token you get a fresh subscription, not a replay.
3

Dedupe anyway

Delivery is at-least-once and redelivery is expected. Dedupe on 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}.
Position-change subscriptions are account-scoped, so the call metadata needs x-participant-id as well as authorization. The published streaming examples build metadata with authorization alone and 403.

Subscribe and handle deltas

Confirm the request message name and the delta field spellings against your own bundle. The bundle is unversioned and has no checksum, it advertises 14 services while defining 5, and CreatePositionSubscription has no proto definition anywhere — do not assume the two names are interchangeable.
Subscribe gives you snapshot then delta: the first messages describe your current positions, and changes follow. Delivery is at-least-once, so dedupe and expect redelivery.

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.
1

Take the symbol off the position change

You now know which instrument moved for which account.
2

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/*.
3

De-scale settlementPx with priceScale

settlementPriceScale is reserved and reads 0. Winning contracts settle at 1.00andlosingat1.00 and losing at 0.00 — see Settlement fields.
4

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.
We credit the participant’s clearing account at resolution. Do not also credit your user from your own ledger — see Who credits the user.

Preprod does not resolve markets

Markets do not resolve in preprod. Instruments reach INSTRUMENT_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 stream cannot be resumed, and it is not a settlement source.It declares a resume_token and never populates it. Anything that transitions while you are disconnected is lost, and there is no replay path — you recover by reading instrument state back, not by resuming.It returns the updated Instrument and does not include the settlement result fields. We told a partner the opposite and then corrected it. Settlement values come from the market data stream or GET /v1/orderbook/{symbol}; the trigger comes from Position change.
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

Confirm the service and request message names against your own bundle. The bundle is an unversioned zip with no changelog or checksum, and it advertises 14 services while defining 5, so the generated module path above may differ in your copy.
An empty symbols list subscribes to every instrument, which collides with the 1000-instrument cap that applies per market-data stream. Name your symbols explicitly. Sharding arithmetic is on Reconnect and recover gaps.

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.
Not yet published. Read the enum out of your own generated code and confirm the list with your integration lead before you branch on any state not named above. Do not infer the missing values from the retail surface.
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.
1

Re-subscribe immediately

Backoff and cadence are on Reconnect and recover gaps.
2

Re-read instrument state for your symbols

Use Reference Data for the instruments you track. 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.
3

Do not use this stream to fill settlement gaps

It does not carry settlement result fields. Go to Detect that a market resolved.

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.
This stream does not scale, and there is no substitute yet.It is per account, and every open stream counts against the 20 concurrent streams per firm cap that is shared across all gRPC subscriptions. With 20 in total — minus drop copy, position change and market data — you cannot run one per participant. A firm-level ledger stream is “coming soon” with no date.The consequence: there is no scalable push channel for cash movements or RESOLUTION entries for an ISV at scale. Plan the interim pattern below into your architecture rather than discovering the cap at 500 users.
The published balance-ledger example omits 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

1

Replay phase

On subscribe you receive existing ledger entries for the account. Treat this as a snapshot you may already have seen — delivery is at-least-once, so dedupe on the entry’s own identifier rather than on arrival order.
2

Live phase

New entries arrive as they are written. There is no published marker that tells you the replay has finished. Until that is confirmed, make your handler idempotent instead of stateful about the phase change.
3

Reconnect

Reconnecting restarts at the replay phase. That is your gap recovery on this stream — there is no populated resume_token here to close a gap precisely. See Reconnect and recover gaps.

Subscribe to the ledger

Confirm the service, request message and entry field names against your own bundle. The bundle is unversioned, has no checksum, and advertises 14 services while defining 5 — and the published 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 suppressedNETTING, 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.
Not yet published. Only the four suppressed names above and the two consumable types are confirmed. Confirm the full allowlist with your integration lead before you request any other type.
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.
1

Budget your 20 streams explicitly

Write the allocation down: 1 drop copy, 1 position change, N market data at 1000 instruments each, and whatever is left for balance ledger. The cap is 20 per firm across all gRPC subscriptions, so balance-ledger streams are the residual, not the baseline.
2

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.
3

Poll the ledger read for cash, within the REST budget

REST is 100 req/sec per firm on a one-minute average, and ledger CSV downloads are ~5/min per firm. Size the polling interval against your account count and that budget, and back off on a 429, which carries Retry-After.
4

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.
5

Reconcile on a schedule, not on every event

Stored execution history is archived during maintenance and pre-maintenance execution queries return empty, so your own persisted ledger is the durable copy. See Preprod.

What can go wrong

Next

Reconnect and recover gaps