Skip to main content
The Request for Quote (RFQ) Trades Partner API lets your platform settle trades that two counterparties negotiated off Polymarket. Each side is a standard Polymarket exchange order signed for that counterparty, your platform submits the signed pair in one request, and Polymarket executes it on Polygon. The orders never rest on the order book. See the Institutional Overview for how RFQ trades relate to the order book.
Access is by invitation. Polymarket issues your API key during partner onboarding. To request access, email institutional@polymarket.com.

How an RFQ Trade Works

Your platform authenticates every request with its partner API key. Each counterparty authorizes only its own side: it gives your platform a session key for its Polymarket Deposit Wallet, and your platform signs that counterparty’s orders with it.
  1. Find a market through the public Gamma API.
  2. Validate the terms (optional). One request runs the market, price, account, and guardrail checks and reports all of them.
  3. Prepare the orders (optional). Polymarket builds both unsigned orders, their signing payloads, and the fee each side pays.
  4. Sign the orders. Your platform signs each order with its counterparty’s session key.
  5. Submit the trade. This is the only request that creates a trade.
  6. Track the trade until it reaches FILLED or REJECTED, by polling or over the WebSocket stream.

Operations

Every RFQ trade is one of three operations, determined by the sides of the two orders. The collateral is pUSD, Polymarket’s USDC-backed ERC-20 token on Polygon. Requests and responses key each side by these party keys. For example, the fees returned for a split are keyed yes and no.

Base URL

Conventions

  • Amounts (shares, makerAmount, takerAmount, fees) are base-10 integer strings in 6-decimal units. "1000000" is one share or one pUSD.
  • Prices and tolerances are decimal strings, such as "0.65".
  • Times are RFC 3339 UTC strings, except order timestamps, which are Unix seconds.
  • Request bodies are decoded strictly. An unknown field returns 400.
  • Every error from the API uses one envelope. See Errors.
Every request and response is specified in the API Reference.

Authentication

Every request is signed with your partner API key. Polymarket identifies your platform from the key and never from the request body, so a request can only read and create your own trades. The key does not authorize moving funds: each counterparty authorizes its own side through its order signature. Every request except GET /v1/time carries three headers: Create a new timestamp for each request, within 30 seconds of server time. Then sign the timestamp, the uppercase HTTP method, the path, and the exact serialized body you send:
For a GET, the body is empty. The examples on this page use these placeholders. See Authentication for TypeScript and Python signing helpers, a test vector, and the WebSocket handshake.

Find Markets

Find markets through the public Gamma API at https://gamma-api.polymarket.com. It needs no API key. To list, page, and search events and fetch a market, see Discover Markets. Discovery data does not need to be perfectly fresh: Polymarket re-checks the market and its on-chain state on every RFQ trade request.

Check That a Market Is Tradable

Trade only markets where all of these hold:
Any other umaResolutionStatus, such as proposed, disputed, or resolved, means a CTF market has entered resolution and its outcome may already be known. Skip it even though the other flags can still be true at that point. Polymarket Protocol V2 markets don’t set umaResolutionStatus.

Read the Trading Identity

An RFQ trade request references the market and its outcome tokens with these fields: Index 0 of positionIds or clobTokenIds is always the yes side and index 1 the no side, in outcomes order, even when the outcomes are names such as ["Lakers", "Celtics"]. clobTokenIds, outcomes, and outcomePrices are JSON-encoded strings, such as "[\"1234…\", \"5678…\"]". Parse them before use. orderPriceMinTickSize and orderMinSize are order book conventions. RFQ trades never enter the book, so they are not enforced, but staying within them is recommended. An RFQ trade price can have at most 6 decimals.

Price a Trade

The public CLOB API at https://clob.polymarket.com gives the current order book, prices, and midpoints for quoting, by outcome token ID. See Market Data. Every RFQ trade price must sit inside a price band around the CLOB midpoint. Read the current band for both outcome tokens before you quote:
Sign the request with the method GET, the path /v1/markets/253591/price-band, and an empty body. The response body gives the band of each outcome token at the default tolerance:
mark is the token’s CLOB midpoint, and a price on that token must sit between lower and upper. See Price Band for how the band is computed.

Settle a Trade

Once two counterparties agree on a market, an operation, a price, and a size, settle the trade with the requests below. Only the submission creates a trade. Every request carries the three authentication headers.
1

Describe the Terms

First, describe the agreed trade as terms. Validate and prepare both take the terms as their request body:
Where:
  • marketId is the Gamma market id.
  • operation is split, merge, or trade. See Operations.
  • price is the YES price for a split or merge, and the traded token’s price for a trade.
  • shares is the number of shares on each side, in 6-decimal units. "1500000000" is 1,500 shares.
  • parties holds each counterparty’s Deposit Wallet address as maker, with signatureType 3.
Each party describes one counterparty’s account:
2

Validate the Terms

Next, optionally check the terms before anyone signs. Validate runs the market, price, account, and guardrail checks a submission would run, reports all of them, and stores nothing:
Sign the request with the method POST, the path /v1/rfq-trades/validate, and the exact body you send.A completed validation returns 200, even when a check fails. A 4xx means the request itself was malformed. The response body reports every check:
Where:
  • valid is true when the market, both parties, and every guardrail pass, and the price check doesn’t fail. parties is keyed like the request.
  • A check that could not run reports "status": "missing". It is not a pass. A missing price check alone leaves valid unchanged, because the price_deviation guardrail decides whether a trade without a midpoint is blocked.
  • When the market check fails because the market can’t be found, the other checks are left out of checks.
  • price.band is the band of the first order’s token: YES for split and merge, the traded token for trade. Read both tokens’ bands from the price band request.
  • guardrails lists every guardrail with its result.
  • Each reason is listed under Rejection Reasons.
The result is a point-in-time preview. Balances and bands keep moving, and the same checks run again when the trade is submitted. Signatures and session keys are checked only on submission.
3

Prepare the Orders

Then, send the same terms to prepare. Polymarket builds both unsigned orders, with fresh salts and timestamps, and the fee each side will pay:
Sign the request with the method POST, the path /v1/rfq-trades/prepare, and the exact body you send.The response body holds one prepared order per party, keyed like parties. This excerpt shows the yes order. orders.no has the same fields for the NO side:
Where:
  • order is the unsigned order. Submit it unchanged.
  • typedData, digest, and signatureSuffix are what the counterparty’s session key signs in the next step.
  • role is the fee role the order was quoted under. Send it back with the order on submission.
  • fees is the fee each side will pay, in pUSD base units.
  • After expiresAt, about 10 minutes after the order timestamp, a submission of these orders is refused as stale_order. Prepare again and have both sides re-sign.
Prepare also stamps the builder code Polymarket configured for your platform on both orders. It refuses terms that fail the market, price band, or guardrail checks with 422. It doesn’t check accounts or funds, so the counterparties don’t need to be funded yet. Each call returns new orders, so prepare again rather than retrying with the old ones.
4

Sign the Orders

Each side of the trade is authorized by its own counterparty, not by your platform. Sign each prepared order with the session key its counterparty gave you:
  1. Sign the order’s typedData exactly as returned, with eth_signTypedData_v4 or an equivalent library call. The result is a 65-byte ECDSA signature that recovers to the session key over the returned digest.
  2. Append the returned signatureSuffix to the 65-byte signature.
  3. ABI-encode the session key’s address (as bytes32), 32 zero bytes, and the signature from the previous step (as bytes), then append the 32-byte marker 0x6492…6492 (6492 repeated 16 times). This envelope tells the Deposit Wallet which session key signed.
The result is the order’s signature. A signature that skips the suffix or the envelope, or that signs the plain Order instead of typedData, is refused as invalid_order. See Signing Details for the wrapper and its domains.
5

Submit the Trade

Next, submit both signed orders. Each order is the prepared order, unchanged, plus its signature and role. Set idempotencyKey to your own reference for the trade:
Sign the request with the method POST, the path /v1/rfq-trades, and the exact body you send.Where:
  • idempotencyKey is required: your own reference for the trade, up to 128 bytes. See Idempotency.
  • orders holds exactly two signed orders, in any order. The operation and each side are derived from the orders’ sides and token IDs.
  • priceTolerance is optional: the same value you validated and prepared with.
Do not send fee amounts. Polymarket computes the fees, and a non-zero takerFeeAmount or makerFeeAmount is refused as fee_not_accepted.
6

Check the Response

Then, check the HTTP status. A 201 Created response means the trade was accepted as PENDING. A 422 response means the trade was refused, and no trade was created:
Keep the tradeId to track the trade. On a failure, details.reason is one of the rejection reasons. A 201 is not a fill: some checks run again just before execution. See Acceptance Is Not Execution.
7

Track the Trade

Finally, read the trade by its tradeId until it is final:
Sign the request with the method GET, the path /v1/rfq-trades/6f1c2b8e-3d4a-4f5b-9c6d-7e8f9a0b1c2d, and an empty body.The response body is the trade’s current status, and the source of truth for its outcome:
status is one of:On REJECTED, failureReason is one of the rejection reasons, or an execution error code such as execution_failed when the trade could not execute. A trade ID that is unknown, or belongs to another partner, returns 404. To avoid polling, open the trade status stream.

Session Keys

A session key is an ECDSA key pair that a counterparty authorizes for its Polymarket Deposit Wallet. The counterparty creates it on Polymarket with the EXTERNAL_RFQ_INTEGRATION scope and shares it with your platform, together with its Deposit Wallet address, which is the maker of its orders. Your platform then signs RFQ trades on the counterparty’s behalf. The key is shown only once: to replace a lost key, the counterparty deletes it and creates a new one. See Session Keys for how authorization works.
  • Scoped to RFQ trades. A key with only the EXTERNAL_RFQ_INTEGRATION scope cannot place CLOB orders, deposit, or withdraw. A signature from a key that isn’t authorized for RFQ trades is refused as invalid_order.
  • Revocable. The counterparty can revoke the key at any time. A signature from a revoked or expired key is refused as invalid_order.
  • Invisible in the order. Both maker and signer are the Deposit Wallet address. The session key appears only inside the signature.

Signing Details

Deposit wallets verify signatures through ERC-1271 and expect Solady’s ERC-7739 TypedDataSign wrapper: the order is bound to the wallet’s own domain, nested under the exchange’s domain. A plain order signature fails the wallet’s isValidSignature check.
The market’s version selects the outer domain, and for a CTF market its negRisk flag selects the verifying contract. Prepare applies both, which is why signing the returned typedData is the reliable path.

Trade Submission

Acceptance Is Not Execution

A 201 means the trade was accepted for execution, not that it will fill.
  • Checked at submission. The request shape, the order rules and timestamps, the market’s status, and the size guardrails. A failure returns 422 and creates no trade.
  • Checked just before execution. Signatures, accounts and funds, the price band, the price guardrail, and a fresh market check. A failure ends the trade as REJECTED with a failureReason.
Follow every trade to FILLED or REJECTED. To learn the verdict before anyone signs, validate first. Trades that share a counterparty wallet execute one at a time, so a trade can wait while an earlier trade on the same wallet settles. A trade that still can’t execute about 10 minutes after submission, because it is queued or a dependency is down, ends REJECTED with execution_failed.

Idempotency

idempotencyKey is scoped to your platform, across all of its API keys. Submitting a key you have already used returns 201 for the original trade, with its current status and transactionHash, and the header Idempotency-Replayed: true. This makes a submission safe to retry after a timeout or a retryable error. Only the key identifies the trade, and the body is not compared on replay, so never reuse a key for a different trade. A submission refused with 422 creates no trade, so its key stays free. A trade that ends REJECTED keeps its key.

Order Rules

Prepared orders already follow these rules. If you build orders yourself instead of preparing them, follow them too. A broken rule is refused as invalid_order unless noted:
  • The two orders belong to different accounts.
  • maker and signer are both the Deposit Wallet address, and signatureType is 3.
  • salt is a positive base-10 integer, used once per maker. A trade rejected by the checks just before execution frees its salts again. See Rejection Reasons.
  • timestamp is in Unix seconds. An order more than 10 minutes old is refused as stale_order, and one more than 1 minute in the future as invalid_order.
  • metadata is zero, and makerAmount and takerAmount are positive.
  • builder is your builder code, or zero if you don’t have one.
  • makerAmount is what the order gives and takerAmount what it receives: a buy gives pUSD for shares, and a sell gives shares for pUSD. A buy’s price is makerAmount / takerAmount, and a sell’s is takerAmount / makerAmount.
  • For split, both orders buy the same number of shares, and their pUSD together covers at least those shares. For merge, both orders sell the same number of shares, and together ask for at most that much pUSD.
  • For trade, both orders carry the same token and are exact mirror images: the same shares and the same pUSD amount, at a price of at most 1. A pair that merely crosses is refused rather than settled at one side’s price.
For the order fields themselves, see Place Orders.

Trade Status Stream

To avoid polling, open the WebSocket at wss://external-rfq-partner.polymarket.com/v1/ws/rfq-trades, authenticated on the upgrade request. On connect it sends one trade_update per trade still PENDING, then one each time a trade moves, such as when it is accepted, mined, filled, or rejected. Each frame carries the same object as the GET:
A frame can repeat a status you have already seen, and a trade’s first frame can arrive before the submission’s response. Match frames to your trades by tradeId or idempotencyKey. The stream is a latency optimization, not a ledger: delivery is lossy by design. A connection that falls behind drops frames without being closed, and a trade that finished while you were disconnected is not sent again. GET any trade that has stayed PENDING longer than you expect, and every trade you still hold as PENDING after a reconnect. For any trade whose outcome matters, the GET is the source of truth. The server pings every 45 seconds and closes a connection that hasn’t answered within 60 seconds.

Fees

Email blocks@polymarket.com for details on RFQ trade fees.

Checks and Limits

Every RFQ trade must pass the same checks: the market, the price, each counterparty’s account, and a set of guardrails. Validate reports them without refusing anything, while prepare and submit refuse a trade that fails one.

Market

The market must be tradable and at least 30 seconds from its end time. Otherwise the trade is refused as market_not_tradable.

Price Band

Each order’s price must sit inside a band around the CLOB midpoint, its mark, of the outcome token that order prices:
T is 0.5 by default. Pass priceTolerance to tighten it, such as "0.05" for ±5% of the mark. A tolerance wider than the default is refused. Out of band, the trade is refused as price_out_of_band. Both orders are checked, each against its own token. For a split at 0.65, the YES order is judged at 0.65 against the YES mark and the NO order at 0.35 against the NO mark. Because the band is relative, it is narrowest on the cheaper token, so price your quotes against both bands. Price a Trade shows how to read both tokens’ current bands. When no midpoint can be read, the band’s source is unavailable and its mark, lower, and upper are omitted. The band check then reports missing and does not block the trade, but a market that has no order book is refused as price_mark_missing. If the midpoint can’t be read because the CLOB is unreachable, validate and prepare return 502, and a submitted trade waits until it can be read.

Accounts and Funds

Each counterparty’s account must:
  • Be a registered Polymarket account (unknown_account).
  • Not be restricted from trading (account_restricted). An account limited to closing positions can sell but not buy.
  • Pass sanctions screening, for both the account and the signing key (ofac_blocked).
  • Hold the funds for its side (insufficient_balance): pUSD for its makerAmount plus its fee for a buy, or the outcome tokens it sells for a sell.
  • Have approved the exchange contracts to spend those funds. On a CTF market the spender is the Standard Exchange, or the Neg Risk Exchange and Neg Risk Adapter for a negRisk market. On a Polymarket V2 market it is the Polymarket V2 Exchange, for both pUSD and PositionManager outcome tokens. See Set Up Trading Approvals for the approvals and contract addresses.
Only a buyer’s pUSD approval on a CTF market is checked ahead of execution (insufficient_allowance). A seller’s missing outcome token approval, and any funding problem on a Polymarket V2 market, fail the trade at execution with an execution error such as execution_failed, so confirm both sides’ approvals before you submit.

Guardrails

Every RFQ trade is also checked against guardrails, such as limits on its size and on how far its price sits from the midpoint. Email blocks@polymarket.com for details on the guardrails that apply to your platform.

Errors

Every error uses one envelope:
  • code is the error class. Several HTTP statuses share one, as the table below shows.
  • message is human-readable, for logs.
  • retryable is true when the same request may be sent again as is. For a submission, retry with the same idempotencyKey.
  • details.reason is the machine-readable rejection reason, when the request was rejected.
  • details.party is the side at fault, by fee role (taker or maker), and details.field the request field at fault, such as orders[0].salt, when there is one.
A method and path pair that is not one of the API’s requests is refused by Polymarket’s edge network before it reaches the API, with 404 and a plain {"error": "not found"} body instead of the envelope.

Rejection Reasons

The same reasons appear in details.reason, in validate’s checks, and in a rejected trade’s failureReason. New reasons may be added, so treat an unknown one as a rejection. After a 422, fix the request and submit again: no trade was created, so the same idempotencyKey still works. After a REJECTED trade, submit with a new idempotencyKey: reusing the rejected trade’s key returns that trade. A trade rejected by the checks just before execution frees its orders’ salts, so once the cause is fixed, such as a counterparty funding its account, the same signed orders can be submitted again until they are 10 minutes old. Otherwise, prepare again and have both sides re-sign. Until signed orders go stale, treat them as live even after their trade is rejected. A rejected trade’s failureReason can also be execution_failed, or another execution error code, when the trade could not execute: it waited about 10 minutes without executing, for example because its funds couldn’t be held or a dependency was down, or the exchange refused the matched orders. Prepare again before retrying.

API Reference

Each request and response is generated from the API specification in the Institutional API reference: