> ## 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.

# Outcomes and recovery

> Tell a filled fill-or-kill from a killed one, resolve a PENDING order without doubling a position, and skip the cancel path.

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.

<Info>
  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](/orders);
  * you know which stream carries fills: drop copy is the source of record for fills and commissions
    — see [Drop copy](/streams#drop-copy).
</Info>

<Info>
  **API reference:** <a href="https://docs.polymarket.us/institutional/trading/overview" target="_blank" rel="noreferrer">Trading</a>. Opens on the public documentation site in a new tab.
  Where it disagrees with this page, this page is authoritative for the partner surface.
</Info>

## Order outcomes

<Note>
  **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.
</Note>

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.

| Value      | What it means                                                                                                                         | What it does not mean                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| `ACCEPTED` | Durably accepted, **including an order that was cancelled under fill-or-kill.**                                                       | Not filled. Not resting. Acceptance is not a fill. |
| `REJECTED` | An exchange-level rejection.                                                                                                          | Not a gRPC error — see below.                      |
| `PENDING`  | **Indeterminate, not queued.** We could not confirm a terminal outcome on that call, so the order's state is genuinely unknown to us. | Not known-not-placed. It may be live.              |

#### `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](#recover-a-lost-order).

### The exchange order state

A separate enum, `OrderState`, describes the order at the exchange:

| Value              | Number | Meaning                                      |
| ------------------ | ------ | -------------------------------------------- |
| `NEW`              | 1      | **Accepted and resting.**                    |
| `PARTIALLY_FILLED` | 2      | Partly filled.                               |
| `FILLED`           | 3      | Fully filled.                                |
| `CANCELED`         | 4      | Cancelled.                                   |
| `REJECTED`         | 7      | Rejected by the exchange.                    |
| `EXPIRED`          | 9      | **A fill-or-kill order that never crossed.** |

**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.

<Warning>
  **Not yet published.**&#x20;
  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.
</Warning>

```mermaid theme={null}
stateDiagram-v2
    state "PENDING — indeterminate, not queued" as vPending
    state "REJECTED — gRPC OK, not an error" as vRejected
    state "ACCEPTED — durable, not a fill" as vAccepted
    state "FILLED(3) — crossed resting depth" as sFilled
    state "EXPIRED(9) — never crossed, no reason" as sExpired
    state "REJECTED(7) — failure_reason has text" as sRejected
    state "CANCELED(4)" as sCanceled

    [*] --> vPending : outcome unconfirmed
    [*] --> vRejected : rejected on the call
    [*] --> vAccepted : accepted on the call

    vPending --> vAccepted : resubmit, same idempotency_key
    vPending --> vRejected : resubmit, same idempotency_key

    vAccepted --> sFilled : depth available
    vAccepted --> sExpired : no depth to cross
    vAccepted --> sRejected : exchange rejects
    vAccepted --> sCanceled : cancelled

    vRejected --> [*]
    sFilled --> [*]
    sExpired --> [*]
    sRejected --> [*]
    sCanceled --> [*]
```

### `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](/orders#you-are-always-the-taker) has the depth numbers.

### Telling a filled FOK from a killed one

| What you observe                                                                             | What happened                                                                                                                                                 | What you do                                                                                                                |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Drop-copy execution report with a non-zero `order_qty` and a `commission_notional_collected` | It crossed. This is the only positive proof of a fill.                                                                                                        | Credit the position. De-scale `order_qty` by `fractionalQtyScale` and the commission by `priceScale × fractionalQtyScale`. |
| `OrderState EXPIRED(9)`, no execution report, empty `failure_reason`                         | FOK never crossed. Not a rejection.                                                                                                                           | Credit nothing. No fee was charged. Re-price and resubmit under a new `idempotency_key` if you still want the trade.       |
| `OrderState REJECTED(7)` with text in `failure_reason`                                       | Exchange-level rejection.                                                                                                                                     | Log the text verbatim and show it to your operators, not to your users.                                                    |
| `failure_reason: Global Rate Limit Exceeded`                                                 | You exceeded a rate limit. It arrives as an **execution-report rejection**, not as an HTTP or gRPC error — so it reached the exchange and came back terminal. | Back off first. Then read the rule below before you resubmit anything.                                                     |
| `VendorOrderStatus ACCEPTED` on the call and then nothing at all                             | Almost always an `EXPIRED` FOK.                                                                                                                               | Do not hold the order open in your own state machine waiting for a fill.                                                   |
| An `ORDER_EXECUTION` entry on the balance ledger                                             | It crossed. Executions fold commission into a **single net** `ORDER_EXECUTION` entry.                                                                         | Reconcile against drop copy; do not expect a separate `COMMISSION` entry for an execution.                                 |
| A position delta on the position-change stream                                               | It crossed.                                                                                                                                                   | Use this stream to answer "did a market I hold resolve?", not as your primary fill feed.                                   |
| No signal on any stream and your `clord_id` is nowhere                                       | You may have lost the **stream**, not the order.                                                                                                              | Resume drop copy from your stored `resume_token` before you conclude anything.                                             |

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](#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.

<Warning>
  **Not yet published.**&#x20;
  \[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.
</Warning>

## 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

<Warning>
  **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.
</Warning>

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.

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Warning>
  **Not yet published.**&#x20;
  \[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.
</Warning>

<Note>
  **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](/funding#move-cash).

  Whether `CreateVendorOrder` behaves the same way is not published — do not assume it does.
</Note>

### 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.

<Note>
  The generated message and stub names below assume you ran `protoc` over `polymarket-protos.zip`.
</Note>

<CodeGroup>
  ```python Python theme={null}
  # Resolves one PENDING order. Safe to run repeatedly — the key and the bytes never change.
  # PolymarketClient is the canonical auth client published on /connect#authentication.
  import sys

  import grpc
  from polymarket_auth import PolymarketClient

  # Generated from polymarket-protos.zip. VERIFY your own package path.
  from polymarket.v1 import order_pb2, order_pb2_grpc

  # The gRPC target is GRPC_TARGET in polymarket_auth.py — one place, both environments.
  pmx = PolymarketClient()
  # Written at first submission: req.SerializeToString(deterministic=True). NEVER regenerated.
  REQUEST_PATH = "./orders/examplefirm-7f3c1a90.pb"

  with open(REQUEST_PATH, "rb") as f:
      req = order_pb2.CreateVendorOrderRequest.FromString(f.read())

  print(f"resubmitting idempotency_key={req.idempotency_key} clord_id={req.order.clord_id}")

  try:
      with pmx.channel() as ch:
          # x-participant-id is NOT sent on this RPC: pass no participant ID.
          resp = order_pb2_grpc.VendorOrderAPIStub(ch).CreateVendorOrder(req, metadata=pmx.metadata())
  except grpc.RpcError as e:
      if e.code() == grpc.StatusCode.ALREADY_EXISTS:
          # The guard working: the body differs from the first submission under this key.
          # Fix the BODY to match the stored bytes. Do NOT mint a new idempotency_key.
          sys.exit(f"ALREADY_EXISTS: body differs from the original submission — {e.details()}")
      raise  # still indeterminate; retry this same script, do not re-place

  status = order_pb2.VendorOrderStatus.Name(resp.status)

  if status == "PENDING":
      # Still unknown. Re-run this script. Never widen the blast radius with a new key.
      sys.exit(f"still PENDING {req.idempotency_key}: re-run, do not re-place")
  if status == "REJECTED":
      sys.exit(f"REJECTED: {resp.failure_reason}")

  # Resolved. ACCEPTED still does not mean filled — confirm on drop copy.
  print(f"resolved ACCEPTED funding_request_ids={list(resp.funding_request_ids)}")
  ```

  ```typescript TypeScript theme={null}
  // Resolves one PENDING order. Safe to run repeatedly — the key and the bytes never change.
  // PolymarketClient is the canonical auth client published on /connect#authentication.
  import { readFileSync } from "node:fs";
  import { PolymarketClient } from "./polymarketAuth";

  // Generated from polymarket-protos.zip. VERIFY your own package path.
  import {
    VendorOrderAPIClient,
    CreateVendorOrderRequest,
    VendorOrderStatus,
  } from "./gen/polymarket/v1/order";

  // The gRPC target is GRPC_TARGET in polymarketAuth.ts — one place, both environments.
  const pmx = new PolymarketClient();
  // Written at first submission with deterministic serialization. NEVER regenerated.
  const REQUEST_PATH = "./orders/examplefirm-7f3c1a90.pb";

  const req = CreateVendorOrderRequest.fromBinary(readFileSync(REQUEST_PATH));

  console.log(`resubmitting idempotency_key=${req.idempotencyKey} clord_id=${req.order?.clordId}`);

  let resp;
  try {
    // x-participant-id is NOT sent on this RPC: pass no participant ID.
    resp = await new VendorOrderAPIClient(pmx.channel())
      .createVendorOrder(req, await pmx.metadata());
  } catch (e: any) {
    if (e?.code === "ALREADY_EXISTS") {
      // The guard working: the body differs from the first submission under this key.
      // Fix the BODY to match the stored bytes. Do NOT mint a new idempotencyKey.
      throw new Error(`ALREADY_EXISTS: body differs from the original submission — ${e.details}`);
    }
    throw e; // still indeterminate; re-run this script, do not re-place
  }

  const status = VendorOrderStatus[resp.status];

  if (status === "PENDING") {
    // Still unknown. Re-run this script. Never widen the blast radius with a new key.
    throw new Error(`still PENDING ${req.idempotencyKey}: re-run, do not re-place`);
  }
  if (status === "REJECTED") {
    throw new Error(`REJECTED: ${resp.failureReason}`);
  }

  // Resolved. ACCEPTED still does not mean filled — confirm on drop copy.
  console.log(`resolved ACCEPTED funding_request_ids=${resp.fundingRequestIds}`);
  ```

  ```bash grpcurl theme={null}
  #!/usr/bin/env bash
  # Resolves one PENDING order. Safe to run repeatedly — the key and the bytes never change.
  # curl cannot speak gRPC. Reflection is entitlement-gated, so pass the protos.
  set -euo pipefail

  TOKEN="$(./token.sh)"   # token.sh is published on /connect#authentication; stdout is the token
  # Written at first submission, verbatim. NEVER regenerated: regeneration is what breaks recovery.
  REQUEST_PATH="./orders/examplefirm-7f3c1a90.json"

  IDEMPOTENCY_KEY=$(jq -r '.idempotency_key' "$REQUEST_PATH")
  echo "resubmitting idempotency_key=${IDEMPOTENCY_KEY}"

  set +e
  RESP=$(grpcurl \
    -import-path ./polymarket-protos \
    -proto polymarket/v1/order.proto \
    -H "authorization: Bearer ${TOKEN}" \
    -d @ < "$REQUEST_PATH" \
    grpc-api.preprod.polymarketexchange.com:443 \
    polymarket.v1.VendorOrderAPI/CreateVendorOrder 2>&1)
  RC=$?
  set -e

  if (( RC != 0 )); then
    case "$RESP" in
      *AlreadyExists*|*ALREADY_EXISTS*)
        # The guard working: the body differs from the first submission under this key.
        # Fix the BODY. Do NOT mint a new idempotency_key.
        echo "ALREADY_EXISTS: body differs from the original submission: ${RESP}" >&2; exit 2 ;;
      *)
        echo "still indeterminate, re-run this script, do not re-place: ${RESP}" >&2; exit 3 ;;
    esac
  fi

  echo "$RESP"
  case "$(echo "$RESP" | jq -r '.status')" in
    PENDING)  echo "still PENDING ${IDEMPOTENCY_KEY}: re-run, do not re-place" >&2; exit 4 ;;
    REJECTED) echo "REJECTED: $(echo "$RESP" | jq -r '.failure_reason')" >&2; exit 5 ;;
    ACCEPTED) echo "resolved ACCEPTED — confirm the fill on drop copy" ;;
    *)        echo "unknown status" >&2; exit 6 ;;
  esac
  ```

  ```go Go theme={null}
  // Resolves one PENDING order. Safe to run repeatedly — the key and the bytes never change.
  package main

  import (
  	"context"
  	"fmt"
  	"log"
  	"os"

  	"google.golang.org/grpc/codes"
  	"google.golang.org/grpc/status"
  	"google.golang.org/grpc/metadata"
  	"google.golang.org/protobuf/proto"

  	// Canonical auth client from /connect#authentication. The gRPC target is
  	// pmauth.GRPCTarget — one place, both environments.
  	pmauth "example.com/yourfirm/pmauth"

  	// Generated from polymarket-protos.zip. VERIFY your own package path.
  	orderv1 "github.com/yourfirm/gen/polymarket/v1"
  )

  // Written at first submission with proto.MarshalOptions{Deterministic: true}. NEVER regenerated.
  const requestPath = "./orders/examplefirm-7f3c1a90.pb"

  func main() {
  	ctx := context.Background()
  	pm := pmauth.New()

  	raw, err := os.ReadFile(requestPath)
  	if err != nil {
  		log.Fatalf("read stored request: %v", err)
  	}
  	req := &orderv1.CreateVendorOrderRequest{}
  	if err := proto.Unmarshal(raw, req); err != nil {
  		log.Fatalf("unmarshal stored request: %v", err)
  	}

  	fmt.Printf("resubmitting idempotency_key=%s clord_id=%s\n",
  		req.GetIdempotencyKey(), req.GetOrder().GetClordId())

  	conn, err := pm.Channel()
  	if err != nil {
  		log.Fatalf("dial: %v", err)
  	}
  	defer conn.Close()

  	// x-participant-id is NOT sent on this RPC: pass "" for the participant ID.
  	md, err := pm.Metadata("")
  	if err != nil {
  		log.Fatalf("metadata: %v", err)
  	}

  	resp, err := orderv1.NewVendorOrderAPIClient(conn).CreateVendorOrder(
  		metadata.NewOutgoingContext(ctx, md), req)
  	if err != nil {
  		if status.Code(err) == codes.AlreadyExists {
  			// The guard working: the body differs from the first submission under this key.
  			// Fix the BODY to match the stored bytes. Do NOT mint a new idempotency key.
  			log.Fatalf("ALREADY_EXISTS: body differs from the original submission: %v", err)
  		}
  		// Still indeterminate. Re-run this program; do not re-place under a new key.
  		log.Fatalf("still indeterminate, re-run, do not re-place: %v", err)
  	}

  	switch resp.GetStatus().String() {
  	case "PENDING":
  		log.Fatalf("still PENDING %s: re-run, do not re-place", req.GetIdempotencyKey())
  	case "REJECTED":
  		log.Fatalf("REJECTED: %s", resp.GetFailureReason())
  	}

  	// Resolved. ACCEPTED still does not mean filled — confirm on drop copy.
  	fmt.Printf("resolved ACCEPTED funding_request_ids=%v\n", resp.GetFundingRequestIds())
  }
  ```
</CodeGroup>

## 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](#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.

<Warning>
  **Not yet published.**&#x20;
  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.
</Warning>

### 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

<Warning>
  **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.
</Warning>

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](/orders#you-are-always-the-taker) has the numbers.

## What can go wrong

| Where                | Symptom                                                             | Cause                                                                                                                                      | What you do                                                                                                          |
| -------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| Order outcomes       | Users hold positions they never got                                 | Crediting on `VendorOrderStatus ACCEPTED`                                                                                                  | Credit on a drop-copy execution report only. `ACCEPTED` includes a killed FOK.                                       |
| Order outcomes       | Every exchange rejection is logged as a success                     | Branching on the gRPC status code only                                                                                                     | A rejection is gRPC `OK` with `status = REJECTED`. Branch on `resp.status`.                                          |
| Order outcomes       | \~99% of orders come back `EXPIRED`                                 | The FOK found no resting depth at your limit                                                                                               | Expected. Not an error, no reason field, no fee.                                                                     |
| Order outcomes       | Your error dashboard is unusable                                    | Alerting on `EXPIRED`                                                                                                                      | Exclude `EXPIRED` from rejection metrics; count it as "no liquidity".                                                |
| Order outcomes       | Duplicate positions after an incident                               | `PENDING` re-placed under a fresh `idempotency_key`                                                                                        | Same key, byte-identical body: [Recover a lost order](#recover-a-lost-order).                                        |
| Order outcomes       | Code waits forever for an `OrderState` of `PENDING`                 | There is no `PENDING` order state                                                                                                          | `PENDING` is a `VendorOrderStatus` value only.                                                                       |
| Order outcomes       | Rate-limit failures never appear in your API error rate             | `Global Rate Limit Exceeded` arrives as an execution-report rejection                                                                      | Parse `failure_reason` on execution reports into your rate-limit metrics.                                            |
| Order outcomes       | Credited quantity does not match the fill                           | Crediting the submitted quantity instead of the executed one                                                                               | Credit the execution report's `order_qty`, de-scaled. `PARTIALLY_FILLED(2)` exists in the enum.                      |
| Order outcomes       | A whole session of fills is missing after a reconnect               | Drop copy resumed from the start instead of from `resume_token`                                                                            | Persist the `resume_token` (\~576 bytes) and resume from it.                                                         |
| Recover a lost order | A user ends up with two positions and two prefunded transfers       | A `PENDING` order re-placed under a new `idempotency_key`                                                                                  | Same key, same bytes. A new key is a new order.                                                                      |
| Recover a lost order | `ALREADY_EXISTS` on every retry                                     | Your body differs from the first submission — regenerated `clord_id`, re-rounded price, a defaulted field, or echoed `funding_request_ids` | Replay the stored serialized request. Do not rebuild it.                                                             |
| Recover a lost order | `ALREADY_EXISTS` although you replayed the stored request           | Non-deterministic re-serialization in your stack                                                                                           | Serialize deterministically, or send the stored bytes without parsing.                                               |
| Recover a lost order | Recovery keeps returning `PENDING`                                  | The outcome is still unresolved on our side                                                                                                | Re-run the same script. There is no automatic retry and no published deadline — escalate with the `idempotency_key`. |
| Recover a lost order | You cannot tell whether the order filled                            | There is no status-lookup RPC on this surface                                                                                              | Resume drop copy from `resume_token`, then check for an `ORDER_EXECUTION` ledger entry.                              |
| Recover a lost order | Fills appear missing after a reconnect                              | Drop copy restarted from live instead of from `resume_token`                                                                               | Persist the token (\~576 bytes) on every message.                                                                    |
| Recover a lost order | You opened a balance-ledger stream per participant to check         | That stream is per-account and counts against the 20 streams/firm cap                                                                      | Do not fan out. The firm-level ledger stream is "coming soon" with no date.                                          |
| Recover a lost order | A retry after a terminal rejection does nothing                     | On `CashMovementService`, a terminally-rejected key replays its stored rejection                                                           | Use a new key after a *terminal reject* there. The behaviour on `CreateVendorOrder` is unconfirmed.                  |
| Cancelling           | Your order state machine hangs waiting for a cancel acknowledgement | There is nothing to cancel on a fill-or-kill surface                                                                                       | Remove the cancel path. Orders reach a terminal state on the call.                                                   |
| Cancelling           | `ALREADY_EXISTS` when re-pricing                                    | Reusing the original `idempotency_key` with a changed body                                                                                 | A re-price is a new order. New key.                                                                                  |
| Cancelling           | You cannot get a single fill in preprod to pass UAT                 | Thin, intermittent book plus FOK-only order entry                                                                                          | The only working workaround contradicts UAT §0. Escalate it rather than picking a side.                              |
| Cancelling           | You planned a modify/replace flow                                   | No modify or replace exists on the partner surface                                                                                         | Place a new order under a new key.                                                                                   |

<Snippet file="support.mdx" />

## Next

[Streams](/streams)
