Skip to main content
Decide what actually happened to an order you submitted, resolve one whose outcome you never learned without doubling a customer’s position, and see why a fill-or-kill surface has nothing to cancel.
Before this page:
  • you can place an order, and you know that CreateVendorOrder accepts TIME_IN_FORCE_FILL_OR_KILL and nothing else — see Place an order;
  • you know which stream carries fills: drop copy is the source of record for fills and commissions — see Drop copy.
API reference: Trading. Opens on the public documentation site in a new tab. Where it disagrees with this page, this page is authoritative for the partner surface.

Order outcomes

If you are an IB, this differs. The six-value OrderState enum is the exchange’s, not yours. A fill-or-kill partner order will only ever reach you as ACCEPTED, REJECTED, PENDING or EXPIRED. Say that plainly in your procedures so a reviewer does not conclude you can work or amend an order.
Tell a filled fill-or-kill apart from one that was killed. The most common outcome of a partner order is EXPIRED, it is not a rejection, and it has never been documented. That is the single largest functional gap in the order docs we are closing: the outcome you will see most often was the one nobody had written down.

The RPC result: three values

VendorOrderStatus has exactly three values. This is the status on the CreateVendorOrder response, and it is about the call, not about the fill.

ACCEPTED is not a fill

An order that reached the book, found nothing to cross, and was cancelled under fill-or-kill comes back ACCEPTED. If you credit a user’s position on ACCEPTED, you will credit positions that do not exist, on the majority of orders. Credit on a drop-copy execution report and nothing else.

REJECTED arrives as gRPC OK

An exchange-level rejection is a successful RPC carrying status = REJECTED. It is not a gRPC error status. A client that only inspects the gRPC status code — try/except grpc.RpcError, or if err != nil — treats every exchange rejection as a success. Branch on resp.status on every call, including the ones that return without error.

PENDING is indeterminate, not queued

PENDING does not mean “we have your order and will work it”. It means we do not know whether your order was placed. Both outcomes are still possible from that state. Treating PENDING as “not placed” and re-placing with a fresh key is how a partner doubles a customer’s position. The recovery procedure is one rule and it is under Recover a lost order.

The exchange order state

A separate enum, OrderState, describes the order at the exchange: There is no PENDING order state. PENDING exists only on VendorOrderStatus, describing the call. Any code path that looks for an OrderState of PENDING will never match. NEW means accepted and resting — not “newly received and unprocessed”. Older order-management documentation defined it backwards and invented a PENDING state alongside it. On the FOK-only partner surface nothing rests, so NEW is not a state your orders sit in.
Not yet published. Whether an FOK order on this surface can partially fill, or is strictly all-or-none, is not published. PARTIALLY_FILLED(2) is in the enum. Handle it — credit exactly the quantity on the execution report rather than the quantity you submitted — and confirm the semantics with your integration lead.

EXPIRED is a fill-or-kill that never crossed

This is the section partners have been asking for. EXPIRED is a fill-or-kill order that never crossed. It is not a rejection, and it carries no reason by design. Nothing was wrong with your order. There was no resting depth at your limit price at the moment it arrived, so the exchange killed it, which is exactly what fill-or-kill instructs it to do. One partner reported it this way:
failed orders rarely return a reason — ~99% just show EXPIRED
That is expected behaviour, and the report is an expectation mismatch rather than a defect. The partner was reading EXPIRED as a class of failed order and looking for the missing diagnostic field. There is no missing field. An EXPIRED order has nothing to explain: it was not refused, it was not malformed, and it was not unfunded. It did not cross. Three consequences for your integration:
  1. Do not alert on EXPIRED. At preprod book depth it is the normal outcome, and paging on it buries the rejections that do matter.
  2. Do not surface EXPIRED to a user as an error. “No liquidity at your price” is what happened.
  3. Do not retry it on a tight loop at the same price. The book has not changed in the microsecond since. Re-price, or wait for a market-data update, and resubmit with a new idempotency_key — a re-price is a new order, not a recovery.
If you are seeing EXPIRED on nearly every order in preprod, that is the environment, not your code. You are always the taker has the depth numbers.

Telling a filled FOK from a killed one

A new idempotency_key is a new order, so mint one only against a terminal outcome you have actually observed. The full rule, and what to do when you cannot tell which outcome you are holding, is in Recover a lost order. failure_reason carries exchange-authored text only. It is not a stable enum, it is not localized, and it is not a code you can branch on. Store it, quote it in support requests, and drive your logic off status and OrderState instead.
Not yet published. [GAP] There is no status-lookup RPC on the partner surface — you cannot ask “what happened to clord_id X?”. SearchOrders appears in the published rate-limit table at 12 requests/min, but whether it is entitled for ISVs, and which message carries the terminal OrderState, are not published. Confirm both before you design your order state machine, because the alternative is inferring terminal states from the absence of a fill.

Recover a lost order

Resolve an order whose outcome you never learned, without placing it twice. PENDING means the order’s state is unknown to us, not that it was not placed. Every rule in this section exists to stop you turning one uncertain order into two real ones.

The rule

Resubmit CreateVendorOrder with the same idempotency_key and a byte-identical body.
  • ALREADY_EXISTS is the guard working, not an error. You get it when the body differs from the first submission under that key — even by one field. Fix your body to match the original bytes; do not change the key.
  • Never mint a new idempotency_key. A new key is a fresh placement attempt. If the first order did reach the book, a new key gives your user two positions and two prefunded transfers.
  • Only mint a new key against a terminal outcome you have actually observed. A rejection or an EXPIRED is terminal, so re-pricing and resubmitting under a fresh key is a genuinely new order and is safe. A PENDING is not terminal — it means we could not confirm an outcome — and resubmitting that under a fresh key places a second order and takes a second position. When you are unsure which of the two you are holding, treat it as PENDING and follow this section, which is safe to run repeatedly.
Three corollaries: Serialize once, store the bytes, replay the bytes. Rebuilding the request from your own order row re-introduces every difference that trips ALREADY_EXISTS — a regenerated clord_id, a re-rounded price, a field your library now populates by default. Persist the serialized request next to the idempotency_key at first submission and send those exact bytes on every retry. If your stack re-serializes, use deterministic serialization and touch no field. funding_request_ids are values we return, not values you send. They appear on the CreateVendorOrder response. Echoing them back on a retry changes the body and earns you ALREADY_EXISTS. The dedupe key is idempotency_key and nothing else. A re-price is not a recovery. If you decide to try again at a different price or size, that is a new order and it takes a new idempotency_key. Recovery replays; re-pricing does not.

When you never receive an execution report

A missing execution report is more often a lost stream than a lost order. Work in this order.
1

Resume drop copy from your stored resume_token

Drop copy is the source of record for fills and commissions and it is resumable — resume_token is populated, at roughly 576 bytes. Resume from the last token you persisted before you conclude anything about the order. A reconnect that restarts from live loses every report in the gap.
2

Look for an ORDER_EXECUTION entry on the balance ledger

Executions fold commission into a single net ORDER_EXECUTION entry. One entry against the participant account for your clord_id is proof the order crossed, whatever your own state machine says.
3

Check positions for a delta

A position change on the account is also positive proof. Note the balance-ledger stream is per-account and counts against the 20 concurrent streams per firm cap, so do not open one per participant to answer this question.
4

Resubmit under the same idempotency_key with the original bytes

This is the only sanctioned way to resolve a PENDING. Run the snippet below. It is safe to run repeatedly, because the key and the bytes do not change between runs.
5

Escalate with the identifiers, not a description

If you still cannot determine the outcome, post the idempotency_key, the clord_id, the order.account, the environment and the verbatim error including the gRPC status code.
Not yet published. [GAP] No PENDING deadline is published, so there is no sanctioned interval to wait before resubmitting, and we have not published whether a resubmission returns the stored terminal outcome or makes a fresh attempt under the same key. Agree both with your integration lead before you automate this, and until then keep recovery operator-triggered rather than on a timer.
A terminally-rejected idempotency_key replays its stored rejection on CashMovementService — after a terminal reject there, an identical retry will not make a fresh attempt and you must use a new key. That rule is documented for cash movements under Move cash.Whether CreateVendorOrder behaves the same way is not published — do not assume it does.

What the platform does not do for you

[GAP] There is no automatic retry of a PENDING order. Nothing on our side re-drives it. If you do not resubmit, the outcome stays unresolved. [GAP] There is no status-lookup RPC on the partner surface. You cannot query an order by clord_id or by idempotency_key. Resubmission under the same key is the substitute for a lookup, which is why the stored bytes matter so much. [GAP] No PENDING deadline is published, so there is no point at which you may safely conclude an order is dead.

Safe recovery

All four snippets read a stored (idempotency_key, serialized request) pair written at first submission and resend it. Run them as many times as you like: the key and the bytes are identical every time, so they can resolve the order but can never place a second one.
The generated message and stub names below assume you ran protoc over polymarket-protos.zip.

Cancelling

Understand why your normal order flow contains no cancel call, and what to do instead.

There is normally nothing to cancel

Your orders never rest. CreateVendorOrder accepts fill-or-kill only, so an order either crosses on arrival or the exchange kills it, and it reaches a terminal state within the same call. By the time you could issue a cancel, the order is already FILLED(3) or EXPIRED(9). That is the answer to the question most readers arrive with: there is no open-order book of yours to manage, no cancel-on-disconnect to configure, and no stale orders to reap at end of day. Two things follow: You do not need a cancel path to go live. If your design has one, delete it. An order state machine that waits for a cancel acknowledgement will wait forever. To change a price or a size, place a new order. A re-price is a new order and takes a new idempotency_key. Reusing the old key with a changed body returns ALREADY_EXISTS — see Recover a lost order.

What exists where cancellation does apply

Cancellation belongs to the generic order-entry surface, not to the vendor-order surface you use. OrderEntryAPI declares exactly three RPCs:
  • CreateOrderSubscription
  • InsertOrder
  • CancelOrder
CancelOrder applies to an order that can rest, which means an order placed through InsertOrder with a resting time-in-force. Nothing you place through CreateVendorOrder qualifies. [GAP] There is no cancel example anywhere in the partner tree. The quickstart promises one and does not deliver it. If you need to drive CancelOrder, ask your integration lead for a worked example rather than inferring one from the proto.
Not yet published. Whether OrderEntryAPI is entitled for an ISV, and what CancelOrder takes and returns, are not published. Do not design against it until you have both in writing.

There is no modify and no replace

[GAP] The partner surface has no modify and no replace RPC. There is no cancel/replace pair and no amend. Changing anything about a live order is not an operation that exists, which is consistent with nothing resting in the first place.

Day orders, if you read generic order-entry material

As of 2026-09-13 (v0.0.89), day orders cancel at the traded-day roll. Use GTD where you need an order to survive a specific horizon. This does not apply to you on the FOK-only partner surface. It matters if you are reading generic order-entry documentation as an IB or an FCM, where resting time-in-force values are available.

The legacy insertOrder contradiction

you cannot resolve this one yourself, so do not try.In a thin preprod book, the only way a partner can currently manufacture a fill is to rest an order through the legacy OrderEntryAPI/insertOrder so that a fill-or-kill order has something to cross. UAT §0 asks partners to disavow exactly that. Both requirements are live and they contradict each other: one testing path is the only one that works, and the acceptance document forbids it.Do not sign a UAT attestation that contradicts your own test method. Raise it with your integration lead in writing and get one of the two requirements changed before you run acceptance.
Related: because the partner surface is FOK-only, a thin book blocks ISVs completely while market makers are unaffected — they post the resting depth you need to cross. You are always the taker has the numbers.

What can go wrong

Next

Streams