API reference

Trading accounts

List, read, provision, assign and archive the trading accounts your firm runs.

A trading account is one simulated account running on your executor. It is created against an account type, freezes that type’s rule set version at creation, and is identified by an 8-digit number your trader signs in with.

List accounts

GET/v1/accountsAvailablescope · accounts:read

Newest first, cursor-paginated.

curl
curl "https://api.propexecutor.com/v1/accounts?limit=50" \
  -H "Authorization: Bearer $PFX_KEY"

Each row

iduuid
Internal identifier. Stable forever.
accountinteger
The 8-digit number. What your trader types into the executor, and what every path on this page takes.
account_type_iduuid
The account type this was provisioned against.
account_type_namestring
Its name, e.g. $10K Standard.
trader_iduuid | null
The trader holding it, or null if it is provisioned and unassigned.
trader_emailstring | null
That trader's email, for display.
statusstring
active, breached, passed or archived.
status_reasonstring | null
Why the rule engine ended the challenge — which rule, and the numbers.
status_changed_attimestamp | null
When that happened.
starting_balancenumber
What the account was funded with.
created_attimestamp
When it was provisioned.

Read one account

GET/v1/accounts/{account}Availablescope · accounts:read

The same row plus live trading state — balance, equity and open positions as of now.

200 OK
{
  "id": "6ab66c2b-a334-ee36-8da4-5f2700000001",
  "account": 10000042,
  "account_type_name": "$10K Standard",
  "status": "active",
  "starting_balance": 10000,
  "balance": 10229.72,
  "equity": 10187.4,
  "peak_equity": 10310.55,
  "open_positions": [
    {
      "id": "b2c4…",
      "instrument": "EURUSD",
      "side": "long",
      "size": 0.5,
      "entry_price": 1.08421,
      "mark_price": 1.08337,
      "stop_loss": 1.081,
      "take_profit": null,
      "margin_used": 180.7,
      "floating_pnl": -42.32,
      "opened_at": "2026-09-27T13:02:11Z"
    }
  ],
  "created_at": "2026-09-14T09:12:04Z"
}

Balance and equity are not the same number

balance is realized: it only moves when a position closes. equity is balance plus the floating profit and loss of everything still open, marked at the latest price — the number the rule engine judges drawdown against.

Provision accounts

POST/v1/accountsPlannedscope · accounts:write

Creates one or more accounts against an account type. Up to 100 per request.

curl
curl -X POST https://api.propexecutor.com/v1/accounts \
  -H "Authorization: Bearer $PFX_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"account_type_id":"0f9c…","quantity":25}'
account_type_iduuid · required
Which account type to provision.
quantityinteger
How many. Defaults to 1, capped at 100 per request.
trader_iduuid
Assign them to this trader immediately. Omit to provision unassigned and assign later — which is what most firms do.

This spends credits, permanently

Each account costs one account credit for good. Breaching or archiving it gives nothing back. Send an Idempotency-Key so a network retry cannot spend a second batch, and see limits for the 402 you get when you run out.

Assign a trader

PUT/v1/accounts/{account}/traderAvailablescope · accounts:write

Hands an account to a trader, identified by email. You do not have to create the trader first and you do not have to keep our ids: if your organization has no trader with that email, this creates one with the name you pass and assigns it, in the same transaction.

curl
curl -X PUT https://api.propexecutor.com/v1/accounts/10000042/trader \
  -H "Authorization: Bearer $PFX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Jordan Ellis","email":"jordan@example.com"}'
namestring
Required. Used only when the trader is being created. If the email is already known, the name we have wins and this is ignored — see below.
emailstring
Required. The identity this call resolves on. Lower-cased and trimmed, so Jordan@Example.com and jordan@example.com are the same trader. Nothing else is normalized: jordan+prop@ and jor.dan@ stay distinct people, because deciding otherwise is your call, not ours.
200 OK
{
  "account": 10000042,
  "trader": {
    "id": "3d21f0c2-…",
    "name": "Jordan Ellis",
    "email": "jordan@example.com"
  },
  "trader_created": true,
  "assigned": true,
  "sessions_revoked": true
}
trader_createdboolean
True when this call created the trader record, false when it reused one you already had.
assignedboolean
True when the account actually changed hands. False when it was already assigned to this same trader — the call is a no-op, and nothing was written.
sessions_revokedboolean
Always equal to `assigned`. Sessions are only ended when the holder actually changed.

An email you already have

The trader record is reused and their stored name is left alone. The name in your request is dropped without comment, and trader_created comes back false. This is deliberate: an assignment call that renamed people on every retry would make your roster depend on whichever integration called last. Correcting a name is a separate, explicit act in the admin panel.

Calling this twice with the same email is safe. The second call writes nothing, ends no sessions, and answers 200 with assigned: false — so a retry after a timeout cannot disconnect the trader it just confirmed.

An account somebody else holds

This does not reassign. It refuses.

If the account already belongs to a different trader you get 409 account_assigned and nothing changes — including the trader you named, who is not created either. Unassign the account first, then assign it.

Handing an account straight from one trader to another in one call is the operation most likely to be a mistake: a wrong account number inside a loop would silently cut a paying customer off and give their account away. One extra call makes that intent explicit.

409 Conflict
{
  "error": {
    "code": "account_assigned",
    "message": "this account is already assigned to another trader — unassign it first (DELETE /v1/accounts/10000042/trader)",
    "current_trader_id": "3d21f0c2-…"
  }
}

current_trader_id is who holds it, so you can log the collision or unassign without a second lookup.

Unassign

DELETE/v1/accounts/{account}/traderAvailablescope · accounts:write

Takes the account back off whoever holds it. No body — the account number is the whole request.

curl
curl -X DELETE https://api.propexecutor.com/v1/accounts/10000042/trader \
  -H "Authorization: Bearer $PFX_KEY"
200 OK
{
  "account": 10000042,
  "trader_id": null,
  "unassigned": true,
  "previous_trader_id": "3d21f0c2-…",
  "sessions_revoked": true
}

Idempotent: an account nobody holds answers 200 with unassigned: false and previous_trader_id: null, not an error.

What assigning and unassigning actually do

Both end every live executor session on the account. That is the part that matters — it is what actually gets a former holder out of the terminal, rather than only changing a row.

The password is not rotated

Neither call changes the executor password, and the account number and server never change. So a previous holder who wrote the password down can log in again and keep trading an account they no longer hold.

Rotating is a separate, deliberate call — credentials — because whoever rotates has to see the new password to relay it to the new holder. A full handover is unassign → rotate → assign.

Neither call touches the frozen rule set version, the starting balance, the trade history or the account’s status. Assigning a trader to a breached or closed account is allowed — it is bookkeeping, and it does not put the account back in play.

Archive

POST/v1/accounts/{account}/archivePlannedscope · accounts:write

Takes the account out of circulation and revokes its sessions. The record, its trades and its history stay readable. No credit is returned.

Traders

GET/v1/tradersPlannedscope · accounts:read
POST/v1/tradersPlannedscope · accounts:write

A trader is a name and an email you create so an account has somewhere to hang. Creating one sends nothing, grants nothing and creates no login — traders have no account with us.

You rarely need these: assigning an account creates the trader for you when the email is new. These are for reading your roster and for creating a trader ahead of having an account to give them.

curl
curl -X POST https://api.propexecutor.com/v1/traders \
  -H "Authorization: Bearer $PFX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Jordan Ellis","email":"jordan@example.com"}'

Trade history

GET/v1/accounts/{account}/tradesAvailablescope · accounts:read

Every position the account has ever taken, open and closed, newest first, cursor-paginated. An open position has closed_at and realized_pnl of null.

iduuid
The position.
ticketinteger
The deal’s display number, unique across the platform — what a trading terminal would call the ticket. Use it in anything a person reads; use id to join.
instrumentstring
e.g. EURUSD, XAUUSD, BTCUSD.
sidestring
long or short.
sizenumber
In lots.
entry_pricenumber
Fill price.
exit_pricenumber | null
Fill price on close.
opened_attimestamp
When it filled.
closed_attimestamp | null
When it closed. Null while open.
realized_pnlnumber | null
Profit or loss booked on close. Null while open.

Paginated because an account traded hard for a year is tens of thousands of rows. If you want every aggregate over the whole ledger in one response instead of walking it, that is the account report — it computes the totals server-side rather than making you sum pages.

Open positions

GET/v1/accounts/{account}/positionsAvailablescope · accounts:read

What the account holds right now, each marked to the latest price. Not paginated: how many positions an account may hold at once is a rule, and an account with no such rule is still bounded by its margin.

200 OK
{
  "data": [{
    "id": "50ac559c-…",
    "trading_account_id": "4592a6d9-…",
    "instrument": "USDCAD",
    "side": "short",
    "size": 0.03,
    "entry_price": 1.39153,
    "opened_at": "2026-09-15T17:21:50Z",
    "stop_loss": null,
    "take_profit": null,
    "margin_used": 30,
    "floating_pnl": -53.4,
    "mark_price": 1.41675
  }],
  "next_cursor": null
}
floating_pnlnumber
The position marked to mark_price. Derived on every read, never stored.
mark_pricenumber
The price floating_pnl was computed at.
margin_usednumber
Capital this position ties up under the account's leverage, fixed at the price it opened at.
stop_lossnumber | null
Where it closes itself. Null when unset — distinguishable from a stop at zero.

This is a live view, not a ledger read

Positions, their floating P&L and their margin are derived from the current price and held in memory — the same view the trader sees in their own terminal. Two of our instances serve requests and reconcile with the database every 10 seconds, so a position opened a moment ago may take that long to appear on every read.

For anything you are going to reconcile or report on, read trades instead: that is the ledger, and it is exact.

Resting orders

GET/v1/accounts/{account}/ordersAvailablescope · accounts:read

Limits and stops that have been placed and have not triggered. Same live, in-memory source as positions, and the same caveat.

order_typestring
Which kind of resting order it is.
trigger_pricenumber
The price that turns it into a position.
required_marginnumber
What it would tie up if it triggered at its trigger price. Informational — it reserves nothing, and the real requirement is recomputed at the actual fill price.
statusstring
Resting orders are pending; a triggered one becomes a position and leaves this list.

Rule flags

GET/v1/accounts/{account}/rule-flagsAvailablescope · accounts:read

Rules the account tripped whose action is flag rather than breach: worth a look, trading continued. Unlike a breach these accumulate, so each carries an occurrence count and a first/last seen time.

200 OK
{
  "data": [{
    "rule_id": "r3",
    "rule_key": "max_lot_per_order",
    "reason": "Order of 2.50 lots exceeds the 2.00 lot guidance",
    "occurrences": 4,
    "first_seen_at": "2026-09-14T11:02:10Z",
    "last_seen_at": "2026-09-27T08:41:55Z"
  }],
  "next_cursor": null
}

This is the answer to “why is breaches empty when I know something happened” on the account report: a flag is not a breach, and the two are reported separately on purpose.