> ## Documentation Index
> Fetch the complete documentation index at: https://platform.trendlinelabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Trading

> **Beta: not yet enabled; the launch venues are Polymarket, predict.fun and Limitless (Kalshi comes later). Requests answer 404 with \{"detail": "Not Found"}, as an unregistered path does, until trading is switched on.**

Keys granted `trade` place, list, read and cancel limit orders on their own user's connected venue
accounts, on the venues enabled for trading. Every order gets our id, `ord_` and 24 hex digits, the
moment we accept it.

## Placing an order

Name the market by its `universal_id`, the same id every market-data route uses. The order goes to
that market's leg on `venue`, so moving the same order to another venue is one field:

```sh theme={null}
curl -sS -X POST "${TRENDLINE_API_BASE:-https://systems.trendlinelabs.ai}/public/v1/orders" \
  -H "Authorization: Bearer $TRENDLINE_API_KEY" -H "Content-Type: application/json" \
  -d '{"venue":"polymarket","universal_id":"mk-btc-100k-2026","outcome":"yes","side":"buy","type":"limit","price":"0.42","size":10,"client_order_id":"s1-0001"}'
```

The answer (201) is the order, with our `id`:

```json theme={null}
{"id": "ord_6702291f8e1c2a0012ab34cd", "client_order_id": "s1-0001", "venue": "polymarket",
 "venue_order_id": "0x5e5e…5e", "universal_id": "mk-btc-100k-2026", "status": "open", "filled_size": "0",
 "remaining_size": "10", "created_at": "2026-10-06T05:59:59+00:00", "updated_at": "2026-10-06T05:59:59+00:00", "…": "…"}
```

The same order on Limitless changes `venue` only: `"venue":"limitless"`. There is no
`"venue":"best"`: the API does not choose a venue for you (a possible later addition, not built).

You may also send the venue's own market id, `venue_market_id`, instead of or beside `universal_id`.
Sent together, both must name the same leg, else the order is refused with 422 `invalid_request`; an
order is never re-routed. Every answer carries both ids, plus the leg's title on the venue when we
hold it.

## Routes

| route | operation | what it does |
| - | - | - |
| `POST /public/v1/orders` | placeOrder | place one limit order |
| `GET /public/v1/orders` | listOrders | your orders, newest first, filtered by `venue` and `status`, paged by `limit` and `cursor` |
| `GET /public/v1/orders?client_order_id=` | listOrders | the one order with your id, the same answer as by our id |
| `GET /public/v1/orders/{id}` | getOrder | one order by our id |
| `DELETE /public/v1/orders/{id}` | cancelOrder | cancel one order by our id |
| `DELETE /public/v1/orders?client_order_id=` | cancelOrderByClientId | cancel one order by your id |

Every route answers only for the key's own account: another account's order is 404 `order_not_found`.
`{id}` is `ord_` and 24 lowercase hex digits, else 422 `invalid_request`; an unknown query parameter is
also 422, and `client_order_id` on `GET /public/v1/orders` is sent alone.

These routes read our own record of the orders you placed through the API; `GET
/public/v1/account/orders?venue=` is different: it is the venue's own list of your account's orders,
however they were placed (a key also granted `account:read`).

## The order

| field | meaning |
| - | - |
| `id` | our id, `ord_` and 24 hex digits; it exists from the moment we accept the request, so an `unknown` order has one too |
| `client_order_id` | your id, as sent |
| `venue`, `venue_order_id` | the venue and its own order id; `venue_order_id` is null while `unknown` and for an order never placed |
| `universal_id`, `venue_market_id`, `venue_market_title` | the market and its leg on the venue |
| `outcome`, `side`, `type`, `price`, `size`, `time_in_force` | as you sent them |
| `status` | `open` (resting), `filled`, `partially_filled` (part filled and nothing resting: the rest was cancelled, or an `ioc` order's unfilled part), `cancelled` (nothing filled), `unknown` or `not_placed` |
| `filled_size`, `remaining_size`, `avg_price` | the venue's fill facts as exact decimal strings; null while `unknown` |
| `notional_reserved` | the order's maximum loss counted against your limits, USD |
| `created_at`, `updated_at` | when we accepted the order and when we last recorded a change, ISO 8601 |

Prices are decimal strings from 0 to 1 of your `outcome` on every venue, and `size` is the whole
contracts you sent.

## The order's lifecycle

1. We accept the request and the order gets its `id`. Nothing has been sent yet.
2. We send it to the venue once. Its answer gives `open`, `partially_filled`, `filled` or `cancelled`
   (201), or a refusal (4xx, nothing placed).
3. If the venue's answer is lost, the order is `unknown` (202): it may or may not exist. Do one of:

   * retry the same request with the same `client_order_id`, or
   * read `GET /public/v1/orders/{id}`.

   Either looks the order up at the venue (at most once every few seconds) and answers its state, or
   `not_placed` once we can prove it never arrived. We never send it a second time.
4. An order on the book ends `filled`, `partially_filled` or `cancelled`. Reading it by its `id` (or
   by `client_order_id`) re-reads an `open` order at the venue, at most once every few seconds, and
   records new fills or its final state, including a cancel made on the venue's own website. Between
   those reads, and when the venue cannot be read, the answer is our last recorded state, and
   `updated_at` is when it last changed. A final order is never re-read.

Some venues cannot prove that an order is absent (see the table below). There an order whose answer
was lost can stay `unknown` for good: it is never reported `not_placed`. Check the order on the venue
itself before you trade the same intent again under a new `client_order_id`.

## Who can trade

Trading is granted by hand, per account, with its limits and the account holder's jurisdiction. A
`trade` key is refused at creation unless the account holds an active trade grant and the key's
`ip_allowlist` is:

* non-empty;
* made only of global (public internet) address ranges;
* no wider than /24 for IPv4 or /48 for IPv6;

and its lifetime is at most 90 days.

An example allowlist entry is `198.51.100.0/24`. That range is reserved for documentation, so a real
key needs your own server's public range instead: a documentation, private or shared range is not
global and is refused.

## The request

Every field is required unless marked optional or a default is shown. Unknown fields are refused with
422 `invalid_request`, and nothing is ever coerced or rounded.

| field | type | meaning |
| - | - | - |
| `venue` | string | the venue to trade on: `polymarket`, `predictfun` or `limitless` (`kalshi` later), when enabled for trading |
| `universal_id` | string, optional | the market universal id; send this, `venue_market_id`, or both |
| `venue_market_id` | string, optional | the venue's own market id; with `universal_id`, both must name the same leg |
| `outcome` | string | `yes` or `no`, the venue's own label |
| `side` | string | `buy` or `sell`, the action on `outcome` |
| `type` | string | `limit` |
| `price` | string | the limit price of `outcome`, a plain decimal string from 0 to 1 on the venue's price grid; off the grid is 422 `price_off_tick`, not a decimal is `invalid_request`, never rounded |
| `size` | integer | whole contracts, 1 to 1,000,000 |
| `time_in_force` | string | `gtc` (rests until cancelled, the default) or `ioc` (fills what it can now and cancels the rest), where the venue offers it |
| `client_order_id` | string | an id **you choose** for this order (e.g. a UUID), 1 to 64 of `A-Z a-z 0-9 _ -`; use a new one per order, and resend the same one only to retry that order |

Before any order is sent, its market leg is checked: a leg we cannot verify is the venue market you
meant is refused with 409 `venue_leg_unverified`, and nothing is sent.

## Venues

What differs by venue. A venue answers only once it is enabled for trading.

| | Polymarket | predict.fun | Limitless | Kalshi |
| - | - | - | - | - |
| status | at launch | at launch | at launch | Coming later |
| `time_in_force` | `gtc`, `ioc` (the CLOB's fill-and-kill) | `gtc` only (rests up to 30 days); `ioc` is 422 `venue_unsupported` | `gtc`, `ioc` (fill-and-kill) | `gtc`, `ioc` |
| `venue_order_id` | the order's hash, `0x` + 64 hex | the order's hash, `0x` + 64 hex | Limitless's order id (assumed to fit `A-Z a-z 0-9 -`, up to 64; unconfirmed) | Kalshi's order id (a UUID) |
| `venue_market_id` | the market's slug | the market's id | the market's slug | the market's ticker |
| price grid and minimum size | the market's own tick, within \[tick, 1 − tick]; on a market whose tick is finer than `0.01`, `price` × `size` must also come to whole cents, else 422 `price_off_tick`; a buy must reach the market's minimum order value | the market's tick, at least `0.01`, within \[tick, 1 − tick] | a 0.001 grid within `0.01` to `0.99` | whole cents, `0.01` to `0.99` |
| per-order limit | your account's limits, plus the server signer's own per-order and daily limit | your account's limits, plus the server signer's own per-order and daily limit | your account's limits, plus the server signer's own per-order and daily limit | your account's limits |
| one-time setup | the website's embedded wallet, with server-side signing granted, and its Polymarket trading credentials | the website's embedded wallet, with server-side signing granted, and a predict.fun login | the website's embedded wallet with server-side signing ("Fast trading") on, and its Limitless sub-account | connect your Kalshi API key on the website |
| `unknown` | can stay `unknown` (absence is never claimed) | can stay `unknown` (cancelled and expired orders are not listed) | can stay `unknown` (absence is never claimed) | ends: found, or proven `not_placed` from a complete read |
| re-reading an `open` order | found by its hash, in any state | found while open or filled; an order cancelled or expired on predict.fun is not listed, so it keeps reading `open` here (its venue view is `GET /public/v1/account/orders`) | found among your latest orders on that market | found among your orders on that market since it was placed |
| jurisdiction | no rule is set yet, so orders are refused (503 `dependency_unavailable`) | no rule is set yet, so orders are refused (503 `dependency_unavailable`) | no rule is set yet, so orders are refused (503 `dependency_unavailable`) | the one on your trade grant: US only, not a blocked state |
| cancel | cancels on the CLOB | removes the order from the book only (off-chain) | cancels a resting order on Limitless | cancels at Kalshi |

Where an order can stay `unknown`, retry the same request with the same `client_order_id` or read it
by its `id` (it is found once the venue lists it), or cancel it, which checks it first.

## Idempotency

`client_order_id` makes an order idempotent: one order per id, ever, across all your keys.

* **The same request again** returns the latest answer for that order with `Idempotent-Replay:
  true`: the first 201, or the 200 a later check produced, with the same `id`. It does so even after
  the market closed, the limits changed or trading was halted.
* **A different request with a used id** is 409 `idempotency_conflict`.
* **While the first is still in flight** the answer is 409 `request_in_progress` with `Retry-After`.

A refusal answers this request only; after any lost or unclear answer, retry with the same
`client_order_id`.

## Listing your orders

`GET /public/v1/orders` answers `{"orders": [...], "next_cursor": ...}`, newest first, from our own
record: no venue is called, so each status is as of our last check. Read an order by its `id` for a
fresh one, or `GET /public/v1/account/orders?venue=` for the venue's own list. `venue` and `status` filter it; `limit` is 1 to 100 (default 50). Pass
`next_cursor` back as `cursor` for the next page, with the same filters: a cursor from other filters
or another account is 422 `invalid_request`. `next_cursor` is null on the last page.

## Limits

Each account has a per-order and a daily limit on **maximum loss**: a buy can lose its price, a sell
the rest of the dollar, times the size. Fees are not included: a fill's cash outlay can exceed the
counted amount by the venue's fee. The daily limit counts **submitted** notional per UTC day: a
cancelled, unknown or never-placed order still counts that day. Only a rejection by the venue, a 503
`venue_unavailable` answer for an order that never reached the venue, and the unfilled part of an
`ioc` order give it back. Orders also have their own per-key rate limit; cancels
and reads never count against it.

## Cancelling

Cancel an order this account placed through the API by our id (`DELETE
/public/v1/orders/ord_6702291f8e1c2a0012ab34cd`) or by yours (`DELETE
/public/v1/orders?client_order_id=s1-0001`); the venue is the order's own. Both answer the order with
`cancelled_size`, the contracts this cancel removed. The order in the answer is the venue's view after
the cancel: `cancelled`, or `partially_filled` when part of it filled first, with `filled_size` saying
how much. On Kalshi the venue answers only the removed amount, so there `filled_size` is our last
recorded count. Cancels are never blocked by a halt, a revoked trade grant or the order rate, so you
can always reduce risk.

An order still `unknown` is checked at the venue first. Found, it is cancelled. Proven never placed,
the answer is 200 with `status: not_placed` and nothing cancelled. Not yet proven, the answer is 409
`request_in_progress` with `Retry-After`, and nothing was cancelled. An order already final (filled,
partially filled, cancelled or never placed) answers 200 with its state and `cancelled_size` `"0"`, and
nothing is sent to the venue.

## Codes

| status | code | meaning |
| - | - | - |
| 401 | unauthorized | no key, or a key that is unknown, revoked or expired |
| 403 | access\_not\_granted | API access is not granted for this account |
| 403 | tier\_required | the key owner's plan does not include API access |
| 403 | insufficient\_scope | the key lacks `trade` |
| 403 | ip\_not\_allowed | outside the key's allowlist, or the allowlist no longer meets the trading rules |
| 403 | trade\_not\_granted | the account holds no active trade grant |
| 403 | jurisdiction\_refused | the jurisdiction recorded on your trade grant may not trade on that venue |
| 403 | trading\_tier\_required | the account's plan may not trade on that venue |
| 404 | not\_found | no such market |
| 404 | venue\_not\_listed | the market has no leg on that venue |
| 404 | venue\_not\_connected | no usable venue account of yours is connected |
| 404 | order\_not\_found | no order with that id was placed through the API by this account |
| 409 | idempotency\_conflict | the `client_order_id` was used with a different request |
| 409 | request\_in\_progress | a request with this `client_order_id` is in flight, or its outcome is still being checked; retry after `Retry-After` |
| 409 | venue\_leg\_unverified | the market's leg could not be verified as the venue market you meant; nothing was sent |
| 409 | venue\_auth\_failed | the venue refused your stored credential; reconnect it |
| 409 | order\_not\_cancellable | the venue no longer holds the order, or another credential placed it |
| 410 | market\_closed | the market's leg on that venue no longer trades |
| 422 | invalid\_request | a field or parameter is missing, unknown or malformed, an `id` is not `ord_` and 24 hex digits, a cursor does not match, or `universal_id` and `venue_market_id` name different legs |
| 422 | venue\_unsupported | trading is not enabled for that venue |
| 422 | price\_off\_tick | the price is not on the venue's grid |
| 422 | limit\_exceeded | over the per-order or daily limit, or no limit is set (`limit_unset`) |
| 422 | venue\_rejected | the venue rejected the order; the message carries its own error code only |
| 429 | rate\_limited | too many requests or orders; wait `Retry-After` |
| 500 | internal\_error | an unexpected failure on our side |
| 503 | trading\_halted | new orders are halted for everyone; cancels and retries still answer |
| 503 | venue\_unavailable | the venue did not answer or limited us; retry after `Retry-After` when sent |
| 503 | dependency\_unavailable | one of our own stores could not be read; retry later |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.