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.- Find a market through the public Gamma API.
- Validate the terms (optional). One request runs the market, price, account, and guardrail checks and reports all of them.
- Prepare the orders (optional). Polymarket builds both unsigned orders, their signing payloads, and the fee each side pays.
- Sign the orders. Your platform signs each order with its counterparty’s session key.
- Submit the trade. This is the only request that creates a trade.
- Track the trade until it reaches
FILLEDorREJECTED, 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.
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 exceptGET /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:
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 athttps://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: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 athttps://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:
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:
marketIdis the Gamma marketid.operationissplit,merge, ortrade. See Operations.priceis the YES price for asplitormerge, and the traded token’s price for atrade.sharesis the number of shares on each side, in 6-decimal units."1500000000"is 1,500 shares.partiesholds each counterparty’s Deposit Wallet address asmaker, withsignatureType3.
All Term Fields
All Term Fields
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 Where:
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:validistruewhen the market, both parties, and every guardrail pass, and the price check doesn’t fail.partiesis keyed like the request.- A check that could not run reports
"status": "missing". It is not a pass. Amissingprice check alone leavesvalidunchanged, because theprice_deviationguardrail 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.bandis the band of the first order’s token: YES forsplitandmerge, the traded token fortrade. Read both tokens’ bands from the price band request.guardrailslists every guardrail with its result.- Each
reasonis listed under Rejection Reasons.
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 Where:
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:orderis the unsigned order. Submit it unchanged.typedData,digest, andsignatureSuffixare what the counterparty’s session key signs in the next step.roleis the fee role the order was quoted under. Send it back with the order on submission.feesis 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 asstale_order. Prepare again and have both sides re-sign.
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:The result is the order’s
- Sign the order’s
typedDataexactly as returned, witheth_signTypedData_v4or an equivalent library call. The result is a 65-byte ECDSA signature that recovers to the session key over the returneddigest. - Append the returned
signatureSuffixto the 65-byte signature. - ABI-encode the session key’s address (as
bytes32), 32 zero bytes, and the signature from the previous step (asbytes), then append the 32-byte marker0x6492…6492(6492repeated 16 times). This envelope tells the Deposit Wallet which session key signed.
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 Sign the request with the method
order,
unchanged, plus its signature and role. Set idempotencyKey to your
own reference for the trade:POST, the path /v1/rfq-trades, and
the exact body you send.Where:idempotencyKeyis required: your own reference for the trade, up to 128 bytes. See Idempotency.ordersholds exactly two signed orders, in any order. The operation and each side are derived from the orders’ sides and token IDs.priceToleranceis optional: the same value you validated and prepared with.
takerFeeAmount or makerFeeAmount is refused as fee_not_accepted.6
Check the Response
Then, check the HTTP status. A Keep the
201 Created response means the trade was
accepted as PENDING. A 422 response means the trade was refused, and
no trade was created: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 Sign the request with the method
tradeId until it is final: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 theEXTERNAL_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_INTEGRATIONscope cannot place CLOB orders, deposit, or withdraw. A signature from a key that isn’t authorized for RFQ trades is refused asinvalid_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
makerandsignerare 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-7739TypedDataSign 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 wrapper and its domains
The wrapper and its domains
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
A201 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
422and 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
REJECTEDwith afailureReason.
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 asinvalid_order unless noted:
- The two orders belong to different accounts.
makerandsignerare both the Deposit Wallet address, andsignatureTypeis3.saltis 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.timestampis in Unix seconds. An order more than 10 minutes old is refused asstale_order, and one more than 1 minute in the future asinvalid_order.metadatais zero, andmakerAmountandtakerAmountare positive.builderis your builder code, or zero if you don’t have one.makerAmountis what the order gives andtakerAmountwhat it receives: a buy gives pUSD for shares, and a sell gives shares for pUSD. A buy’s price ismakerAmount / takerAmount, and a sell’s istakerAmount / makerAmount.- For
split, both orders buy the same number of shares, and their pUSD together covers at least those shares. Formerge, 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.
Trade Status Stream
To avoid polling, open the WebSocket atwss://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:
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 asmarket_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 itsmakerAmountplus 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
negRiskmarket. 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.
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:codeis the error class. Several HTTP statuses share one, as the table below shows.messageis human-readable, for logs.retryableistruewhen the same request may be sent again as is. For a submission, retry with the sameidempotencyKey.details.reasonis the machine-readable rejection reason, when the request was rejected.details.partyis the side at fault, by fee role (takerormaker), anddetails.fieldthe request field at fault, such asorders[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 indetails.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.