Skip to content

Trading

Two endpoints place trades on behalf of an account: one opens a position, one closes it. Both are POST, both require an Idempotency-Key, and both move real money the moment they succeed.

Early access

The trading endpoints are not enabled on production. See the changelog.

Who may trade

A server credential needs trades:write. A delegated trader token needs trader:trade and must not be read-only. Either scope reaches the same endpoints.

The difference is reach, and it comes from the token, not from the scope name:

  • A delegated token can only trade its own account, and — when the token was minted against a specific trading account — only that trading account. Anything else is a 404.
  • A server credential holding trader:trade is not limited to one account. It reaches every account its credential reaches, exactly as trades:write does. The trader: prefix describes who the scope is meant for, not a boundary the API enforces on a server credential.

Open a position

POST /api/v1/positions/open
curl -sS -X POST "$BASE/api/v1/positions/open" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Idempotency-Key: 5c0a9e77-2f41-4d1a-9c3e-b0a2f7e41d55' \
  -H 'Content-Type: application/json' \
  -d '{
        "account_id": "4210001",
        "trading_account_id": "4210044",
        "instrument_id": "770012",
        "side": "Buy",
        "size": "0.25",
        "stop_loss": "1.08900",
        "take_profit": "1.10500",
        "external_id": "acme-order-99213"
      }'
Field Required Notes
account_id Yes The account that owns the trading account.
trading_account_id Yes Must belong to account_id, or the answer is 404.
instrument_id Yes Decimal string.
side Yes Buy or Sell.
size Yes Volume in lots, as a decimal string. Must be greater than zero.
desired_price No Omit for a market order and the current price is used.
stop_loss, take_profit No Decimal strings.
external_id No Your own label, carried through unchanged.

external_id is not a deduplication key

It is never checked for uniqueness, and sending the same one twice places two orders. Exactly-once is the Idempotency-Key's job. external_id exists so you can reconcile against your own records.

Both ids are required because a trading account is reached through its account. The pair is verified, so a trading account that does not belong to the account you named is a 404 rather than a trade on someone else's book.

Close a position

POST /api/v1/positions/close
curl -sS -X POST "$BASE/api/v1/positions/close" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Idempotency-Key: b71d3f0c-58a2-4e6b-8c11-9d40e2f7a6b3' \
  -H 'Content-Type: application/json' \
  -d '{
        "account_id": "4210001",
        "trading_account_id": "4210044",
        "position_id": "88104",
        "size": "0.10"
      }'

size is optional: omit it to close the whole position, or send a smaller volume for a partial close. desired_price and external_id behave as they do on an open. You do not send a side — the close takes the position's own.

The response

Both endpoints answer with the same shape:

{
  "id": "91002",
  "account_id": "4210001",
  "trading_account_id": "4210044",
  "instrument_id": "770012",
  "action": "Open",
  "side": "Buy",
  "size": "0.25",
  "desired_price": "1.09442",
  "state": "Confirmed",
  "rejection_reason": null,
  "confirmed_price": "1.09444",
  "position_id": null,
  "external_id": "acme-order-99213",
  "received_at": 1753634000123
}
Field Meaning
id The order's id. Keep it — it is how you correlate the outcome.
action Open or Close.
state Confirmed, Rejected, or Placed (accepted, outcome not settled yet).
rejection_reason Set when state is Rejected. A fixed vocabulary you can branch on.
confirmed_price The price actually filled.
position_id On a close, the position that was closed.
received_at Unix epoch milliseconds.

200 and 202 mean different things

Status Meaning
200 The order reached a final state. Read state: it is Confirmed or Rejected.
202 Your brand runs a configured execution delay and the order did not settle within it. The body is the latest snapshot, usually state: "Placed". The outcome is not known yet.

A 202 is not a failure and must not be retried — the order is live. Learn the outcome from the event stream: subscribe to the events channel or register a webhook for the order category, and match your id against the event's resource_id. The order is not readable back through a REST call on this API.

Rejections

A rejected order is a 200 with state: "Rejected", not an HTTP error — the request was valid and the trading engine declined it. rejection_reason tells you which, with values such as InsufficientMargin, MarketIsClosed, TradingIsDisabled, InvalidSize, InvalidStopLoss, MaxExposureReached, CloseOnly, AccountReadOnly and TradeAlreadyClosed. The complete list is in the reference.

Treat a value you do not recognize as a plain rejection rather than failing on it: the vocabulary can gain entries in a minor release.

Refusals

Status code Means
403 forbidden No trading scope, or your firm's own permissions no longer allow it.
403 read_only_credential The delegated token was minted read-only.
404 not_found The account, trading account or position does not exist or is out of reach — including a trading account that does not belong to the account you named.
409 idempotency_in_flight An attempt with this key is still running. Wait, then repeat.
422 validation_failed A size of zero or less, an id that is not a decimal string, or a price that is not a decimal string.
503 write_contention Pricing was briefly unavailable, or the account was busy. Retry with the same key.

One key, one order

The key is scoped to the trading account, not to what you are trading. Reusing a key on the same trading account returns the first order and ignores the new instrument, side and size — with no error. See the body is not compared.

What this API deliberately cannot do

These are operator controls in the Backoffice, and no credential configuration opens them up here:

  • Override the leverage or the margin a trade uses.
  • Override or skip the brand's configured execution delay.
  • Trade outside configured market hours, or on an instrument marked untradeable.
  • Set the fill price by hand.

There is also no endpoint to amend a live order or to place a pending order. Open and close are the whole trading surface in v1.