---
title: "Place a paper order"
method: POST
path: "/portfolio/orders"
tags: ["Trading"]
---

# Place a paper order

`POST /portfolio/orders`

## Request

- Base URL: `https://arena-predictions.com/api/v1`
- URL: `https://arena-predictions.com/api/v1/portfolio/orders`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer

Places an order for the key owner. The default is a paper order, as the CLI and the MCP server do: nothing is sent to Kalshi or any venue. execution kalshi sends a live Kalshi order (scope trade:write:kalshi) through the live door and does not touch the paper book. execution poly-intl sends a live Polymarket International order (scope trade:write:poly-intl) and refuses a US visitor. Polymarket US is not offered. Send X-Arena-Org to place a paper order on that organization's paper sandbox; without the header the paper order is on the personal account. An unknown or forbidden organization places nothing. A market order fills now at the live price or is refused. A limit order fills at limitPriceCents or better, rests until it fills, is canceled or expires (gtc), or is canceled at once when it does not fill (ioc, fok). Opening a position needs Arena Basic. Every paper order counts against the hard trade limits (per key 10 new orders a minute and 50 a day, 20 sells and cancels a minute; per account 20, 100 and 40), which are always enforced. No ticker, title or price in the paper answer while the venue-data switch is off.

## Headers

- `X-Arena-Org` string

## Request body

- OrderRequest: A buy names instrumentId or ticker, side and count. A sell names tradeId and covers the whole open position. A limit order adds limitPriceCents. Unknown fields are refused (400), so a typo never changes an order.
  - `action` 'buy' | 'sell'
  - `type` 'market' | 'limit', required: market: fills now at the live price or is refused. limit: fills at limitPriceCents or better, or rests (gtc), or is canceled (ioc, fok).
  - `idempotencyKey` string, uuid, required: One per order. The same key again answers the stored order with replayed: true and places nothing; the same key with a different order is 409 idempotency_key_reused. Retry with the same key after a timeout or a 503.
  - `clientOrderId` string, uuid: Kalshi's name for idempotencyKey. Send one of the two.
  - `instrumentId` string: Buy: the Arena instrument; side is the instrument's side. It must list on one live Kalshi market (409 no_tradable_listing otherwise).
  - `ticker` string: Buy: a Kalshi market ticker instead of instrumentId; side is the market's side. Never echoed back while the venue-data switch is off.
  - `side` 'yes' | 'no': Buy: the side to buy.
  - `count` integer: Buy: contracts. Your paper balance is the limit.
  - `tradeId` string, uuid: Sell: the open position (GET /portfolio).
  - `limitPriceCents` integer: Limit: your price for the side you trade, in cents (a percent).
  - `timeInForce` 'gtc' | 'ioc' | 'fok' | 'good_till_canceled' | 'immediate_or_cancel' | 'fill_or_kill'
  - `expiresAt` string, date-time: gtc only: when the order stops resting. At most 365 days out.
  - `maxPriceCents` integer: Market buy: refuse to fill above this (409 price_moved, nothing placed).
  - `minPriceCents` integer: Market sell: refuse to sell below this (409 price_moved, nothing sold).
  - `route` 'kalshi' | 'best': best: a fill may be priced at Novig's or Polymarket's price when it passes every check. Omitted: Kalshi's.
  - `statedProbPct` integer: Buy: your probability for the side. Never changes the fill; scored on /eval. Paper only.
  - `execution` 'paper' | 'kalshi' | 'poly-intl': paper (default) uses the paper book and trade:write. kalshi sends a live Kalshi order and needs trade:write:kalshi. poly-intl sends a live Polymarket International order and needs trade:write:poly-intl. Polymarket US is not a value.
  - `op` 'create' | 'cancel' | 'amend': Live only. Default create.
  - `orderId` string, uuid: Live cancel or amend: the Kalshi order id.
  - `priceCents` integer: Live: the price of the side, in cents.
  - `updatedPriceCents` integer: Live amend: the new price of the side.
  - `updatedCount` integer: Live amend: the new contract count.

## Response `200`

A replay: this idempotencyKey was used before for the same order. Nothing new was placed.

- OrderResult
  - `status` 'executed' | 'resting' | 'canceled', required: canceled: an ioc or fok that did not fill. Nothing was spent.
  - `action` 'buy' | 'sell', required
  - `type` 'market' | 'limit', required
  - `idempotencyKey` string, uuid, required
  - `replayed` boolean, required: True when this idempotencyKey was used before: the stored order, nothing new placed.
  - `originalCreatedAt` string, date-time, nullable, required: On a replay: when the order was first placed.
  - `order` OrderDetail, required
    - `orderId` string, uuid, required
    - `action` 'buy' | 'sell' | 'null', nullable, required
    - `instrumentId` string, nullable, required
    - `instrumentLabel` string, nullable, required
    - `instrumentSide` 'yes' | 'no' | 'null', nullable, required
    - `orderSide` 'yes' | 'no' | 'null', nullable, required
    - `contracts` integer, required
    - `filledContracts` integer, required
    - `remainingContracts` integer, required
    - `limitCents` integer, nullable, required: Your own limit.
    - `holdDollars` number, required
    - `timeInForce` string, nullable, required
    - `expiresAt` string, date-time, nullable, required
    - `status` string, nullable, required
    - `statedProbPct` integer, nullable
    - `placedVia` string, nullable
    - `placedPrivate` boolean
    - `createdAt` string, date-time, nullable, required
    - `marketTicker` string, nullable: Only while the venue-data switch is on.
    - `type` 'limit', required
    - `cancelReason` string, nullable, required: user, expired, not_filled, market_closed, market_resolved, position_closed, and so on.
    - `tradeId` string, uuid, nullable, required: The position it opened or closed, once executed.
    - `fillPriceCents` integer, nullable: Only while the venue-data switch is on.
  - `trade` Trade, required
    - `tradeId` string, uuid, required
    - `instrumentId` string, nullable, required: The Arena instrument the market lists; null when not mapped.
    - `instrumentLabel` string, nullable, required: The instrument's YES side in words.
    - `instrumentSide` 'yes' | 'no' | 'null', nullable, required: The side of the instrument held.
    - `boughtSide` 'yes' | 'no' | 'null', nullable, required: The side of the market bought.
    - `contracts` integer, required
    - `costDollars` number, required: Paper dollars paid.
    - `statedProbPct` integer, nullable, required: The probability you stated when placing, if any.
    - `placedVia` string, nullable, required
    - `placedPrivate` boolean, required
    - `parlayId` string, uuid, nullable, required
    - `fillVenue` string, required
    - `openedAt` string, date-time, nullable, required
    - `marketTicker` string, nullable: Only while the venue-data switch is on.
    - `entryCents` number, nullable: Only while the venue-data switch is on.
    - `status` 'open' | 'won' | 'lost' | 'settled_push' | 'cancelled' | 'sold' | 'null', nullable, required
    - `closedAt` string, date-time, nullable, required
    - `closeReason` string, nullable, required
    - `pnlDollars` number, nullable, required
    - `resolvedOutcome` 'yes' | 'no' | 'push' | 'void' | 'null', nullable, required: The instrument's result.
  - `fillVenue` 'kalshi' | 'novig' | 'polymarket' | 'null', nullable, required: Where the fill was priced. Paper only: nothing is sent there. The first leg when the ticket split.
  - `fills` object[]: Present when route best split one ticket across venues. priceCents is omitted while venue data is off.
    - `venue` 'kalshi' | 'novig' | 'polymarket', required
    - `contracts` number, required
    - `priceCents` integer
  - `mode` 'paper', required
  - `venueData` boolean, required
  - `fillPriceCents` integer, nullable: Only while the venue-data switch is on.
  - `organization` object: Present when X-Arena-Org selected an organization sandbox. Absent on the personal account.
    - `id` string, uuid, required
    - `slug` string, nullable: The organization's slug. Null when the sandbox did not name one.

## Other responses

- `201`: Placed: executed, resting, or canceled (an ioc or fok that did not fill).
- `400`: invalid_request (the message names the field), or insufficient_balance.
- `401`: Missing, malformed, unknown, revoked or expired key. Carries a WWW-Authenticate: Bearer challenge.
- `402`: membership_required: opening a position needs Arena Basic. Sells work without it.
- `403`: insufficient_scope: the key does not carry trade:write. organization_forbidden: X-Arena-Org names an organization this key cannot use. Nothing was placed on the personal account.
- `404`: instrument_not_found, market_not_found, trade_not_found (a sell), or organization_not_found.
- `409`: Nothing placed: idempotency_key_reused, no_tradable_listing, market_not_open, market_resolved, no_liquidity, price_moved (past maxPriceCents or minPriceCents), or already_closed (a sell of a position that is no longer open).
- `429`: rate_limited: over a hard trade limit (always enforced), or over the plan's requests while those limits are enforced; or quota_exceeded. Nothing placed. error.retryAfterSeconds equals Retry-After.
- `503`: trading_off: trading with API keys is switched off right now; nothing placed. internal_error: the key could not be checked, or Arena could not confirm the order; retry after Retry-After with the same idempotencyKey, which never places twice.

---

[API](https://skmtc.dev/arena-predictions/apis/arena-api.md) · [All operations](https://skmtc.dev/arena-predictions/apis/arena-api/llms.txt) · [OpenAPI document](https://skmtc.dev/arena-predictions/apis/arena-api/revisions/9d89d166dae6?raw)
