Prerequisites: an access token with
read:reports
(Authentication) and the participantId we issued you
(Firms, participants and accounts) — reports are
account-scoped, so report calls carry x-participant-id; a running drop-copy consumer and
balance-ledger consumer (Choose a stream); a pinned copy of the proto
bundle you build against (Protos and SDKs); and a shared Slack channel
with us plus a named integration lead. If you do not have the last two, start at
Path to production.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.
Reporting and bulk export
If you are an IB, this differs.
This section and Daily reconciliation are where your reporting pack
comes from. Read Reporting pack alongside them: it maps the ten
named reports to the surfaces below and names the five that have no source today.
The number that decides your export design
POST /v1/report/trades/search unfiltered hits the 30-second CloudFront 504 about two thirds of the time. The same call with a symbol filter returns in ~130 ms, reliably.
A 504 means you spent more than 30 seconds at the edge. It is not a retryable blip: an unfiltered query will keep costing you 30 seconds and then failing, so retrying it is the wrong response. Narrowing it is the right one. Filter by symbol, and sweep a day by iterating over the symbols you care about.
Pagination
Pages come back with anextPageToken. Keep requesting until it is absent.
Page tokens are offset-based, not cursor-based. An offset does not pin the underlying rows, so a
row can appear on two pages or be skipped between them if data lands mid-sweep. Dedupe as you
sweep, keyed per symbol — that is why sharding by symbol is also what makes the dedupe tractable:
each shard is a small, self-contained set you can hold in memory and reconcile.
EOF behaviour on the last page is a known defect that partners have chased 404s over. Treat an
absent nextPageToken as the end of the sweep and do not issue one more request to confirm it.
Runnable export
This sweeps one day, one symbol at a time, dedupes each shard, and writes newline-delimited JSON. It raises on any terminal failure and never swallows one.GetTradeStats
The semantics below were explained to partners by hand and published nowhere. All four have caused a
wrong number in a partner’s system.
int
A divisor, not an interval. The window you request is split into that many equal slices, so
bars: 6 over 14 days gives 2.8-day buckets — not six one-day bars. If you want daily buckets, set
bars to the number of days in your window.string
Use this, not
field_filter. field_filter validates against a fixed column allowlist that
excludes update_time and create_time, so a time cursor expressed there returns
InvalidArgument: field not allowed.number
Reflects post-clearing state, so these read zero for recently executed trades. A zero here is
not a data error and is not a reason to re-query.
/v1/report/trades/stats does not require x-participant-id.
Ledger CSV downloads
Ledger CSV downloads are limited to roughly 5 per minute per firm. Exceeding it puts you into the standard 429 path, which carriesRetry-After. Five per minute per firm is a firm-wide budget, so a
second process pulling CSVs shares it with you — run exports from one place.
Rate limits that shape a sweep
All values below are per firm unless stated. Exceeding any of them returns 429 with aRetry-After header.
At 12 calls per minute for
SearchTrades, a symbol-sharded sweep is rate-limited long before it is
latency-limited: 60 symbols is five minutes of wall clock at the cap. Size your sweep window
accordingly rather than parallelising into 429s.
There is also an endpoint-level rung that is not on the published table and was unknown even to
support:
retry after value out of that message rather than assuming your usual backoff.
Daily reconciliation
With an export path that survives the edge timeout, you can close the day. Reconcile your own persisted stream record against a small number of anchor reads, per concern, once a day.The three rules
1. Streams are the system of record. Unary reads are anchors.
Drop copy carries execution reports and trade capture and is resumable — itsresume_token is
populated, at roughly 576 bytes. Persist every message as it arrives and treat your own store as
authoritative.
The anchor reads exist to catch a gap in your consumer, not to rebuild your book. Two reasons they
cannot be the record:
- Stored execution history is archived during maintenance, and execution queries for the period before a window return empty. An anchor read after an overnight window can legitimately return less than your stream record. If the read were your record, a maintenance window would delete your books.
- The read caps are low:
SearchOrders,SearchExecutionsandSearchTradesare 12/min per firm. You cannot poll your way to a book.
2. One elected leader per stream.
A subscription session is keyed on caller identity — the token subject plus the participant header — not on the accounts or symbols you requested. A second process holding the same identity and opening the same subscription takes the session away from the first, which surfaces as13 INTERNAL: Subscription manager revoked session. Nine partners investigated that error
independently before this was written down.
So: elect one consumer per stream, run the others as hot standbys that consume nothing until they win
the election, and have the winner resume from the shared persisted checkpoint. The cap reinforces it:
20 concurrent streams per firm across all gRPC subscriptions, so duplicated consumers are also
spending a budget you need for market data.
Known workaround for repeated revocations: pass an explicit list of markets on
CreateOrderSubscription — there is no symbol cap on that path.
3. A process restart is not a cold start.
On restart, read your persisted checkpoint and resume the stream against your existing local database. Do not re-run the day’s anchor reads, and do not replay from the start of the day. A restart that re-reads is how partners hit 429 on a 12/min endpoint during an incident, which is the worst possible moment to lose your reads. It is also how a maintenance-archived window silently truncates a book that was complete in your store a minute earlier. Cold start means exactly one thing: you have no checkpoint at all. Everything else resumes.
Snapshot-then-delta applies on subscribe, delivery is at-least-once, and redelivery happens. Dedupe
on your own key before you apply anything.
Per-concern reconciliation
Position change is also the stream that answers “did a market I hold resolve?” — not instrument
state change, which carries the updated
Instrument and not the settlement result fields. One
partner built settlement against instrument state change and had to be redirected.
The identities you can assert
These four close with the facts available today. Assert them daily and alert on any break.- Cash, per participant account.
- Executions. Every execution report in your drop-copy store for day D appears in a
symbol-filtered
POST /v1/report/trades/searchfor day D, and the reverse. A row present only in the anchor is a consumer gap; a row present only in your store, after a maintenance window, is expected. - Fees, per fill.
Cis the de-scaled contract count. Using raworder_qtyon afractional_quantity_scale = 100instrument overstates the fee 100×, and on a scale-1instrument the naive math is accidentally correct, so this break only ever shows up on your first scale-100 fill. Θ is the taker coefficient; you are structurally always the taker and can never earn the maker rebate. - Orders to effects. Every
EXPIREDorder produces no execution, no ledger entry and no position change. AnEXPIREDorder with any downstream effect is a real break and worth escalating immediately.
priceScale × fractionalQtyScale notional units. When both are 100,
commission_notional_collected = 100 means 1.00 — reading it one scale short turned a
1.00 on a 32-cent trade, exceeded a customer’s prefund and suppressed their refund.
The taker coefficient changed on 2026-09-14, and there is no fee-schedule endpoint. Store Θ with
an effective date in your own configuration and reconcile each fill against the coefficient in force
on its traded day, not against today’s. Every partner hand-edited their systems for that change.
Runnable end-of-day skeleton
This resumes from a checkpoint, never replays the day, reconciles executions and fees per symbol against the anchor read, and exits non-zero on a break. It reuses the auth andpost helpers from
Reporting and bulk export rather than restating them.
Where reconciliation does not close today
State these three as known differences in your own controls, because none of them has a fix you can implement. Sub-cent residue. Transfers accept at most 2 decimal places, and balances carry more —172.395 and 1377.57275 are both on record. There is no sanctioned handling for the residue, so a
participant balance cannot be swept to exactly zero. Carry it as a standing, named difference rather
than forcing it.
Accrued vendor fees are not exposed on any endpoint. The platform never knows your fee basis, so
you track accruals yourself and reconcile against the daily Vendor Fees report. Your spendable-cash
figure must subtract your own accrued, uncollected fees, because nothing on the API does it for you.
No firm-level ledger stream. The balance-ledger stream is per account and counts against the
20-streams-per-firm cap, so you cannot run one per participant. The firm-level stream is “coming
soon” with no date, which leaves no scalable push channel for RESOLUTION for any ISV at scale.
Two smaller ones worth encoding: twelve internal LedgerEntryType values are suppressed
(NETTING, GIVE_UP, INTEREST, SETTLEMENT_FEE among them) and requesting one returns Aborted
/ 409, so filter your ledger queries to the allowlisted types. And proto3 does not populate scalar
fields at their default value, so an absent field and a zero are indistinguishable on the wire —
do not treat a missing number as a break.
Breaking changes and the changelog
A daily close only holds if the surface underneath it does not move without telling you. Subscribe to the changelog by RSS or Slack/feed subscribe, and treat it as the only authoritative record of a change.
What to subscribe to
Subscribe both a human and a machine-readable consumer. If a feed entry arrives with no release date,
that is a known authoring defect on our side rather than a malformed release: a JSX fragment used as
an entry label slugified the anchor to
#object-object and dropped the date from the feed. Labels are
plain strings now; report an undated entry.
The commitment
Every RPC or field removal gets a changelog entry, a migration note naming the replacement, and a deprecation window. An entry with no migration note is incomplete and worth pushing back on in your shared channel.The honest history
This section exists because the commitment above has not always been met. Four events, all on the record: 2026-08-06 — three RPCs removed with no changelog entry.CreateFundedOrder,
PreviewFundedOrder and OdfSweep were removed and replaced by CreateVendorOrder plus Transfer.
No entry shipped. A partner discovered the removal through gRPC reflection when their entire money
path broke. That discovery path is not even generally available: reflection is entitlement-gated and
returns PermissionDenied: method not permitted without the grant, so most partners would have had no
way to find out at all. A backdated Breaking Change entry for this removal is part of the current
overhaul.
Exchange migration dates moved at least three times, and partners reconstructed the timeline
themselves. If your plan depends on a migration date, hold it as a range in your own tracker and
re-confirm it in writing before you commit a client date to it.
Two changelog corrections were issued as Slack replies rather than changelog amendments. A reader
of the changelog therefore held a wrong value with no way to know it had been corrected.
A correction shipped and did not propagate. Changelog v0.0.25 on 2026-04-17 corrected the gRPC
hostname form to grpc-api.{env}.polymarketexchange.com, but missed /trader-guide/environments,
which still publishes grpc-preprod.polymarketexchange.com. A changelog entry does not guarantee
every page was updated; the changelog wins over a page.
The rule that follows
The changelog is the only authoritative channel for a change. A Slack correction is not a change record. Concretely, for you:- If a value you rely on is corrected in Slack, ask for a changelog entry or amendment in the same thread. Quote the corrected value back so the thread contains it either way.
- If a change reaches you only through Slack, treat it as unconfirmed until it appears in the changelog, and say so in the thread.
- Diff the changelog against your own integration surface on a schedule, rather than reading entries as they arrive. Four of the changes below were only actionable once someone compared a release note to their own code.
Changes that have cost partners work
Read these as the shapes to expect, not as a complete list.
An enum addition is a breaking change for you even though it is additive for us: code that switches
exhaustively over
market_sport_type will throw on a value added in a release you did not read.
Default your enum handling to a safe unknown branch and log it.
Pin what you build against
Two properties of what we ship make version drift your problem to manage:- The proto bundle is an unversioned, anonymous Drive zip (
polymarket-protos.zip) with no changelog and no checksum. Neither side can tell which build you hold. Checksum your copy yourself, record the hash with your release, and quote it in any ticket about a field or RPC. - The bundle advertises 14 services and defines 5.
OrderFundingServiceandCashMovementServiceare absent from it entirely, which is exactly where your money path lives.CreatePositionSubscriptionhas no proto definition anywhere.
CreateCashMovement proto has also shipped with a wrong field number: transfer is
field 4 in the real service, and the published copy omitted an intent occupying field 2 and renumbered
transfer down to fill the gap. A partner’s "20.00" therefore arrived as that internal intent,
which is why the error named amount. If an error names a field you did not set, suspect your proto
build before you suspect your code, and quote your checksum. → Protos and SDKs
Stability labels
Support and escalation
Where to take a problem, and what to put in the message, depends on your partner type.ISV support and escalation
Routing, what to include by category, severity, and the routes that are not yet defined.
IB support and escalation
The same, plus IB-specific routes and what a regulatory or reporting question needs.