Before this page:
- you can place an order, and you know that
CreateVendorOrderacceptsTIME_IN_FORCE_FILL_OR_KILLand 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.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.
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:
- Do not alert on
EXPIRED. At preprod book depth it is the normal outcome, and paging on it buries the rejections that do matter. - Do not surface
EXPIREDto a user as an error. “No liquidity at your price” is what happened. - 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.
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.
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
Three corollaries: Serialize once, store the bytes, replay the bytes. Rebuilding the request from your own order row re-introduces every difference that tripsALREADY_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.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 aPENDING 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:
CreateOrderSubscriptionInsertOrderCancelOrder
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.