# Trading accounts

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

Source: https://www.propexecutor.com/docs/accounts
Whole reference in one file: https://www.propexecutor.com/doc.md

A trading account is one simulated account running on your executor. It is created against an [account type](https://www.propexecutor.com/docs/concepts), 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/accounts` · **Available** · scope `accounts:read`

Newest first, [cursor-paginated](https://www.propexecutor.com/docs/pagination).

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

### Each row

| Field | Type | Description |
| --- | --- | --- |
| `id` | uuid | Internal identifier. Stable forever. |
| `account` | integer | The 8-digit number. What your trader types into the executor, and what every path on this page takes. |
| `account_type_id` | uuid | The account type this was provisioned against. |
| `account_type_name` | string | Its name, e.g. `$10K Standard`. |
| `trader_id` | uuid \| null | The trader holding it, or null if it is provisioned and unassigned. |
| `trader_email` | string \| null | That trader's email, for display. |
| `status` | string | `active`, `breached`, `passed` or `archived`. |
| `status_reason` | string \| null | Why the rule engine ended the challenge — which rule, and the numbers. |
| `status_changed_at` | timestamp \| null | When that happened. |
| `starting_balance` | number | What the account was funded with. |
| `created_at` | timestamp | When it was provisioned. |

## Read one account

`GET /v1/accounts/{account}` · **Available** · scope `accounts:read`

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

<!-- 200 OK -->
```json
{
  "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/accounts` · **Planned** · scope `accounts:write`

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

<!-- curl -->
```bash
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}'
```

| Field | Type | Description |
| --- | --- | --- |
| `account_type_id` | uuid · required | Which account type to provision. |
| `quantity` | integer | How many. Defaults to `1`, capped at `100` per request. |
| `trader_id` | uuid | 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](https://www.propexecutor.com/docs/idempotency) so a network retry cannot spend a second batch, and see [limits](https://www.propexecutor.com/docs/limits) for the `402` you get when you run out.

## Assign a trader

`PUT /v1/accounts/{account}/trader` · **Available** · scope `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 -->
```bash
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"}'
```

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | **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. |
| `email` | string | **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 -->
```json
{
  "account": 10000042,
  "trader": {
    "id": "3d21f0c2-…",
    "name": "Jordan Ellis",
    "email": "jordan@example.com"
  },
  "trader_created": true,
  "assigned": true,
  "sessions_revoked": true
}
```

| Field | Type | Description |
| --- | --- | --- |
| `trader_created` | boolean | True when this call created the trader record, false when it reused one you already had. |
| `assigned` | boolean | 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_revoked` | boolean | 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 -->
```json
{
  "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}/trader` · **Available** · scope `accounts:write`

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

<!-- curl -->
```bash
curl -X DELETE https://api.propexecutor.com/v1/accounts/10000042/trader \
  -H "Authorization: Bearer $PFX_KEY"
```

<!-- 200 OK -->
```json
{
  "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](https://www.propexecutor.com/docs/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}/archive` · **Planned** · scope `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/traders` · **Planned** · scope `accounts:read`

`POST /v1/traders` · **Planned** · scope `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](https://www.propexecutor.com/docs).

You rarely need these: [assigning an account](#assign) 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 -->
```bash
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}/trades` · **Available** · scope `accounts:read`

Every position the account has ever taken, open and closed, newest first, [cursor-paginated](https://www.propexecutor.com/docs/pagination). An open position has `closed_at` and `realized_pnl` of `null`.

| Field | Type | Description |
| --- | --- | --- |
| `id` | uuid | The position. |
| `ticket` | integer | 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. |
| `instrument` | string | e.g. `EURUSD`, `XAUUSD`, `BTCUSD`. |
| `side` | string | `long` or `short`. |
| `size` | number | In lots. |
| `entry_price` | number | Fill price. |
| `exit_price` | number \| null | Fill price on close. |
| `opened_at` | timestamp | When it filled. |
| `closed_at` | timestamp \| null | When it closed. Null while open. |
| `realized_pnl` | number \| 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](https://www.propexecutor.com/docs/account-report) — it computes the totals server-side rather than making you sum pages.

## Open positions

`GET /v1/accounts/{account}/positions` · **Available** · scope `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 -->
```json
{
  "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
}
```

| Field | Type | Description |
| --- | --- | --- |
| `floating_pnl` | number | The position marked to mark_price. Derived on every read, never stored. |
| `mark_price` | number | The price floating_pnl was computed at. |
| `margin_used` | number | Capital this position ties up under the account's leverage, fixed at the price it opened at. |
| `stop_loss` | number \| 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](#trades) instead: that is the ledger, and it is exact.

## Resting orders

`GET /v1/accounts/{account}/orders` · **Available** · scope `accounts:read`

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

| Field | Type | Description |
| --- | --- | --- |
| `order_type` | string | Which kind of resting order it is. |
| `trigger_price` | number | The price that turns it into a position. |
| `required_margin` | number | 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. |
| `status` | string | Resting orders are pending; a triggered one becomes a position and leaves this list. |

## Rule flags

`GET /v1/accounts/{account}/rule-flags` · **Available** · scope `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 -->
```json
{
  "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](https://www.propexecutor.com/docs/account-report): a flag is not a breach, and the two are reported separately on purpose.
