Before this page: a working access token and the
read:instruments scope granted against your
client (Authentication). Scopes are granted server-side — you
cannot request them on the token exchange, and you must re-mint your token after we add a grant.Instrument reads do not need x-participant-id. Neither does /v1/refdata/* or
CreateInstrumentStateChangeSubscription, which needs read:instruments only. See
Firms, participants and accounts for where the header
does apply.API reference: Reference data · Order book. Opens on the public documentation site in a new tab.
Where it disagrees with this page, this page is authoritative for the partner surface.
Find instruments
The hierarchy
Reference data is five levels deep: category → series → event → market → instrument (symbol) Only the bottom level is tradeable.order.symbol names an instrument, market-data subscriptions take
instrument symbols, and there is no RPC that accepts a category, series, event or market as a trading
identifier. If your data model stops at “event”, you cannot place an order.
The two levels above the instrument are where partners lose time, because grouping is ambiguous:
eventAttributes.eventIdgroups instruments into events.metadata.event_idgroupsaec,ascandtscinstruments into the same event.
Two spellings for the same field
The same instrument value is spelled differently depending on which surface you read it from: REST responses are camelCase (priceScale, fractionalQtyScale, eventAttributes.eventId) and the
institutional proto surface is snake_case (fractional_quantity_scale, metadata.event_id,
market_sport_type, long_participant_id). Normalize on ingest. A parser that looks for
fractionalQtyScale on a proto message finds nothing, and an absent scale is not a scale of 1 — see
the proto3 caveat.
What you need off an instrument before you can order
InstrumentState has nine values, including PENDING, CLOSED, TERMINATED and
MATCH_AND_CLOSE_AUCTION. Treat the enum as open — code that switches exhaustively over a five-state
subset will silently mishandle the other four.
Read the instrument list
The generated stub names below assume you ran
protoc over polymarket-protos.zip with the package
layout that bundle ships. The bundle is unversioned, has no checksum and no changelog, so neither
side can tell which build you hold — check your generated package path before copying these imports.
OrderFundingService and CashMovementService are absent from the bundle entirely.Rate limit
ListInstruments is 6 requests per minute, per firm. ListSymbols is the same; GetOrderBook
and GetBBO are 12/min each. Exceeding a REST limit returns 429 with a Retry-After header.
There is also an undocumented per-endpoint rung that support did not know about, which returns gRPC
code 8 with its own backoff hint:
retry after value in that message rather than your own backoff curve.
The four gaps you will hit
These are the real reasons a sports integration stalls. None of them has a workaround we have not listed.1. There is no main line, and you compute it
[GAP] There is nomain_line and no is_main field on any instrument. The decision on record is
that partners compute the main line themselves. That decision exists only in a Slack message, so
nothing in reference data will ever mark it for you.
Rule: derive the main line from the instruments you already hold — do not wait for a field to appear,
and do not treat the first instrument in a list as the main line, because list order is not documented
as stable.
2. Home and away are not recoverable by name
[GAP] On a same-nickname matchup, all four name fields carry the same string. Foraec-cfb-clmsn-lsu-2026-09-05 — Clemson at LSU — long_participant_name,
short_participant_name, home_team_name and away_team_name all come through as "Tigers". There
is no name-based way to tell which side is home. This is common in CFB and CBB, not an edge case.
Rule: resolve sides from the symbol’s team slugs (clmsn, lsu in the example) or from
/v1/sports/teams, never from the four name fields. A display layer that reads home_team_name will
print “Tigers vs Tigers”.
3. Tennis name fields are populated in preprod and empty in prod
4. The sports market type enum grows without warning
The value lives onsportsMarketType on the retail surface and market_sport_type on the
institutional surface. It is not a fixed vocabulary: 71 values were added on 2026-09-08 (v0.0.86)
and 8 more on 2026-09-09 (v0.0.87) — 79 new values in two days. One partner waited 13 days to be
given the enum inventory.
Rule: treat it as an open string. Map the values you support, route everything else to a generic
renderer, and alert on unmapped values instead of dropping the instrument. Subscribe to the changelog
feed — additions ship there, not to a schema endpoint.
Players and teams
/v1/sports/players and /v1/sports/teams exist and are how you get participant metadata.
long_participant_id keys to SportRadar and SDIO provider IDs, so join your own provider data on
long_participant_id rather than on any name field. These two endpoints were undocumented for six
months and that blocked one partner’s player props from March to September 2026.
Credentials do not carry across. gateway.polymarket.us and api.polymarket.us are a different
product with X-PM-Access-Key / X-PM-Timestamp / X-PM-Signature (Ed25519) auth. Those credentials
never work against api.*.polymarketexchange.com, and the reverse is also true.
Books diverge between surfaces
Money on the wire
Convert every price, quantity, cash amount and fee between your decimal ledger and the wire using the two scales published on the instrument you are trading — the same two scales you just read off it. This section caused a real customer-money loss. Every number in it is load-bearing.The three conversions
priceScale of 100 does not mean “two decimal places” and does not mean “10²”. It means multiply
by 100. Code that treats the scale as an exponent computes 10^100 where it should have multiplied
by 100 — a number no money field can hold, on an instrument whose scale is one of the two live
values.
Note the third line: cash_order_qty is a USD amount and it is scaled by priceScale, not by a
cash scale and not by priceScale × fractionalQtyScale. There is no separate cash scale on the
instrument.
Derive both scales per instrument. Never hard-code them.
priceScale 100 and priceScale 1000 are both live in production. So are fractionalQtyScale 100
and fractionalQtyScale 1.
Two of 121 open ATP instruments publish priceScale: 1000 with fractionalQtyScale: 100, which
makes the notional divisor 100,000 rather than 10,000. A hard-coded divisor of 10,000 is correct
only where both scales are 100 — it is wrong by 10× on those two instruments, and wrong by 100×
on any instrument publishing fractionalQtyScale: 1, where the real divisor is 100. The table below
gives all three combinations.
That is the worst available failure shape: it reconciles perfectly for weeks, passes preprod
acceptance, and then breaks on two symbols out of 121 with no error anywhere. Read both scales off
the instrument on every order and every fill.
One dollar is priceScale × fractionalQtyScale notional units
Notional fields — commission_notional_collected above all — are quoted in units of
1 / (priceScale × fractionalQtyScale) dollars.
When both scales are 100,
commission_notional_collected = 100 means 1.00.
The most common reconciliation mistake
Using raworder_qty as the contract count.
On an instrument with fractional_quantity_scale = 100, order_qty: "75" is 0.75 contracts, not
75. Feeding 75 into the fee formula overstates the fee by 100×.
It hides because instruments with fractionalQtyScale = 1 make the naive math accidentally correct.
Your reconciliation passes on every scale-1 market, so the error usually surfaces on your first fill
in a scale-100 market — often in production, because preprod liquidity is concentrated in different
symbols than production volume.
Rule: divide order_qty by that instrument’s fractionalQtyScale before it touches any formula, and
assert the scale was read from reference data rather than defaulted.
Fees
C is the de-scaled contract count and p is the de-scaled YES price. Round the result to
the cent with banker’s rounding (round-half-to-even): a fee of 0.04 and a fee of
0.06. Round-half-up drifts against you by a cent on every tie.
The published schedule has two coefficients:
Combos are fixed constants
Combo instruments (caoc) do not vary: tick size $0.001, priceScale 1000, qtyScale 100.
Those are fixed, so hard-coding them for caoc is correct — this is the one place the per-instrument
rule does not apply.
You have to hard-code them, because the RFQ stream does not carry scales at all and per-combo
reference-data lookups are not viable at roughly 100 combos per second against a 6/min
ListInstruments limit.
Settlement prices
settlementPriceScale is reserved and reads 0. It is not the scale of settlementPx.
De-scale settlementPx with priceScale. A divide by settlementPriceScale is a divide by zero.
Two wire-format traps
int64 fields are serialized as strings in JSON. order_qty, cash_order_qty and
commission_notional_collected arrive as "75", not 75. A JavaScript client that does
Number(report.commission_notional_collected) works until a value exceeds 2^53; parse them as
BigInt or Decimal, never as a float.
proto3 default-value caveat: a scalar at its default value is not populated on the wire, so an
absent field and a zero are indistinguishable. You cannot tell “no scale published” from
“priceScale is 0”. Treat a missing or zero priceScale or fractionalQtyScale as a hard error and
refuse to place the order — never fall back to 1, and never fall back to 100.
Tick sizes of 0.5¢ and 0.25¢ are both live, so a price that is a valid multiple of one tick can be
off-tick on another instrument.
A worked decode
A real-shaped execution report for a fill of 0.75 contracts at $0.32 on an instrument withpriceScale: 100 and fractionalQtyScale: 100: