Skip to main content
Every order names one instrument symbol, and every money field on that order is scaled by two numbers that live on that same instrument. You read the symbol and both scales off one response, so discovery and unit conversion are one job, not two.
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.eventId groups instruments into events.
  • metadata.event_id groups aec, asc and tsc instruments into the same event.
Those are two different groupings of the same instruments, and which one is authoritative is not documented.
do not build a single event key yet. eventAttributes.eventId and metadata.event_id group differently and we have not published which is canonical. Key your own rows on the instrument symbol, which is unambiguous, and carry both event identifiers alongside it rather than choosing one.

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.
Not yet published. We have not published the complete InstrumentState inventory or which states accept an order. Confirm both with your integration lead, and until then treat any state you do not recognise as not-tradeable rather than tradeable.

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.
The response shape, with only the fields you will branch on:

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:
Honour the retry after value in that message rather than your own backoff curve.
Not yet published. No separate preprod value is published for the reference-data limits. Assume they are the same in both environments and confirm with your integration lead before you build a refresh schedule around them.

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 no main_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.
Not yet published. We have not published a recommended derivation. Agree yours with your integration lead in writing, because it will determine which line your users see as the headline price.

2. Home and away are not recoverable by name

[GAP] On a same-nickname matchup, all four name fields carry the same string. For aec-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

do not gate on these fields. Tennis home_team_name, away_team_name and tournament_name are populated in preprod and empty in production. A tennis integration that passes preprod acceptance on those fields ships blank rows to production users.Build your tennis display to render correctly with all three fields empty, and source names from /v1/sports/players instead.

4. The sports market type enum grows without warning

The value lives on sportsMarketType 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.
Both endpoints are on gateway.polymarket.us, which is the retail surface. Engineering guidance is that partners should not build against gateway.polymarket.us, and /v2/home and /v2/live on it are internal-only. There is no institutional equivalent of players and teams today, so the only route to that data and the guidance about that host point in opposite directions.Raise this with your integration lead before you make player props a launch feature.
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

the gateway book and the exchange book disagree. They diverge in both depth and values, and which one is authoritative is undocumented. Partners who compared them found the gateway book “more reasonable”, which is not a basis for pricing customer orders.Price against the exchange book you trade on — GET /v1/orderbook/{symbol} or the market-data stream on api.*.polymarketexchange.com — and do not reconcile your fills against gateway depth.

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

Divide by the same scales to go back. They are multipliers, not exponents and not decimal places. A 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 0.01,not0.01, not 1.00.
commission_notional_collected has no scale field and no comment, in the protos or in the docs. Nothing on the wire tells you which divisor to use — you must compute it from the instrument’s two scales yourself.Reading it at priceScale instead of priceScale × fractionalQtyScale turns a 0.01feeinto0.01 fee into 1.00 on a 32-cent trade. That is exactly what happened: the inflated fee exceeded a real customer’s entire prefund and suppressed their refund.

The most common reconciliation mistake

Using raw order_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

where 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.045roundsto0.045 rounds to 0.04 and a fee of 0.055roundsto0.055 rounds to 0.06. Round-half-up drifts against you by a cent on every tie. The published schedule has two coefficients:
The taker coefficient changed from 0.06 to 0.0695 on 2026-09-14, and [GAP] there is no fee-schedule endpoint. Every partner had to hand-edit their systems, because Θ is not readable from the API at all.Do not hard-code Θ in more than one place, and confirm today’s value with your integration lead before you invoice a user for a fee. The worked example below uses 0.06.

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 with priceScale: 100 and fractionalQtyScale: 100:
[VERIFY] — the field name carrying the executed price is not published. The block below spells it "price", and that spelling is unconfirmed; so is the rest of the execution report’s field set. clord_id, order_qty and commission_notional_collected are named in verified sources.The lesson below — that the fee is 0.01andnot0.01 and not 1.00 — depends on the two scales and on commission_notional_collected, not on this spelling. Read the price field’s real name off your own execution report before you wire up a decoder.
Decoded, that report is: 0.75 contracts, 0.32,0.32, **0.24 principal and a 0.01fee.ReadthefeeatpriceScalealoneandyouget0.01 fee.** Read the fee at `priceScale` alone and you get **1.00** — four times the entire principal of the trade.
All four print the same two lines:

What can go wrong

Next

Place an order