# Trades

> List, read, create, edit, archive trades; manage screenshots, tags, and confirmations on a trade.
> Source: https://help.tradavity.com/api-docs/trades
> Category: Resources

---

Read trades with filters and pagination, fetch full detail, attach screenshots / tags / confirmations, and create / update / archive trades through the write endpoints below.

## GET /trades

**Scope:** `read:trades`

List your trades, newest first, with cursor pagination. Default 50 per page, max 200.

| Param | Type | Description |
| --- | --- | --- |
| `from` | date | YYYY-MM-DD or ISO 8601, in your user timezone. |
| `to` | date | Same. |
| `account` / `account_id` | int or string | Either alias works. `account_id` (int) or account name (case-insensitive). |
| `symbol` | string | Exact match, case-insensitive (e.g. `NQ`, `MNQ`, `EURUSD`). |
| `direction` | enum | `long` or `short`. |
| `strategy` / `strategy_id` | string or int | Either alias works. Pass an integer to filter by id; a string to match strategy name (case-insensitive). |
| `setup` | string | Setup name, case-insensitive. |
| `grade` / `trade_quality_grade` | enum | Either alias works. `A+`, `A`, `B`, `C`, `F`. |
| `tag` | string | Tag name (any tag on the trade matches). |
| `outcome` | enum | `win`, `loss`, `breakeven` (uses your account’s BE offsets). |
| `pnl_min` / `net_pnl_gte` | decimal | Either alias works. Minimum net P&L. |
| `pnl_max` / `net_pnl_lte` | decimal | Either alias works. Maximum net P&L. |
| `is_copy` | bool | true to only return copy-trades, false to exclude them. |
| `include_archived` | bool | Default false; archived trades are hidden. |
| `limit` | int | 1-200, default 50. |
| `cursor` | string | Opaque token from a previous response’s `meta.next_cursor`. |

Each trade in the response includes `trade_number`, `trade_date`, `symbol`, `direction`, `net_pnl`, `account`, `grade`, `strategy`, `setup`, `top_tag`, `is_copy`.

## GET /trades/{trade_number}

**Scope:** `read:trades` (also `read:screenshots` to include the screenshots array)

Full trade detail. The response includes:

- **Identity:** `trade_number`, `trade_date`, `symbol`, `asset_type`, `direction`, `quantity`, `multiplier`
- **Risk markers:** `sl_price`, `tp_price`, `rr_expected`, `rr_realized`
- **P&L:** `gross_pnl`, `fees`, `net_pnl`, `total_points`
- **Context:** `holding_time`, `market_condition`, `trading_session`, `volume`, `news_events`
- **Subjective:** `grade`, `emotional_state`, `bias`, `confidence_level`, `setup_quality`, `exit_type`
- **Notes:** `thought_process`, `mistakes_made`, `learning_notes`, `general_notes`
- **Relations:** `account` (id+name+currency), `strategy` (name), `setup` (name)
- **Copy info:** `is_copy`, `is_copy_source`, `source_trade_number`
- **Nested arrays:** `executions[]`, `tags[]`, `confirmations[]`, `screenshots[]`, `protocol_responses[]`
- **Audit:** `created_at`, `updated_at`

Copy-trade aware: shared data (confirmations, screenshots, protocol responses) is resolved through the source trade automatically. The `trade_number` is the per-user counter shown in the journal UI — not an internal ID.

**`protocol_responses[]`** (requires `read:protocols` scope on the key — omitted otherwise) returns at most one entry per protocol type (`pre_trade`, `in_trade`, `post_trade`). Each row carries `response_id`, `protocol_id`, `protocol_type`, `protocol_name`, `status`, `completion_pct`, `completed_at`, and a `source` field indicating `"trade"` (saved directly on this trade) or `"source"` (inherited from the copy’s source trade). For per-item answers, fetch the response details via `GET /protocols/{protocol_id}/responses?trade_number={n}`.

## GET /trades/{trade_number}/screenshots

**Scope:** `read:screenshots`

List screenshot metadata for a trade. Each entry has a `download_url` for the binary endpoint.

## GET /trades/{trade_number}/screenshots/{screenshot_id}

**Scope:** `read:screenshots`

Stream the screenshot file. Returns binary bytes with the appropriate `Content-Type` (image/png, image/jpeg, application/pdf). Cached for 5 minutes (screenshots are immutable once uploaded).

---

## POST /trades

**Scope:** `write:trades`

Create a single trade row. Returns the full trade detail. Out of scope at create time: tags, confirmations, screenshots, executions, copy-trade duplication. Use the dedicated endpoints below (`PUT /trades/{n}/tags`, `PUT /trades/{n}/confirmations`, `POST /trades/{n}/screenshots`, `PUT /trades/{n}/executions`) to attach related data after the create.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `account_id` | int | yes | Must be an account you own and not archived. |
| `trade_date` | ISO 8601 datetime | yes | Rejected if > tomorrow or < year 2000. |
| `symbol` | string (1–50) | yes | Stored uppercased. |
| `direction` | enum | yes | `long` or `short`. |
| `net_pnl` | decimal | yes | ±999,999,999,999,999.99. The server does not compute this from gross/fees. |
| `gross_pnl` | decimal | no | Defaults to `net_pnl + fees`. |
| `fees` | decimal | no | Defaults to 0. |
| `quantity` | decimal ≥ 0 | no | Up to 9,999,999,999.99 (DECIMAL(18,8); upper bound capped one digit below the theoretical column max because PHP doubles can’t represent the full 8-decimal mantissa exactly). |
| `multiplier` | decimal | no | Defaults to 1.0. Up to ±99,999,999.99. |
| `asset_type` | string | no | Defaults to `futures`. Must be a known type (`futures`, `forex`, `stocks`, `options`, `crypto`). |
| `asset_config` | object | no | Asset-type-specific config stored as JSON. Forex: `{ "lot_type": "standard"\|"mini"\|"micro", "pip_value": number }`. Options: `{ "option_type": "call"\|"put", "strike_price": number, "expiration": "YYYY-MM-DD" }`. Ignored for futures/stocks/crypto. Per-field validation runs against the same schema the website uses; bad fields return `400 validation_error` with `error.details.asset_config_errors`. |
| `sl_price` / `tp_price` | decimal | no | Stop-loss and take-profit prices. Must be strictly > 0 (zero / negative rejected with `400`; pass `null` to clear). DECIMAL(18,8) on the column — the API caps at **9,999,999,999.99** on the high end and **0.00000001** on the low end (anything below the floor would silently round to 0 in the DB). |
| `total_points` | decimal | no | ±999,999,999,999,999.99. |
| `rr_expected` / `rr_realized` | decimal | no | ±99,999,999.99. |
| `holding_time` | int (seconds) | no | 0 to 2,147,483,647. |
| `market_condition` | enum | no | `trending`, `ranging`, `choppy`, `breakout`. |
| `trading_session` | enum | no | `asian`, `london`, `newyork`, `overlap`. |
| `trade_quality_grade` | enum | no | `A+`, `A`, `B`, `C`, `F`. **Note:** the response serializer renames this field to `grade` on the trade object — you POST `trade_quality_grade` and read it back as `trade.grade`. |
| `volume` | enum | no | `very_low`, `low`, `below_average`, `average`, `above_average`, `high`, `very_high`. |
| `emotional_state` | string | no | Must match an entry in your `/emotions` catalog (the same list the journal UI dropdown reads). Use `GET /api/v1/emotions` for the valid values; add custom emotions via the website’s manage-emotions modal. |
| `bias` | enum | no | `Bullish`, `Bearish`, `Neutral` (case-sensitive). Matches the journal UI dropdown. |
| `confidence_level` | int 1–10 | no | Database constraint enforces the range. |
| `exit_type` | enum | no | `Take Profit`, `Stop Loss`, `Trailing Stop`, `Partial / Scale Out`, `Breakeven`, `Time-Based`, `Manual` (case-sensitive). Matches the journal UI dropdown so the value renders back in the edit form. |
| `setup_quality` | string ≤ 100 | no | Free-form. |
| `news_events` | string ≤ 65535 | no | Free-form text. |
| `strategy_id` | int | no | Must be a strategy you own. If account-scoped, must match this account; if group-scoped, this account must be a group member. |
| `setup_id` | int | no | Must belong to `strategy_id`. |
| `thought_process` / `mistakes_made` / `learning_notes` / `general_notes` | string ≤ 65535 | no | Plain text. The journal UI applies its own HTML sanitization for rich-text input; the API stores whatever you send verbatim. |

```bash
curl -X POST https://app.tradavity.com/api/v1/trades \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": 192,
    "trade_date": "2026-05-10T14:32:00Z",
    "symbol": "MNQ",
    "direction": "long",
    "quantity": 1,
    "multiplier": 2,
    "net_pnl": 50.0,
    "fees": 1.24,
    "trade_quality_grade": "A",
    "general_notes": "Clean breakout, executed plan."
  }'
```

*Returns:* `201 Created` with `{ data: { trade: {...} } }`. Use `data.trade.trade_number` in subsequent calls (chain tags / confirmations / screenshots).

*Plan limits:* POST /trades respects the same daily/monthly trade caps the website enforces. If your plan’s trade cap is exhausted, the API returns `429 limit_reached` with `error.details.usage` showing day/month counters. Pro plans return 429 only on hard caps (Free plans hit a soft daily/monthly cap).

## PATCH /trades/{trade_number}

**Scope:** `write:trades`

Update qualitative fields on an existing trade. Send only the fields you want to change — everything else is left alone.

Editable fields: notes (4 separate fields: `thought_process`, `mistakes_made`, `learning_notes`, `general_notes`), `trade_quality_grade`, `emotional_state`, `bias`, `confidence_level`, `market_condition`, `trading_session`, `exit_type`, `setup_quality`, `news_events`, `volume`, `strategy_id`, `setup_id`, `sl_price`, `tp_price`.

**SL / TP:** `sl_price` and `tp_price` are per-trade (not propagated to copies, mirroring the website edit flow). Send a strictly-positive number to set, `null` to clear. Same DECIMAL(18,8) bounds as POST (0.00000001 floor, 9,999,999,999.99 ceiling). Zero, negative, or sub-precision values return `400`.

> **Copy-trade propagation**
> If the trade is part of a copy-trade decision, the fields `strategy_id`, `setup_id`, and `trade_quality_grade` are propagated to the source trade and every copy. Notes, emotional state, bias, and confidence stay per-trade. The response includes `data.propagated: true/false`.
> 
>  Account-scoped strategies are validated per copy: a strategy bound to `account_id=72` applied to a copy on `account_id=73` will null out `strategy_id` and `setup_id` on that copy, not error out.

Editing the executions, screenshots, tags, or confirmations of a trade is done via dedicated endpoints (below). Core fields like trade_date, symbol, direction, and PnL cannot be edited through PATCH; create a new trade and archive the old one if the original was wrong.

```bash
curl -X PATCH https://app.tradavity.com/api/v1/trades/581 \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "trade_quality_grade": "A+",
    "emotional_state": "calm",
    "general_notes": "Trade went exactly to plan."
  }'
```

## DELETE /trades/{trade_number}

**Scope:** `write:trades`

Soft-delete (archive) a trade. The row stays in the database with `ret_archived = 1`. Recovery is via the data-retention UI within 30 days; after that it is hard-purged by cron.

Optional query param `including_copies=true` cascades the archive to every copy of the same decision, but only when the target is the copy *source*. If you pass `including_copies=true` on a copy that is not a source, you get `400 validation_error`.

The 204 response includes two informational headers:

- `X-Trades-Archived`: how many trades the call archived (1 unless cascading).
- `X-Copies-Archived`: how many copies were cascaded (0 unless `including_copies=true` was set on a source).

Re-deleting an already-archived trade returns `404 not_found` with the message “Trade is already archived.”

```bash
curl -X DELETE "https://app.tradavity.com/api/v1/trades/581?including_copies=true" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE"
```

## POST /trades/{trade_number}/screenshots

**Scope:** `write:screenshots`

Multipart upload. Attaches a screenshot to a trade. Copy trades store screenshots on the source — uploading to a copy actually writes the file under the source’s account directory and is visible from every copy via GET.

| Form field | Type | Required | Notes |
| --- | --- | --- | --- |
| `file` | file | yes | JPEG, PNG, or PDF. Max 10 MB. Both extension and MIME type are checked. |
| `label` | string ≤ 100 | no | Caption for the screenshot. |
| `sort_order` | int | no | Defaults to one past the highest existing sort_order on the trade. |

Storage is gated two ways: the account-wide quota (your plan’s total file budget) and a per-trade screenshot count cap. Free plans: 1 screenshot per trade. Pro plans: 3 screenshots per trade. Either ceiling returns `413 limit_reached` with `error.details` showing which cap fired.

The Idempotency-Key for screenshot uploads hashes the file’s SHA-256 plus the label and sort_order — a true retry of the same file with the same key returns the cached response. A different file under the same key returns `409 idempotency_conflict`.

JPEG and PNG uploads are run through an image optimizer; the response includes `optimized: true` and `savings_bytes` when compression succeeded. PDFs are stored as-is.

```bash
curl -X POST "https://app.tradavity.com/api/v1/trades/581/screenshots" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@chart.png" \
  -F "label=Entry breakout"
```

*Returns:* `201 Created`. Body `data` contains `screenshot_id`, `trade_number`, `file_size`, `original_size`, `file_type`, `width`, `height`, `label`, `sort_order`, `uploaded_at`, `download_url`, plus `optimized` and `savings_bytes` when compression ran.

## DELETE /trades/{trade_number}/screenshots/{screenshot_id}

**Scope:** `write:screenshots`

Soft-delete a screenshot. The on-disk file stays; storage quota is freed immediately. Re-deleting an archived screenshot returns `404`.

```bash
curl -X DELETE "https://app.tradavity.com/api/v1/trades/581/screenshots/3300" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE"
```

## PUT /trades/{trade_number}/tags

**Scope:** `write:tags`

Replace the entire tag set on a trade. Pass an empty array to clear all tags.

Tags are **not** propagated to copies — each copy keeps its own tag set. If you want the same tags on every copy of a decision, call this endpoint on each copy.

```bash
curl -X PUT "https://app.tradavity.com/api/v1/trades/581/tags" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"tag_ids": [19, 109, 804]}'
```

Cap of 100 tags per trade. Cross-user `tag_ids` return `400 validation_error` with `error.details.unknown_tag_ids` listing the offenders.

## PUT /trades/{trade_number}/confirmations

**Scope:** `write:trades`

Replace the confirmation response set. Confirmations are stored on the source trade — setting them on a copy mutates the source’s rows, and a GET on any copy reads from the source.

```bash
curl -X PUT "https://app.tradavity.com/api/v1/trades/581/confirmations" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "confirmations": [
      {"confirmation_id": 215, "was_present": true,  "notes": "clean entry"},
      {"confirmation_id": 216, "was_present": false}
    ]
  }'
```

Each item must include `confirmation_id` (int). `was_present` (bool) is optional and defaults to `true` when omitted. Optional `notes` is a string up to 65,535 bytes. Cap of 200 rows per trade. Cross-user `confirmation_id` values are rejected with `400 validation_error` and `error.details.unknown_confirmation_ids`.

## GET /trades/{trade_number}/executions

**Scope:** `read:trades`

Returns every entry + exit fill on the trade. Each row carries `execution_id`, `type` (`entry` or `exit`), `price`, `quantity`, `execution_time` (ISO 8601 UTC or `null`), and `sort_order`. Auto-synced trades will already have populated executions from the broker; manually-created trades have an empty list until you PUT some.

## PUT /trades/{trade_number}/executions

**Scope:** `write:trades`

Replace the full execution list on a trade in one call. Mirrors the website’s edit-trade flow (`features/journal/jrnl_update_trade.php` via `jrnl_updateExecutions`): existing rows are deleted and the new set is inserted inside one transaction.

Executions are **per-trade**, not shared via `source_trade_id` — each copy in a copy-trade decision carries its own fills (matches the website edit form).

```bash
curl -X PUT "https://app.tradavity.com/api/v1/trades/586/executions" \
  -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "entries": [
      {"price": 18000.25, "quantity": 1, "execution_time": "2026-05-11T14:30:00Z"},
      {"price": 18002.50, "quantity": 1, "execution_time": "2026-05-11T14:31:15Z"}
    ],
    "exits": [
      {"price": 18010.00, "quantity": 2, "execution_time": "2026-05-11T14:45:00Z"}
    ]
  }'
```

Body fields:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `entries` | array | at least one of `entries`/`exits` | Entry fills. Each item: `{price, quantity, execution_time?, sort_order?}`. |
| `exits` | array | at least one of `entries`/`exits` | Exit fills. Same shape as entries. |
| `price` | decimal > 0 | yes | Bounded by DECIMAL(18,8). API caps at **9,999,999,999.99** on the high end (one digit below the theoretical column max, kept in float-safe range) and **0.00000001** on the low end (anything smaller would silently round to 0 in the DB). |
| `quantity` | decimal > 0 | yes | Same bound as price. |
| `execution_time` | ISO 8601 datetime | no | UTC. Same format as `trade_date`. `null` or omitted is allowed. |
| `sort_order` | int | no | Defaults to the array index inside its group. |

Cap of 200 total executions per trade (entries + exits combined). Sending a body with neither `entries` nor `exits` returns `400 validation_error` — pass `{"entries": [], "exits": []}` explicitly to clear everything. The 200 OK response returns the freshly-read list with the new `execution_id`s assigned by the database.

**Concurrent-write semantics.** Each PUT replaces the full execution set inside one transaction (DELETE + INSERT). N concurrent PUTs against the same trade serialize at the DB and resolve last-write-wins; every call receives `200 OK` with the body it tried to write echoed back. Subsequent reads will only see the last commit. If you need read-after-write guarantees, send sequentially or use the same `Idempotency-Key` so the second call replays the first’s cached response instead of overwriting.

**Field constraints:**

- `price` and `quantity` are stricter than the website’s internal save: zero, negative, or sub-precision values return `400` rather than being silently dropped. Both bounded by DECIMAL(18,8) — API caps at **9,999,999,999.99** (float-safe ceiling) and **0.00000001** (DB precision floor).
- `execution_time` uses the same ISO 8601 envelope as POST `/trades` `trade_date`: rejected if more than 24h in the future or before `2000-01-01`.
- `sort_order` is accepted as-is from the caller (no normalization). Duplicate or sparse values tie-break by `execution_id` ASC on read — pass distinct integers if order matters.

**Out of scope:** this endpoint does *not* recompute the trade’s `net_pnl`, `gross_pnl`, or `fees` from the fills you write — those columns remain caller-authoritative. If editing executions implies a PnL change, PATCH the trade fields in a separate call.

**Response codes:**

| Status | Code | Meaning |
| --- | --- | --- |
| 200 | (no error code) | Replace succeeded. Body returns the new execution list. |
| 400 | `validation_error` | Bad body shape, missing required fields, or invalid value (price/qty ≤ 0, bad datetime, etc.). |
| 404 | `not_found` | No trade with that `trade_number` belonging to you, OR the trade is archived (`ret_archived = 1`). |
| 409 | `idempotency_conflict` | Same `Idempotency-Key` reused with a different body. |
