Skip to main content
This is the work that continues after go-live: pulling activity out, closing the books against it, catching a change before it breaks you, and getting an answer when something does. Each section stands alone; read the reporting one first, because the reconciliation close depends on it.
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.
Pull a day of activity by sharding your report queries by symbol; an unfiltered query times out at the edge about two thirds of the time.

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.
No async report job, no published window limit and no documented bulk-export path exist.There is no “request a file, collect it later” facility. Sharding by symbol inside the 30-second budget is the whole mechanism. Because no window limit is published, treat one day per symbol as your unit and measure before you widen it.

Pagination

Pages come back with a nextPageToken. 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.
Two names in this snippet are not published and are set as constants at the top.Confirm ROWS_KEY, PAGE_TOKEN_FIELD and the filter shape with your integration lead and set them once. Everything else in the snippet is verified.
The only response field this loop branches on:
Present means another page. Absent means the sweep is done.

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.
Empty buckets are returned, not omitted, as an all-zero object. Treat zero as “no trades”, never as a print at price 0. A partner averaging bucket prices across a sparse window without this rule computes a price that never traded. /v1/report/trades/stats does not require x-participant-id.
One current page lists trade stats among endpoints that do not require x-participant-id, and a partner hit PERMISSION_DENIED on exactly that call. If you get PERMISSION_DENIED on trade stats, send the header and tell us, rather than assuming a missing scope.

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 carries Retry-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.
The ledger CSV download path is not published here. The limit is published; the call is not. Get the path from your integration lead.

Rate limits that shape a sweep

All values below are per firm unless stated. Exceeding any of them returns 429 with a Retry-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:
Read the retry after value out of that message rather than assuming your usual backoff.
No preprod-specific values are published for any of these limits. Assume the production numbers apply in preprod and confirm before you tune a sweep against them. The published summary table also states “0.5–60 req/min” for query endpoints; nothing is documented at 0.5/min and the lowest real value is 6/min.

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 — its resume_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, SearchExecutions and SearchTrades are 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 as 13 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.
No positions anchor read is published. GetAccountBalance returns InvalidArgument: invalid account for a participant clearing account while succeeding for a firm account, and partners currently derive a participant’s cash from the newest balance-ledger entry’s afterBalance — a ledger scan standing in for a one-field read. Ask your integration lead which read anchors positions before you design a break report around it.

The identities you can assert

These four close with the facts available today. Assert them daily and alert on any break.
  1. Cash, per participant account.
  2. Executions. Every execution report in your drop-copy store for day D appears in a symbol-filtered POST /v1/report/trades/search for 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.
  3. Fees, per fill.
    C is the de-scaled contract count. Using raw order_qty on a fractional_quantity_scale = 100 instrument overstates the fee 100×, and on a scale-1 instrument 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.
  4. Orders to effects. Every EXPIRED order produces no execution, no ledger entry and no position change. An EXPIRED order with any downstream effect is a real break and worth escalating immediately.
One dollar is priceScale × fractionalQtyScale notional units. When both are 100, commission_notional_collected = 100 means 0.01,not0.01, not 1.00 — reading it one scale short turned a 0.01feeinto0.01 fee into 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 and post helpers from Reporting and bulk export rather than restating them.
The break report is the deliverable, not a clean run. Alert on the count, keep the per-symbol detail, and never let the job repair your store automatically — a repair that trusts the anchor read will delete executions that maintenance archived.

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.
The Vendor Fees report has no delivery mechanism. The fee-collection loop has no defined channel today. Agree a manual delivery with your integration lead and record which side owns the comparison until this ships.
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.
Three settlement-side questions are open and two of them affect a daily close.Until the gross-versus-net question is answered, do not assume RESOLUTION behaves like ORDER_EXECUTION. Identity 1 above holds either way; a per-entry fee attribution does not.
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 length of the deprecation window is not yet published. Do not plan a migration against an assumed window. Ask your integration lead for the committed minimum, and until it is published, treat any announced removal as immediate work.

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.
There is no fee-schedule endpoint. Store Θ in your own configuration with an effective date, and reconcile each fill against the coefficient in force on its traded day. → Daily reconciliation
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. OrderFundingService and CashMovementService are absent from it entirely, which is exactly where your money path lives. CreatePositionSubscription has no proto definition anywhere.
The published 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

There is no published convention for what alpha, beta and GA mean on this surface.Until one exists, “beta” on a page tells you a surface is enabled per partner and may change. It does not tell you what notice you get. Ask for the notice commitment per surface you depend on, in writing.

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.

What can go wrong

Next

Start here