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:tradeis not limited to one account. It reaches every account its credential reaches, exactly astrades:writedoes. Thetrader: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.