Skip to main content
Settlement is the one place in this integration where a mistake pays a user twice, so start with who is allowed to originate a credit.
Before this page: the two-firm identity model, Firms, participants and accounts; how cash reaches a participant account, Move cash; the trigger stream, Position change; and the scales, Money on the wire.
API reference: Report. Opens on the public documentation site in a new tab. Where it disagrees with this page, this page is authoritative for the partner surface.

Who credits the user

If you are an IB, this differs. An ISV is told not to pay its user from its own ledger. You may not accept or hold customer funds at all, so there is no ledger you could legitimately pay from — see Funds handling. You do own the customer-facing explanation of a settlement, including the cases where we cannot tell you why a market settled as it did (see Support and escalation for IBs), and cancelled events settle inconsistently, which you will be asked about. The gross-versus-net RESOLUTION question is also open for Customer Statements and reconciliation — see Reporting pack.
We credit the participant’s clearing account at resolution. You must not pay your user independently from your own ledger: if both sides credit, every settled position pays out twice. Two partners shipped settlement code switched off in production rather than risk exactly this.
This sentence needs explicit sign-off before publication.The division of responsibility above is inferable from the funding flow — settlement credits land in the participant account and stay there — but it has never been stated to partners as a responsibility. Confirm it with your integration lead in writing before you build or disable a payout path on it.

What lands where

At resolution, winning contracts settle at 1.00andlosingcontractsat1.00 and losing contracts at 0.00. The credit goes to the participant’s clearing account — the same account the position was held in. Three things stay in that account and become buying power there:
  • settlement credits,
  • realized profit,
  • collateral released when a position closes or settles.
Nothing needs sweeping after a fill or a settlement. There is no post-settlement transfer you must run to make the money usable, and OdfSweep — which used to exist for this shape of problem — was removed on 2026-08-06 along with CreateFundedOrder and PreviewFundedOrder. If your design has a sweep step in it, delete the step.

What you must not do

Do not credit your user from your own balance when a market resolves. The user’s money is already in their participant account. A second credit on your side is a real double payout, not a display bug, and you will discover it in reconciliation after the user has withdrawn. Two partners shipped their settlement crediting code switched off in production because this was never written down. Switching it off was the right call with the information available. The safe design is one ledger of record per dollar:

Mirroring settlement into your own wallet

If your product needs the money to appear in a wallet you operate, move it deliberately with CashMovementService rather than crediting it twice.
1

Detect the resolution

CreatePositionChangeSubscription tells you something happened on a market your user holds; the market data stream or GET /v1/orderbook/{symbol} carries the values. Gate it properly — settlement_preliminary: false does not mean resolved. See Detect that a market resolved.
2

Read the account, do not compute it

Take the balance from the newest balance-ledger entry’s afterBalance. GetAccountBalance returns InvalidArgument: invalid account for a participant clearing account, so a ledger read is the working path — see Balance ledger.
3

Move it with a WITHDRAWAL transfer

CashMovementService reasons are DEPOSIT, WITHDRAWAL and VENDOR_FEES, and direction is fixed by the reason. You name only the participant account; we resolve your funding account from your configured relationship. Transfers to your firm as a destination you choose, between participants, or to an external destination are structurally impossible.
4

Send no participant header

CashMovementService calls are firm-scoped. Do not send x-participant-id — that is the opposite of the account-scoped reads.
5

Round to 2 decimal places and keep the residue

Transfers accept at most 2 decimal places while balances carry more — 172.395 and 1377.57275 have both been observed. There is no sanctioned handling for the sub-cent residue, so your balance sheet cannot reach zero on those accounts. Track the residue rather than forcing it.
6

Never reuse a terminally-rejected idempotency key

A terminally-rejected idempotency_key replays its stored rejection. After a terminal reject, use a new key; an identical retry will not make a fresh attempt.
Transfer rate limit: do not build against a number yet.Three different values are live in our own material, and they disagree on both the number and the scope — one of them puts the ceiling shared across all partners rather than per firm. We are not publishing any of them here, because a settlement sweep sized against the wrong one fails in production: this already blocked a partner’s entire order flow as their stated number-one issue. Get the value and its scope from your integration lead before you size a sweep, and build backpressure regardless. Rate limits tracks this.

Fees do not come out of settlement

There is no settlement_fee. It was cited to a partner in error and retracted. The only fees are the trading fees in the published Fee Schedule, and the platform never knows your own fee basis — you track your accruals yourself and reconcile against the daily Vendor Fees report. That report has no delivery mechanism today (“Delivery method TBD”), so agree the channel with your integration lead. See Settlement fields.

Detect that a market resolved

With the crediting question settled, the next problem is knowing that a market resolved at all. Resolution detection is two calls, not one: a stream tells you something happened, and a read tells you what the value is.

The pipeline

1

CreatePositionChangeSubscription tells you a market you hold moved

It is resumable and position-driven, so you hear about markets your users actually hold and a disconnect does not drop the event.
2

The market data stream or GET /v1/orderbook/{symbol} carries the settlement values

settlement_px, settlement_preliminary, settlement_price_calculation_method, settlement_price_calculation_text and settlement_set_time live here. /v1/orderbook/* needs no x-participant-id.
3

You gate, then you act

The gate is the hard part. Read the warning below before you write it.
CreateInstrumentStateChangeSubscription does not carry the settlement result fields. It returns the updated Instrument and nothing more, so it is not a settlement source and not a gap-free replay source for settlement values. We told a partner the opposite and then corrected it. See Instrument state changes.

settlementPreliminary: false is not a resolved flag

settlement_preliminary: false does not mean “resolved to an outcome.” A non-binary settlementPx on a non-resolved instrument is a mark, and marks carry settlement_preliminary: false too.There is no positive resolved indicator anywhere on the API. Any partner gating a payout on settlementPreliminary alone will pay out on a mark. One partner called this the most important open item in their entire integration.
Two more facts that break the obvious gates:
  • Do not gate on settlement_price_calculation_text. It is not guaranteed present on a settled instrument. A partner who gated on it had a market stuck suspended.
  • settlementSetTime can precede expirationDate by ~13 hours on a mark, so a populated settlementSetTime is not evidence of resolution on its own.

The safest gate available today

This is a workaround, not a supported resolved flag. It is built from the facts we can stand behind: winning contracts settle at 1.00andlosingat1.00 and losing at 0.00, de-scaled with priceScale. Require all of these before you treat an instrument as resolved:
  1. settlementPreliminary is false,
  2. settlementPx ÷ priceScale is exactly 1.00 or 0.00,
  3. settlementSetTime is at or after expirationDate,
  4. the instrument has reached a terminal lifecycle state.
Anything else — a fractional price, a missing field, a settlementSetTime before expiry — holds for manual review. Do not auto-pay and do not auto-void.
[VERIFY] — condition 4 is the weakest link in this gate, and it decides payouts. InstrumentState has nine values, and which of them are terminal for resolution is not published anywhere. The only wire form we have evidence for is prefixed — INSTRUMENT_STATE_EXPIRED, seen in preprod — so the code below spells the set INSTRUMENT_STATE_CLOSED and INSTRUMENT_STATE_TERMINATED. Both the membership of that set and those two spellings are unconfirmed.Confirm the terminal set with your integration lead before you let this gate release money. Until then, treat an instrument state you do not recognise as a review case, never as terminal — and note that a partner coding to the lossy five-state subset on the legacy market-data page will miss states outright.
This gate is honest about its limits: it will also hold some genuine resolutions. Binary markets have been observed settling to non-binary values (-1.5 → 0.28, +1.5 → 0.77), unexplained, and cancelled events have settled inconsistently within a single event. Those cases fail condition 2 and land in your review queue, which is the correct outcome while the semantics are unresolved — see Cancelled and voided markets.
settlementPriceScale is reserved and reads 0 — de-scale with priceScale. int64 fields serialize as strings in JSON. proto3 does not put a scalar at its default value on the wire, so an absent field and a zero are indistinguishable: treat a missing settlementPx as “no value”, never as 0.00.

Detection code

The fields you branch on, as JSON:
settlementPx: "100" at priceScale: 100 is **1.00awinningcontract.AtpriceScale:1000thesamerawvalueis1.00** — a winning contract. At `priceScale: 1000` the same raw value is 0.10, which is a mark, not a win. That is why the scale is read per instrument and never hard-coded.
Confirm the response field spellings against a live call in your environment. REST responses are camelCase while requests are snake_case and webhooks are snake_case, and the instrument-state field on this response is not documented.

automaticResolution controls nothing

automaticResolution is reserved on the v2 surface and controls nothing. It reads false for all 223 preprod sports, including atp and wta, which do resolve automatically.Our own settlement guide told a partner to require automaticResolution: true before selecting a market, which made the gate unpassable as written. Gate on event series and symbol prefix instead.
[VERIFY] — the list of auto-resolving series and prefixes has to come from us, not from a guess. What is evidenced is that the atp and wta series resolve automatically, and that aec-atp is a real symbol prefix. No other prefix is evidenced: a plausible-looking sibling such as aec-wta is constructed, and a constructed prefix either never matches or matches the wrong markets. The snippets above therefore carry aec-atp only. Keep the list in configuration, not in code, so you can extend it the day your integration lead confirms the authoritative set.

Preprod will not show you a resolution

Markets do not resolve in preprod. Instruments reach INSTRUMENT_STATE_EXPIRED in 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. Test your gate against captured or synthetic messages and treat the first production resolution as the real test, with the review queue switched on.

Settlement fields

Five fields carry settlement, and three of them mean less than their names suggest. Winning contracts settle at 1.00andlosingcontractsat1.00** and losing contracts at **0.00.

The five fields

int64
The settlement price, on the wire as a scaled integer and in JSON as a string. De-scale it with the instrument’s priceScale: settlement_px ÷ priceScale. Do not use settlementPriceScale — it is reserved and reads 0, so dividing by it is a divide-by-zero or a silently wrong number depending on your language.A non-binary value on a non-resolved instrument is a mark, not an outcome.
bool
Not a resolved flag. false does not mean “resolved to an outcome” — marks carry false too, and there is no positive resolved indicator anywhere on the API. Gating a payout on this field alone pays out on marks. Use the full gate on Detect that a market resolved.
enum
The calculation method. CALCULATION_METHOD_VALUE appears on the wire with no documented meaning.
string
Not guaranteed present on a settled instrument. A partner who gated settlement on this field had a market stuck suspended in their system. Log it, show it, never branch on its presence.
timestamp
When the settlement value was set. It can precede expirationDate by ~13 hours on a mark, so a populated settlement_set_time is not evidence of resolution. In the gate it is a necessary condition (settlement_set_time at or after expirationDate), never a sufficient one.
Where they live: the market data stream or GET /v1/orderbook/{symbol}. CreateInstrumentStateChangeSubscription returns the updated Instrument and does not include them.

De-scaling, worked

The same raw value on a priceScale: 1000 instrument is $0.10 — a mark, not a win. Two of 121 open ATP instruments publish priceScale: 1000, so a hard-coded divisor is right everywhere else and wrong by 10× there. proto3 does not put a scalar at its default value on the wire, so an absent field and a zero are indistinguishable. Treat a missing settlement_px as “no value”, never as $0.00 — a losing contract and an unpopulated field look identical otherwise.

Fields that appear on the wire with no documented meaning

Not yet published — do not branch on any of the five values above. They are real values you will see, with no semantics we can stand behind. Log them, and ask your integration lead before you give any of them meaning in your code.
settlementPriceScale is in the same category with one difference: we know what it does, which is nothing. It is reserved and reads 0.

Fees

There is no settlement_fee on the partner API. It was cited to a partner in error and retracted. The only fees are the trading fees in the published Fee Schedule. Fee = Θ × C × p × (1 − p), rounded to the cent with banker’s rounding, at the taker coefficient — an ISV is structurally always the taker on CreateVendorOrder and can never earn the maker rebate. The coefficient changed on 2026-09-14 and there is no fee-schedule endpoint, so partners hand-edit it. The formula, the current coefficient and the scale handling are on Money on the wire. SETTLEMENT_FEE exists as a suppressed internal LedgerEntryType. Requesting it returns Aborted / 409. Its existence in the enum is not a fee you pay — see Balance ledger.

Gross or net: unanswered

Whether the RESOLUTION ledger entry is gross with COMMISSION deducted separately, or net, is unanswered.Do not infer it from executions: an execution folds commission into a single net ORDER_EXECUTION entry, and there is no basis for assuming settlement behaves the same way.Build your reconciliation so that either answer is a configuration change, and confirm the answer with your integration lead before you publish a settled-amount figure to a user.

Cancelled and voided markets

Cancellation and void semantics are not settled, and we will not give you a rule that might be wrong. Hold these positions for review instead of paying them.

What has been observed

Cancelled events have settled inconsistently — voided at 0.5, fractional, or a 404 — within a single event. There is an ITF-tennis $0.50 exception. Sources disagree on which behaviour is correct, so no value on this page is a rule.
Binary markets have been observed settling to non-binary values (-1.5 → 0.28, +1.5 → 0.77). This is unexplained, and it contradicts the rule that winning contracts settle at 1.00andlosingat1.00 and losing at 0.00.
Both items are unresolved. Until they are resolved, treat any settlement that is not exactly 1.00or1.00 or 0.00 as a case for a human.

What to do in the meantime

1

Do not auto-pay on a non-binary settlement

A fractional value is either a mark or an unexplained settlement, and you cannot tell which from the API. Both are reasons not to move money.
2

Hold the position for review

Route it to a queue with the symbol, the raw settlementPx, the instrument’s priceScale, settlementPreliminary and settlementSetTime attached. Those five values are what anyone will ask you for.
3

Reconcile manually against the ledger

The RESOLUTION ledger entry is what actually happened to the account. Reconcile against it rather than against your own expectation of the outcome — and note that whether that entry is gross or net of commission is itself unresolved, see Gross or net.
4

Do not originate a credit or a reversal

We credit the participant’s clearing account. If you pay or claw back from your own ledger while we are still settling the market, the two sides diverge — see Who credits the user.
5

Escalate the first one you see

Post the case in your shared Slack channel, tagged, with the environment and the symbol. These cases are how the semantics get resolved.
A 404 on a cancelled market’s symbol is one of the observed behaviours, so your handler must survive it: treat a 404 on a symbol you hold a position in as a review case, not as “no such market”.

No correction, bust or refund policy exists

Not yet published. There is no published correction, bust or refund policy, and no statement of whether institutional participants are in scope when retail positions are refunded after an outage. If you need to know what happens to your users’ positions in either case — and you do, before go-live — get it in writing from your integration lead.

What can go wrong

Next

Preprod and testing