> ## Documentation Index
> Fetch the complete documentation index at: https://partners.docs.polymarket.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Preprod and testing

> What preprod mirrors, what it does not, and which test flows you cannot drive to a known outcome today.

Preprod runs the production API surface against a different liquidity source and a different resolution path, so some of your test flows cannot complete there at all. Read the fidelity matrix before you put a milestone behind one of them, then size your harness against the fixtures that actually exist.

<Info>
  Prerequisites: preprod credentials and a preprod keypair ([Authentication](/authentication)),
  both of your firm names
  ([Firms, participants and accounts](/identity)), and — for the test
  harness — a funded preprod pool. **Preprod access does not require a signed agreement.** One partner
  lost roughly three weeks believing it did.
</Info>

## What preprod does and does not simulate

### Fidelity matrix

| Area                        | Preprod behaviour                                                                                                                                | Mirrors production?      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ |
| REST and gRPC surface       | Same endpoints, same RPCs, same error strings                                                                                                    | Yes                      |
| Credentials                 | Separate RSA keypair and separate Auth0 tenant. Preprod credentials return no access token against prod                                          | No, and deliberately not |
| Liquidity provider          | A live liquidity provider with thin and uneven symbol coverage. A second liquidity source is configured but not running, and contributes nothing | No                       |
| Book depth                  | Shallow and intermittent, including "straight 0 for a few minutes"                                                                               | No                       |
| Order surface               | `CreateVendorOrder`, FOK only, always taker                                                                                                      | Yes                      |
| **Market resolution**       | **Markets do not resolve.** Instruments reach `INSTRUMENT_STATE_EXPIRED` instead                                                                 | No                       |
| Instrument lifecycle timing | Expiry arrives in large batches, not spread across the day: 363, 455, 544 and 549 instruments in a single session                                | No                       |
| Execution history retention | **Stored execution history is archived during maintenance, and pre-maintenance execution queries return empty**                                  | No                       |
| Maintenance windows         | Near-weekly one-off windows, not the fixed window the FAQ describes                                                                              | No                       |
| Pool funding                | Funded manually by us on a Slack request                                                                                                         | No                       |
| Status signal               | None. There is no preprod status page and no partner-checkable preprod health endpoint                                                           | No                       |
| Shared services             | At least one published transfer ceiling is described as shared across all partners on preprod, so another partner's traffic can affect yours     | See the note below       |

The endpoints themselves:

<Snippet file="endpoints.mdx" />

The gRPC hostname form is `grpc-api.{env}.polymarketexchange.com`. The form `grpc-preprod.polymarketexchange.com` appears on the current `/trader-guide/environments` page and is wrong; it has been wrong since the correction in changelog v0.0.25 on 2026-04-17 missed that page.

<Warning>
  **There are three environments — dev, preprod and production — and you build against preprod and
  production.**&#x20;
  Dev is not part of the partner path. If you were given a third set of credentials, confirm what it
  is with your integration lead before you point a test suite at it.
</Warning>

### Liquidity: what is actually on the book

`pmsim` is the live liquidity provider in preprod. Symbol coverage is thin and uneven: roughly **48% of `astatc-` symbols, 3.7% of `aec-` symbols and 0.1% of `aec-atp` symbols** carry any LP presence at all. Coverage is per environment; production liquidity comes from market makers and does not follow these numbers.

Depth, where it exists, sits at the edges of the book. Of 121 open ATP instruments in one sweep, only a small minority showed any resting depth, and those showed it at prices like a **$0.98 offer and a $0.06 bid**. Books also go empty for minutes at a time.

#### Why a thin book blocks you completely and does not affect market makers

`CreateVendorOrder` accepts `TIME_IN_FORCE_FILL_OR_KILL` and nothing else, so you are structurally always the taker and can only consume resting depth. With no resting depth there is nothing to take, and your order comes back `EXPIRED` — an FOK that never crossed, which is not a rejection and carries no reason by design. Market makers are unaffected because they *post* the resting depth rather than taking it, which is why "preprod is fine" and "preprod is unusable" are both true statements from different partners.

The practical consequence: you cannot produce a fill on demand in preprod, so you cannot deterministically test anything downstream of a fill. See [Test harness](#test-harness) for the workarounds that exist.

### Resolution: markets do not resolve

**No market resolves in preprod.** Instruments reach `INSTRUMENT_STATE_EXPIRED` in large batches — 363, 455, 544 and 549 instruments in a single observed session — with none resolving to an outcome.

One partner asked six times over 40 days to be able to test a settlement flow and never could. If your plan includes a preprod settlement test, it does not currently work; raise it in Slack as a resolution request against a specific market you already hold rather than waiting for one to resolve on its own.

`atpcq` is **absent from the preprod resolution config**, so it will not resolve even if a resolution is run for you.

### Non-parity items to design around

* **`account_beneficial_owner` is unsupported in preprod.** Do not treat a preprod pass on an onboarding payload as proof the field works in production.
* **`atpcq` is absent from the resolution config.** Pick a different series for anything resolution-shaped.
* **`priceScale` 100 and 1000 are both live in production**, and two of 121 open ATP instruments publish `priceScale: 1000` with `fractionalQtyScale: 100`. Whichever instruments preprod happens to serve you is not a sample of production scale combinations. Read both scales per instrument. → [Money on the wire](/instruments#money-on-the-wire)
* **Tennis `home_team_name`, `away_team_name` and `tournament_name` are populated in preprod.** The same fields have been reported empty in production.

<Warning>
  **Tennis reference fields differ by environment.** Do not build a display path that
  depends on those fields being present.

  Treat them as optional in both environments until this is confirmed.
</Warning>

### Preprod-only error strings

| String                                                                                             | What it means                                                                                                                                      | What you do                                                                             |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `market data snapshot admission rejected: heap_pressure heap_inuse=… soft_limit=… reject_pct=0.95` | A market-data snapshot was refused by an admission guard. **That guard is wrong in every environment**; preprod is only where it was first noticed | Retry the snapshot. Report it with the verbatim string; it is our defect, not your load |
| `{"code":12,"message":"unknown service connamara.ep3.v1beta1.KYCAPI"}`                             | gRPC code 12 `UNIMPLEMENTED` against a service name that is not served on that target                                                              | Use the REST KYC endpoints. → [Start verification](/kyc#start-a-verification)           |

Neither string indicates a fault in your client. Both are worth reporting verbatim, with the environment named.

### Funding a preprod pool

**There is no self-service test funding.** Preprod pools are funded manually by us on a Slack request. Ask in your shared channel, name the firm, and ask for an amount that covers your whole test plan rather than one run — a request per run costs you a round trip through a human each time.

Test cash behaves like production cash once it is there: you move it into a participant account with `Transfer`, and it stays in that account. → [Move cash](/funding#move-cash)

<Warning>
  **The transfer rate limit is being confirmed and no number should be coded against yet.**

  One of the live statements describes a ceiling **shared across all partners on preprod**, which
  would mean another partner's transfer traffic can throttle yours. Do not size your transfer loop
  against any of these until your integration lead confirms one. This blocked a partner's entire
  order flow as their stated number-one issue.
</Warning>

### Maintenance and data retention

The FAQ presents a stable Thursday 2am–4am ET window. Through August and September 2026 preprod actually ran **near-weekly one-off windows** at 2–8am, 3–7am, 4–7am and 5–8am.

**Stored execution history is archived during maintenance, and execution queries for the period before a window return empty.** This is a retention fact, not a bug: if your reconciliation test reads yesterday's executions after an overnight window, it will legitimately read nothing. Persist your own copy of every execution as it arrives on drop copy and reconcile against that. → [Daily reconciliation](/operations#daily-reconciliation)

<Warning>
  **There is no published preprod maintenance calendar.**&#x20;
  Ask your integration lead before scheduling a certification run, and do not schedule one overnight
  ET.
</Warning>

### What preprod does not give you

* **No preprod status page.** `status.polymarketexchange.com` covers production only.
* **No working preprod health endpoint.** `GET /v1/health` is documented as `{"status":"ok"}` with no auth, but partners have reported 404 in production and 401 in preprod against it.
* **No published maintenance calendar**, per above.

<Warning>
  **`GET /v1/health` is not a usable liveness check.**
  Do not use it as a liveness check.&#x20;
  The combined effect of these three gaps is that when preprod is down you cannot tell it is down.
  Treat an unexplained preprod failure as ours until proven otherwise and post it in Slack with a
  timestamp. → [Support and escalation](/operations#support-and-escalation)
</Warning>

## Test harness

This section tells you which parts of your integration you can drive to a known outcome on demand, and which you cannot, so you size your test plan against what exists rather than what you would expect to exist.

### Start from the examples repo

`github.com/Polymarket-US/partner-api-examples` contains runnable scripts plus a **full end-to-end test covering order placement, execution and balance update via drop copy**. It is not linked from any other docs page, which is why most partners have never seen it. Clone it before you write your own harness; it is the only worked example of the full money path we publish.

The repo is not a substitute for pinning your protos. The proto bundle is an unversioned, anonymous Drive zip with no changelog and no checksum, so neither side can tell which build you hold. → [Protos and SDKs](/environments#protos-and-sdks)

### What you can deterministically test

| Thing to test                                   | Can you today                                   | How                                                                                                                                                  |
| ----------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Token mint and refresh                          | **Yes**                                         | Mint against the preprod Auth0 token endpoint. `expires_in` is `180`; honour it minus a 30-second buffer rather than hardcoding 180                  |
| A missing scope                                 | **Yes, by observation only**                    | Scopes are granted server-side; you cannot request them. Call an endpoint you lack, observe `PERMISSION_DENIED`, then re-mint after we add the grant |
| KYC reject                                      | **Yes**                                         | Magic email `reject@example.com`                                                                                                                     |
| KYC manual review                               | **Yes**                                         | Name `Paulina Gizela` triggers **manual review, not DocV**                                                                                           |
| KYC DocV flow                                   | **Yes**                                         | Phone `+12125551234` starts the DocV flow. `sdkKey` in the response is always empty, by design                                                       |
| Webhook wiring                                  | **Yes**                                         | `webhook.test` is one of only three events that fire. Verify your signature check against `"<webhook-id>.<webhook-timestamp>.<raw body>"`            |
| Non-terminal KYC states                         | **No event exists**                             | Only `kyc.approved`, `kyc.rejected` and `webhook.test` fire. Poll `GET /v1/kyc/status` for anything else                                             |
| `Transfer` confirm path                         | **Yes, if your pool is funded**                 | Ask us to fund the preprod pool in Slack first                                                                                                       |
| `Transfer` rejection path                       | **No**                                          | There is no fixture, and `rejection_reason` values are not published. → [Move cash](/funding#move-cash)                                              |
| Order accepted                                  | **Yes**                                         | `CreateVendorOrder` returns `ACCEPTED` for a durably accepted order, **including one cancelled under fill-or-kill**                                  |
| Order `EXPIRED`                                 | **Yes, trivially**                              | Any FOK order into an empty book. This is the default preprod outcome                                                                                |
| **A fill**                                      | **Not on demand**                               | Self-seed depth from a second session, below                                                                                                         |
| Order `PENDING`                                 | **No**                                          | `PENDING` means we could not confirm an outcome. There is no fixture and no published deadline value                                                 |
| Drop-copy execution report                      | **Only after a fill**                           | Blocked behind the fill problem                                                                                                                      |
| Position change event                           | **Only after a fill**                           | Blocked behind the fill problem                                                                                                                      |
| Balance ledger stream                           | **Yes**                                         | Replay then live phases. It is per account and counts against the 20-streams-per-firm cap                                                            |
| Instrument state transitions                    | **Yes, but only to `INSTRUMENT_STATE_EXPIRED`** | Preprod expires instruments in batches of 363, 455, 544 and 549 in a single session                                                                  |
| **Settlement values**                           | **No**                                          | Markets do not resolve in preprod. Request a resolution, below                                                                                       |
| A `RESOLUTION` ledger entry                     | **No**                                          | Same cause                                                                                                                                           |
| Scale handling across `priceScale` 100 and 1000 | **Partly**                                      | Only if preprod serves you an instrument of each. Read both scales per instrument regardless                                                         |

<Warning>
  **The KYC magic-value pattern exists and works, but the published DOB fixture table
  does not.**

  Magic DOBs are real, and the table of them has drifted and returned wrong outcomes at least twice.
  Ask your integration lead for a current fixture set instead of coding against a published DOB.
</Warning>

### The fixtures that do not exist

Say this plainly, because four partners have each built a plan around one of them: **the KYC magic-value pattern has no equivalent for orders, transfers, market state or settlement.** There is no way to ask the platform for a fill, a rejection, a state transition or a resolution.

**Four partners have asked for an on-demand "resolve this market" facility by name.** It does not exist. One asked six times over 40 days and never got a settlement test.

<Warning>
  **Deterministic fixtures for orders, transfers, market state and resolution are not yet published.**

  Do not plan a certification date against any of these four. Ask your integration lead for status
  before you commit one to a client.
</Warning>

### Workarounds that actually work

#### Self-seed depth from a second preprod session

Because you are always the taker, the only way to get a fill is for depth to exist at your price. You can create that depth yourself: run a second preprod session that **rests** an order through the legacy `OrderEntryAPI/insertOrder`, then take it with `CreateVendorOrder` from your integration session.

<Warning>
  **This workaround and UAT §0 contradict each other.**&#x20;
  UAT §0 asks you to disavow legacy order entry, and this is legacy order entry. A partner cannot
  resolve that alone. Get it in writing from your integration lead before you rely on it, and see
  [Certification](/certification), where the same issue is flagged.
</Warning>

`OrderEntryAPI` is exactly `CreateOrderSubscription`, `InsertOrder` and `CancelOrder`. Day orders cancel at the traded-day roll as of 2026-09-13 (v0.0.89), so a seeded resting order does not survive the roll; use GTD or re-seed each day.

#### Ask for a resolution on a market you already hold

Take a position first, then ask in Slack for that specific symbol to be resolved. Asking for "a market that resolves" produces nothing, because none do. Do not pick an `atpcq` symbol — `atpcq` is absent from the preprod resolution config.

#### Ask for test funds in one request

Preprod pools are funded manually by us on a Slack request. Ask once, for the whole test plan.

### Test data rules that will bite you

**One SSN maps to exactly one exchange account, platform-wide**, because the trading account ID is derived from it. Reusing a test SSN across test users points all of them at a single account, which breaks transfers with `NOT_FOUND` and requires manual database cleanup by us. That failure produced a 66-reply incident thread. Generate a distinct SSN per test user and keep a registry of which ones you have burned, because **there is no re-KYC, reset or PII-update path** — a user whose SSN is already in use cannot be re-KYC'd by you.

Other constraints your generator must respect:

* `external_id` is **max 49 characters**.
* `postal_code` must be **five digits**.
* Requests are `snake_case`, REST responses are `camelCase`, webhooks are `snake_case`. A test that asserts one casing across all three will fail on two of them.
* Set `docv_eligible: true` unless you are deliberately testing the slow path. Without it, REVIEW cases go to **manual review, 1–2 business days**, which will stall a test run for two days.
* Treat the Digital Intelligence `session_token` as required even though it is optional on paper. It is what keeps the REVIEW rate down.

**Wait for the `kyc.approved` webhook before you enable trading for a test user.** On an `ACCEPT`, `participantId` can come back empty while provisioning finishes, with observed gaps from \~500 ms up to 26 minutes. A harness that reads `participantId` straight off the `ACCEPT` response will pass locally and fail in CI. → [Firms, participants and accounts](/identity)

### Assertions worth building into the harness

* Every order response asserts on `status` being one of exactly `ACCEPTED`, `REJECTED`, `PENDING`. An exchange-level rejection arrives as gRPC `OK` with `status = REJECTED`, so a test that only checks the gRPC status code will pass on a rejected order.
* Every de-scaling assertion reads `priceScale` and `fractionalQtyScale` off the instrument in the same test, and asserts the divisor is their product. A hard-coded 10,000 passes on most instruments and is wrong by 10× on the rest.
* Every fee assertion computes `Fee = Θ × C × p × (1 − p)` with `C` as the **de-scaled** contract count. Using raw `order_qty` on a `fractional_quantity_scale = 100` instrument overstates the fee 100×, and instruments with scale `1` make the naive math accidentally correct, so this bug hides until your first scale-100 fill.
* Every stream test counts open streams against the **20 concurrent streams per firm** cap, across all gRPC subscriptions, and asserts you are under it before opening another.

## What can go wrong

| Part             | Symptom                                                                 | Cause                                                                                   | What you do                                                                                         |
| ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Preprod fidelity | Every order comes back `EXPIRED`                                        | FOK never crossed because there was no resting depth to take                            | Expected in preprod. Check the book first, or self-seed depth. → [Test harness](#test-harness)      |
| Preprod fidelity | The book reads `0` on both sides for minutes                            | `pmsim` coverage is intermittent on that symbol                                         | Pick a symbol with LP presence; `astatc-` has the best coverage at \~48%                            |
| Preprod fidelity | A market you hold expires and never pays                                | Preprod markets do not resolve; `INSTRUMENT_STATE_EXPIRED` is the terminal state there  | Request a resolution on a specific market in Slack. Do not code a timeout around waiting            |
| Preprod fidelity | Yesterday's executions return empty                                     | An overnight maintenance window archived stored execution history                       | Reconcile against your own persisted drop-copy record                                               |
| Preprod fidelity | `heap_pressure` on a market-data snapshot                               | Our admission guard, wrong in every environment                                         | Retry, and report the verbatim string                                                               |
| Preprod fidelity | Preprod credentials fail in production                                  | Keys are per environment                                                                | Generate a production keypair and send us the public half                                           |
| Preprod fidelity | Preprod works, production behaves differently on the same instrument    | A non-parity item: scales, tennis reference fields, `account_beneficial_owner`, `atpcq` | Re-verify against the non-parity list above before go-live                                          |
| Test harness     | The e2e test in the examples repo passes for us and not for you         | Different proto build, or a missing scope grant                                         | Pin your proto bundle; re-mint your token after we add a grant                                      |
| Test harness     | A KYC fixture returns the wrong outcome                                 | The published DOB table has drifted                                                     | Use the email, name and phone fixtures above; ask for a current DOB set                             |
| Test harness     | Every test user ends up on the same account                             | You reused a test SSN                                                                   | Distinct SSN per user. Cleanup needs us; there is no partner-side reset                             |
| Test harness     | `NOT_FOUND: customer relationship claim failed` on a transfer in a test | A shared test SSN, an incomplete server-side setup step, or a half-provisioned firm     | Check the SSN registry first, then raise it in Slack. There is no claim or link RPC for you to call |
| Test harness     | Your harness reads an empty `participantId` and proceeds                | You read it off the `ACCEPT` instead of waiting for the webhook                         | Gate on `kyc.approved`; allow for 26 minutes                                                        |
| Test harness     | A seeded resting order is gone the next morning                         | Day orders cancel at the traded-day roll as of v0.0.89                                  | Re-seed per day, or use GTD                                                                         |

## Next

[Certification](/certification)
