# Tradavity Public API · v1 — Full Reference > Source: https://help.tradavity.com/api-docs · Generated: 2026-05-21 > Base URL: `https://app.tradavity.com/api/v1` > Auth: Bearer token in the `Authorization` header, e.g. `Authorization: Bearer tvty_...` > Per-key scopes (`read:trades`, `write:trades`, etc.) — every endpoint lists the scope it needs. > Response envelope: `{"ok": true, "data": ...}` on success, `{"ok": false, "error": {"code": ..., "message": ..., "details": ...}}` on failure. > All write endpoints (POST/PATCH/PUT/DELETE) require a unique `Idempotency-Key` header per logical write. > Timestamps are ISO 8601 UTC. Dates in your user timezone unless noted. ## Contents - **Getting Started** - [Quickstart](#quickstart) — Introduction to the Tradavity Public API, with a zero-to-first-call walkthrough. - **Core Concepts** - [Authentication](#authentication) — How to authenticate Tradavity Public API requests with a Bearer token or X-API-Key header. - [Scopes](#scopes) — Scope catalog for the Tradavity Public API, with verb:resource semantics and the full read/write pairing. - [Rate Limits](#rate-limits) — Tradavity Public API rate limits: per-key, per-user, per-day, with X-RateLimit headers and Retry-After semantics. - [Idempotency](#idempotency) — How the Tradavity Public API Idempotency-Key header guarantees retry-safe writes. - [Conventions](#conventions) — Response envelope, datetime format, IDs, pagination, body size caps, and write-endpoint conventions for the Tradavity Public API. - [Error Codes](#error-codes) — Every Tradavity Public API error code, mapped to its HTTP status and meaning. - **Resources** - [Trades](#trades) — List, read, create, edit, archive trades; manage screenshots, tags, and confirmations on a trade. - [Strategies](#strategies) — Strategies and setups: list, read, create, update with embedded setups upsert, and delete. - [Tags](#tags) — Tags and tag categories: list, create, update, delete. Per-trade tag assignment lives on the Trades page. - [Goals](#goals) — Goals and period progress snapshots: list, read, create, update, delete. - [Protocols](#protocols) — Protocols (pre-trade checklists, EOD reviews, etc.) and their recorded responses. Full CRUD on both. - [Accounts](#accounts) — Trading accounts: list, read, create, edit, archive. Includes balance events, payouts, and snapshots. - [Copy Groups](#copy-groups) — Copy trading groups: list, read, create, update, delete; replace the member set. - [Auto-Sync](#auto-sync) — Auto-sync connection status, sync history, and manual sync trigger for broker integrations. - **Reports & Calendar** - [Stats](#stats) — Aggregate stats: summary (totals + decisions), by-symbol, by-strategy. All accept the same date and account filters. - [Calendar](#calendar) — Daily P&L over a date range, grouped in your user timezone. - **Catalog** - [Catalog](#catalog) — Identity (/me), usage, brokers, instruments, prop firm presets, scopes, emotions. All read-only. - **Guides** - [Worked Example: AI Agent Journaling a Trade](#worked-example-ai-agent-journaling-a-trade) — Four chained API calls to fully journal a trade — create, tag, screenshot, protocol response. - [Python: Idempotent Client Helper](#python-idempotent-client-helper) — A drop-in Python helper that adds an Idempotency-Key to every write call automatically. --- # Quickstart > Introduction to the Tradavity Public API, with a zero-to-first-call walkthrough. > Source: https://help.tradavity.com/api-docs/quickstart > Category: Getting Started --- The Tradavity Public API gives you programmatic access to your trading journal data over HTTP. Use it to build custom dashboards, feed an AI agent with your trade history, automate journaling from a script, or sync data with external tools. The API is REST-style, returns JSON, authenticates with a Bearer token, and is per-user only — every key is bound to one account and can only see that account’s data. > **Read and write are both live** > Every resource has both read (GET) and write (POST/PATCH/PUT/DELETE) endpoints. Browse them by feature in the **Resources** sidebar section, or jump straight to [Trades](https://help.tradavity.com/api-docs/trades), [Accounts](https://help.tradavity.com/api-docs/accounts), [Goals](https://help.tradavity.com/api-docs/goals), [Protocols](https://help.tradavity.com/api-docs/protocols), and more. ## What you can do **Read:** - List and read trades with full filters (date range, symbol, account, strategy, grade, tag, P&L bounds, copy-trade flag), with cursor pagination across the full history. - Fetch a single trade in full detail — executions, tags, confirmations, screenshot metadata, copy-trade source. - Download screenshot files (JPG, PNG, PDF) attached to trades. - Read accounts, balance status, daily snapshots, balance events, payouts. - Read goals and their period-by-period progress snapshots. - Read protocols (pre-market checklists, EOD reviews) and recorded responses. - Read journal entries, strategies, setups, tags, copy groups, emotions, confirmation templates. - Read calendar P&L, aggregate stats summary, stats by strategy, stats by symbol. - Read catalog data: brokers, instruments, prop firm presets. - Read auto-sync connection status and sync history (no token material is ever exposed). **Write:** - Create / update / archive trades; chain tags, confirmations, and screenshot uploads onto them. - Create / update / delete journal entries, tags, tag categories, strategies + setups, goals, protocols + responses, accounts, copy groups + members. - Record manual balance events; request payouts. - Trigger a manual broker auto-sync. ## Base URL All v1 endpoints live under: ``` https://app.tradavity.com/api/v1 ``` The version (`/v1/`) is locked — we will never break this contract within v1. Future incompatible changes ship under a new version prefix. --- ## Quickstart Three steps from zero to a working API call. ### 1. Generate an API key In Tradavity, open **Settings → API** and click **+ New Key**. Give the key a name (e.g. *My AI Agent*), pick scopes, and optionally set an expiry. After clicking **Create**, your full key is shown **once** — copy it and store it somewhere safe (a password manager or secrets store). After you close the modal, only the prefix is visible. API keys start with `tvty_` followed by 48 hex characters. > **Treat keys like passwords** > Anyone with your key can read your journal data within its scopes. Never embed it in client-side code, mobile apps, or public repositories. Always store keys in environment variables or a secrets manager. Revoke a key from the settings page the moment you suspect it has been exposed. ### 2. Test the connection Hit `/me` — this returns the calling user, the key’s scopes, and the current rate-limit window. It works with any key and is the easiest smoke test. ```bash curl https://app.tradavity.com/api/v1/me \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" ``` You should get back a JSON object with `"ok": true` and your user, plan, preferences, and key info. ### 3. Make a real request Pull your last 50 trades: ```bash curl https://app.tradavity.com/api/v1/trades \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" ``` Or filter to a date range and account: ```bash curl "https://app.tradavity.com/api/v1/trades?from=2026-04-01&to=2026-04-30&account=My%20Account" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" ``` See the [Resources reference](https://help.tradavity.com/api-docs/category/resources) for the full list of endpoints per feature. --- Next reads: [Authentication](https://help.tradavity.com/api-docs/authentication) · [Idempotency](https://help.tradavity.com/api-docs/idempotency) · [Conventions](https://help.tradavity.com/api-docs/conventions). --- # Authentication > How to authenticate Tradavity Public API requests with a Bearer token or X-API-Key header. > Source: https://help.tradavity.com/api-docs/authentication > Category: Core Concepts --- Every request must carry your API key in one of two headers: ``` Authorization: Bearer tvty_YOUR_KEY_HERE ``` or, for clients that cannot set `Authorization`: ``` X-API-Key: tvty_YOUR_KEY_HERE ``` Keys passed via query string are **rejected** — they leak into proxy access logs. Authentication failures return HTTP `401` with one of these error codes: | Code | Reason | | --- | --- | | `unauthorized` | Missing, malformed, or invalid key | | `key_revoked` | Key was explicitly revoked in settings | | `key_expired` | Key reached its `expires_at` date | ### Key storage Keys are stored as a SHA-256 hash on our servers — we cannot show you the plaintext after you create the key. Lose it and you have to revoke and create a new one. Pre-existing keys created before 2026-05 are still valid but cannot be displayed in full anymore. ### Multiple keys per account You can have up to 25 active keys at a time. Each key has its own name, scopes, and rate-limit window. Use separate keys per integration (e.g. one for your AI agent, one for a mobile script, one for backups) so you can revoke them independently if anything leaks. Next: [Scopes](https://help.tradavity.com/api-docs/scopes). --- # Scopes > Scope catalog for the Tradavity Public API, with verb:resource semantics and the full read/write pairing. > Source: https://help.tradavity.com/api-docs/scopes > Category: Core Concepts --- This page lists every scope you can grant an API key. The live catalog is also available via `GET /api/v1/scopes`, and you pick scopes interactively when you create a key in **Settings → API**. Scopes restrict what a key can do. They follow a `verb:resource` pattern. `read` and `write` are independent — granting `write` does not imply `read` (this lets you create write-only audit loggers). Granular scope means a leaked key only exposes what you granted it. When creating a key in the settings UI you have three presets: - **Read everything** — all `read:*` scopes (recommended for AI agents that only consume data). - **Read + Write everything** — all scopes (recommended for personal automation). - **Custom** — pick exactly which read and write scopes to grant. The full scope catalog: | Read scope | Paired write scope | What it allows | | --- | --- | --- | | `read:trades` | `write:trades` | List + read trades, executions, tags, confirmations on a trade. Write: POST / PATCH / DELETE on trades, PUT on per-trade confirmations. | | `read:accounts` | `write:accounts` | List + read trading accounts. Write: POST + PATCH on accounts. | | `read:balance` | `write:balance` | Balance status, events, payouts, daily snapshots. Write: POST manual balance events and payout requests. | | `read:goals` | `write:goals` | Goals + period progress history. Full CRUD on write. | | `read:protocols` | `write:protocols` | Protocol templates + responses. Write: full protocol CRUD plus POST a recorded response. | | `read:tags` | `write:tags` | Tags + tag categories. Write: full CRUD on both, plus PUT the tag set on a trade. | | `read:strategies` | `write:strategies` | Strategies + setups + confirmation templates. Write: POST/PATCH/DELETE strategy with embedded setups upsert. | | `read:screenshots` | `write:screenshots` | Download screenshot files. Write: POST multipart upload, DELETE archive. | | `read:copy_groups` | `write:copy_groups` | Copy trading groups + members. Write: full CRUD on the group plus PUT the member set. | | `read:autosync` | `write:autosync` | Auto-sync connection status + sync history. Write: trigger a manual sync. | | `read:catalog` | — | Brokers, instruments, prop firm presets, emotions catalog. Granted automatically to every key. There is no `write:catalog` — catalog data is platform-managed. | `read:settings` and `write:settings` are reserved in the scope catalog for future-use settings endpoints (general / calculation / grouping prefs). They are grantable today so existing keys can keep working when those endpoints land. If a request needs a scope your key does not have, you get HTTP `403` with error code `forbidden_scope` and the required scope in `error.details.required_scope`. --- # Rate Limits > Tradavity Public API rate limits: per-key, per-user, per-day, with X-RateLimit headers and Retry-After semantics. > Source: https://help.tradavity.com/api-docs/rate-limits > Category: Core Concepts --- Limits are enforced two ways: **per-key** and **per-user**. The tighter window wins. Rate-limit info is on every successful response in `meta.rate_limit` and as headers: > **60-second sliding window** > The limit counts requests made in the last 60 seconds, not requests since a fixed reset point. The bucket refills *gradually* as old requests age out, not all at once. On `429`, `Retry-After` tells you when the **oldest request** in your window ages out — i.e. when **one** slot frees up, not when the full limit refills. After sleeping `Retry-After` seconds you can make roughly one more call before backing off again; the bucket recovers to full only after no requests have been made for a full minute. | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Max requests per minute for this key’s tier | | `X-RateLimit-Remaining` | Remaining requests in the current 60-second window | | `X-RateLimit-Reset` | Unix timestamp at which the current rolling window’s newest entry will age out (sliding-window upper bound) | | `X-RateLimit-Scope` | On 429 responses: `key`, `user`, or `key-daily` | | `Retry-After` | On 429 responses: seconds until **one** slot frees in the rolling window (not until the full limit refills) | Tier limits: | Tier | Per key / minute | Per key / day | Per user / minute (across all keys) | | --- | --- | --- | --- | | Free | 60 | 5,000 | 180 | | Pro | 300 | 50,000 | 900 | The per-user ceiling is roughly 3× the per-key ceiling so up to three keys on one account can all run full-throttle simultaneously without hitting the cross-key limit. Once you have more than three active keys, the per-user window becomes the binding constraint. There is also a pre-auth IP rate limit of **100 requests / minute** for unauthenticated traffic, returning `429 rate_limited` with `X-RateLimit-Scope: ip-preauth`. This blocks key-stuffing scanners before they hit the auth lookup. When you exceed a limit you get HTTP `429` with error code `rate_limited`. Sleep for `Retry-After` seconds and try again. Authenticated requests count against the limit whether they succeed or fail (so 4xx responses still tick the counter). Pre-auth 401s on totally invalid keys are not counted per-key (no key id is known yet); they are still logged for abuse forensics. --- # Idempotency > How the Tradavity Public API Idempotency-Key header guarantees retry-safe writes. > Source: https://help.tradavity.com/api-docs/idempotency > Category: Core Concepts --- Write requests (POST, PATCH, PUT, DELETE) accept an `Idempotency-Key` header so retries are safe: ```http POST /api/v1/trades Authorization: Bearer tvty_YOUR_KEY_HERE Idempotency-Key: my-script-2026-05-10-trade-1 Content-Type: application/json { ... trade body ... } ``` If the same key is reused within 24 hours with the same body, the server returns the cached response (and sets `X-Idempotent-Replay: true`) without re-executing the write. Reusing the same key with a *different* body returns HTTP `409 idempotency_conflict`. Concurrent identical requests are also safe: only one wins the “leader” slot and runs the side effect; the others poll briefly and return the leader’s response. So five parallel POSTs with the same Idempotency-Key produce one DB row plus four replays, never five rows. **Replays still count against your rate limit.** Each call — fresh or replayed — decrements `X-RateLimit-Remaining` by one. The replay path is cheap (no side effect, returns the cached body) but it is a billable request. Build your retry logic to back off on transport errors rather than blindly re-sending the same key in a tight loop. The key must be 1–64 characters of `[A-Za-z0-9_-]`. Use a UUID or a deterministic identifier from your client (e.g. `{user}-{operation}-{timestamp}`). ## Idempotency contract in detail Every write endpoint accepts an `Idempotency-Key` header. The contract is: - **Fresh key** → the side effect runs once. The full response is cached for 24 hours. - **Same key, identical body** → `200/201/204` with the cached response. The header `X-Idempotent-Replay: true` is set. The side effect does **not** run again. - **Same key, different body** → `409 idempotency_conflict`. Pick a fresh key. - **Five concurrent identical requests** → one wins the “leader” slot and runs the side effect. The other four poll for up to 30 seconds, then return the leader’s cached response. Net result: exactly one row inserted, all five callers get `201`. - Keys must be 1–64 characters of `[A-Za-z0-9_-]`. UUIDs are perfect. Deterministic strings like `{user}-{operation}-{2026-05-10}` also work. The 24-hour window starts at the original request time. After that, the same key can be reused for a fresh write. > **Always send Idempotency-Key on writes** > The server caches responses by `(api_key, Idempotency-Key)` for 24 hours. A retry of the same request returns the cached response without re-executing the side effect. A retry with a *different* body returns `409 idempotency_conflict`. Without this header, a network retry can double-create rows. --- # Conventions > Response envelope, datetime format, IDs, pagination, body size caps, and write-endpoint conventions for the Tradavity Public API. > Source: https://help.tradavity.com/api-docs/conventions > Category: Core Concepts --- The shape, headers, and parsing rules every endpoint follows. Read this before integrating; everything else assumes it. ## Response envelope Every response is JSON with this shape: ```json { "ok": true, "data": { ... resource-specific payload ... }, "meta": { "request_id": "req_a3b4c5d6e7f8a9b0", "rate_limit": { "tier": "pro", "limit": 300, "remaining": 297, "reset_at": "2026-05-10T15:30:00Z", "user_ceiling": 900, "user_remaining": 895 } } } ``` On error: ```json { "ok": false, "error": { "code": "validation_error", "message": "direction must be 'long' or 'short'.", "details": { ... optional field-level info ... } }, "meta": { "request_id": "req_a3b4c5d6e7f8a9b0" } } ``` Always check `ok` first. The HTTP status reflects the category (200/201/204/400/401/403/404/409/413/429/500). The `error.code` is a stable snake_case string — switch on this in your client, not the human-readable message. **Shape of `error.details` on `400 validation_error`:** - Per-field errors: `details.fields` is an object mapping each rejected input field to a short reason string. Example: `{"fields": {"confidence_level": "must be an integer 1-10"}}`. - Enum / reference rejections: the offending category lands directly on `details`. Examples: `details.allowed` (list of accepted values for an enum filter or field), `details.unknown_tag_ids` / `details.unknown_setup_ids` / `details.unknown_confirmation_ids` (the unrecognized ids you passed), `details.asset_config_errors` (per-key issues on a nested config object). ## Datetimes Response times are ISO 8601 UTC with a `Z` suffix: `2026-05-10T14:32:18Z`. Convert to your user’s timezone client-side. Use `YYYY-MM-DD` or ISO 8601 in date filters — the parser is lenient and accepts most reasonable formats, but stick to one of these for clarity. Whatever you send is interpreted in your user’s timezone (set in **Settings → Preferences**) and converted to UTC on the server. ## IDs Trades are identified by `trade_number` — a per-user counter (your first trade is `1`, the second is `2`, etc.) shown in the journal UI. The internal `trade_id` is never surfaced. Other resources use their numeric primary key (account_id, goal_id, protocol_id, etc.). ## Pagination - **Trades** use **cursor** pagination (sorted by trade date, newest first). Default page is 50, max is 200. The response’s `meta.next_cursor` — an opaque base64 token — goes into the next request as the `cursor` query param. `meta.has_more` tells you when to stop. - **All other list endpoints** use **offset** pagination via `limit` and `offset` query params. Default `limit` is 50 or 100 (per endpoint); the max is 200 for high-volume resources (balance events, payouts, protocol responses, autosync log) and 500 for static lists. The response includes `total_count` and `has_more`. - **Pure catalogs** (brokers, instruments, prop firm presets) return up to 500 rows; use the catalog filters if you hit the cap. ## Conventions for write endpoints - Body must be valid JSON when the endpoint accepts a body. Always set `Content-Type: application/json`; the server is currently lenient about the header but future hardening may make it strict, so do not rely on its absence working. - Maximum body size is 1 MB. Multipart uploads (screenshots) are exempt and capped separately at 10 MB per file. There is also a per-trade screenshot cap that depends on your plan — Free: 1 per trade, Pro: 3 per trade — exceeding it returns `413 limit_reached` with `error.details` showing the cap. - **Unknown fields are rejected.** The server validates the payload against an explicit allow-list per endpoint and returns `400 validation_error` with the offending field names if it sees anything unexpected. This catches typos early. - Datetime fields accept ISO 8601 in UTC (e.g. `2026-05-10T14:32:18Z`). For datetime fields, a date-only string like `2026-05-10` is also accepted and stored as midnight UTC. Date-only fields use `YYYY-MM-DD`. - Both raw NUL bytes (`\\x00`) inside JSON strings and escaped NUL sequences (`\u0000`) are silently stripped from incoming string values before validation. The request is not rejected for them. Non-printable control characters are stored as-is. - Numeric fields reject non-numeric strings: a value like `"quantity": "abc"` returns `400 validation_error` instead of being silently coerced. JSON numbers (`"quantity": 1`) and number-looking strings (`"quantity": "1"`) are both accepted. - POST returns `201 Created` with the created resource in `data`. PATCH and PUT return `200 OK`. DELETE returns `204 No Content` (no body). - Optimistic concurrency: there is no `If-Match` header today. Last write wins for concurrent PATCH on the same resource. Concurrent writes that violate a uniqueness constraint return `409 duplicate_name`. --- # Error Codes > Every Tradavity Public API error code, mapped to its HTTP status and meaning. > Source: https://help.tradavity.com/api-docs/error-codes > Category: Core Concepts --- | HTTP | Code | Meaning | | --- | --- | --- | | 400 | `validation_error` | Malformed input. `error.details` may include field-level info. | | 401 | `unauthorized` | Missing or invalid key, or the user account is disabled / suspended. | | 401 | `key_revoked` | Key was revoked. | | 401 | `key_expired` | Key passed its expiry date. | | 401 | `token_expired` | (Auto-sync) The broker token cannot be renewed. User must reconnect via the settings UI. | | 403 | `forbidden_scope` | Authentication OK but the key lacks the required scope. | | 404 | `not_found` | The resource does not exist or does not belong to you. Returned identically for both cases to prevent ID enumeration. | | 405 | `method_not_allowed` | Wrong HTTP method. `error.details.allowed` lists what is accepted. | | 409 | `idempotency_conflict` | Same Idempotency-Key reused with a different request body. | | 409 | `duplicate_name` | Tag, tag category, account, strategy, copy group: name already exists. | | 409 | `category_has_tags` | Cannot delete a tag category that still has tags. | | 409 | `conflict` | Resource-specific conflict (e.g. copy group member already in another group). | | 413 | `request_too_large` | Request body exceeds the size cap (1 MB JSON, 10 MB screenshot). | | 413 | `limit_reached` | Storage quota exceeded on a screenshot upload. | | 429 | `rate_limited` | Rate limit hit. Retry after `Retry-After` seconds. `X-RateLimit-Scope` indicates which window: `key`, `user`, `key-daily`, or `ip-preauth` (unauthenticated abuse). | | 429 | `limit_reached` | Subscription cap hit (e.g. account count, strategy count, Pro-only feature without a Pro plan). | | 500 | `internal_error` | Unexpected server-side problem. Include the `request_id` from `meta` when reporting. | | 502 | `broker_api_error` | (Auto-sync) Upstream broker API returned an error. Retry later. | | 503 | `deadlock_retry` | Two concurrent writes deadlocked. Retry after `Retry-After` (1 second). | --- # 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. | --- # Strategies > Strategies and setups: list, read, create, update with embedded setups upsert, and delete. > Source: https://help.tradavity.com/api-docs/strategies > Category: Resources --- Manage your trading strategies and their setups. The PATCH endpoint upserts setups in one transaction so trades stay linked to the same setup row across renames. ## GET /strategies **Scope:** `read:strategies` Your strategies with setup count. Optional filters: `account_id`, `copy_group_id`. ## GET /strategies/{id} **Scope:** `read:strategies` Single strategy with full nested setups, plus risk_rules / exit_rules / optimal_conditions JSON. ## GET /strategies/{id}/setups **Scope:** `read:strategies` Just the setups for one strategy. --- ## POST /strategies **Scope:** `write:strategies` Create a strategy with optional embedded setups in one transaction. Subscription gate: `bill_lim_canAddStrategy`; over the per-plan max returns `429 limit_reached`. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `name` | string (1–255) | yes | Unique per user. | | `description` | string | no | | | `color_tag` | hex | no | Defaults to `#1d4ed8`. | | `account_id` | int | no | Restrict the strategy to one account. Mutually exclusive with `copy_group_id`. | | `copy_group_id` | int | no | Restrict the strategy to one copy group. | | `risk_rules` | object | no | Free-form JSON; stored as-is. | | `exit_rules` | object | no | Free-form JSON. | | `optimal_conditions` | object | no | Free-form JSON. | | `setups` | array | no | Up to 50. Each item: `{name, description?, sort_order?, confirmations?}` — the optional `confirmations` array on each setup defines the per-setup confirmation checklist (up to 100 entries, each `{name, description?, sort_order?}`). | ```bash curl -X POST https://app.tradavity.com/api/v1/strategies \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "name": "Breakout Scalp", "description": "Bull / bear flag breaks on 5m.", "color_tag": "#ef4444", "setups": [ { "name": "Bull Flag Break", "sort_order": 0, "confirmations": [ {"name": "Pulled back to 9 EMA"}, {"name": "Volume spike on break"} ] }, {"name": "Bear Flag Break", "sort_order": 1}, {"name": "Range Breakout", "sort_order": 2} ] }' ``` ## PATCH /strategies/{id} **Scope:** `write:strategies` Update strategy fields and optionally replace the setups list. The `setups` array, if sent, becomes the canonical list: - Items with a `setup_id` matching an existing setup are **updated in place** — the row id is preserved so trades referencing it stay linked. - Items without `setup_id` are inserted fresh. - Existing setup_ids missing from the new list are **hard-deleted**; their confirmations cascade-delete via FK. - Foreign setup_ids (belonging to a different strategy) return `400 validation_error` with `error.details.unknown_setup_ids`. - **Nested `confirmations` on a setup:** when present, the API *diff-upserts* against the setup’s existing confirmations (matches the strategy builder UI). Pass an existing `confirmation_id` to update in place — that preserves the id, so per-trade `jrnl_trade_confirmations` rows (the `was_present` / `notes` answers on every trade that ever used this confirmation) stay intact. Confirmations that exist in the DB but are absent from your payload are deleted; those rows DO cascade-delete the related per-trade answers, same as the website builder. When the key is absent, existing confirmations are left untouched; `[]` clears them all. - Foreign `confirmation_id`s (belonging to a different setup) return `400 validation_error` with `error.details.unknown_confirmation_ids`. GET `/strategies/{id}` and GET `/strategies/{id}/setups` include the full `confirmations[]` array inside each setup so you can read back what you wrote in one request. GET `/strategies` (the list endpoint) returns only `setup_count`, not the nested setups — call the single-strategy endpoint for the tree. Pass `account_id: null` or `copy_group_id: null` to clear the corresponding scope. Setting one auto-clears the other. ## DELETE /strategies/{id} **Scope:** `write:strategies` Hard delete. Trades that reference the strategy have their `strategy_id` and `setup_id` set to NULL first. Setups + confirmations cascade-delete via FK. The 204 response includes `X-Trades-Unlinked` with the count of trades that lost their strategy reference. --- # Tags > Tags and tag categories: list, create, update, delete. Per-trade tag assignment lives on the Trades page. > Source: https://help.tradavity.com/api-docs/tags > Category: Resources --- Manage your tag catalog and tag categories. To assign tags to a specific trade, see [PUT /trades/{n}/tags](https://help.tradavity.com/api-docs/trades). ## GET /tag-categories **Scope:** `read:tags` Your tag categories. ## GET /tags **Scope:** `read:tags` Your tags. Optional filters: `category_id`, `type` (`mistake` or `custom`). --- ## POST /tag-categories **Scope:** `write:tags` Create a tag category. Body: `{ "name": string (1–50 chars) }`. Names must be unique within your account; duplicates return `409 duplicate_name`. ```bash curl -X POST https://app.tradavity.com/api/v1/tag-categories \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{"name": "Setup quality"}' ``` ## PATCH /tag-categories/{id} **Scope:** `write:tags` Rename. Body: `{ "name": string }`. ## DELETE /tag-categories/{id} **Scope:** `write:tags` Hard delete. Refuses with `409 category_has_tags` if any tag still belongs to it — move or delete the tags first. ## POST /tags **Scope:** `write:tags` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `category_id` | int | yes | Must be a category you own. | | `name` | string (1–100) | yes | Unique within your account. | | `color` | hex string | no | `#RRGGBB` format. Defaults to `#3b82f6`. | ```bash curl -X POST https://app.tradavity.com/api/v1/tags \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{"category_id": 6, "name": "A+ Setup", "color": "#10b981"}' ``` ## PATCH /tags/{id} **Scope:** `write:tags` Update any subset of `name`, `category_id`, `color`. Renaming to a name that already exists on another tag returns `409 duplicate_name`. ## DELETE /tags/{id} **Scope:** `write:tags` Hard delete. The `jrnl_trade_tags` rows referencing this tag cascade-delete via FK, so the tag is also removed from every trade. --- # Goals > Goals and period progress snapshots: list, read, create, update, delete. > Source: https://help.tradavity.com/api-docs/goals > Category: Resources --- Quantitative trading goals with period-by-period progress snapshots. ## GET /goals **Scope:** `read:goals` | Param | Type | Description | | --- | --- | --- | | `status` | enum | `active` (default), `paused`, `archived`, `all`. | | `account_id` | int | Filter to one account. | ## GET /goals/{id} **Scope:** `read:goals` Single goal definition. ## GET /goals/{id}/snapshots **Scope:** `read:goals` Period-by-period progress snapshots (target, actual, achieved, raw `snapshot_data`). Pagination via `limit` (max 365) and `offset`; date filter via `from` and `to`. --- ## POST /goals **Scope:** `write:goals` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `goal_name` | string (1–100) | yes | | | `metric` | string | yes | Valid metric key. Common values: `net_pnl`, `gross_pnl`, `max_drawdown`, `balance_target`, `win_rate`, `profit_factor`, `expectancy`, `rr_realized_avg`, `a_plus_rate`, `max_trades_per_day`, `protocol_completion_rate`. | | `target_value` | number | yes | Must be non-zero except for `max_drawdown` (where `0` means “no drawdown allowed”). Negative values are allowed where semantically meaningful (e.g. `net_pnl` as a loss floor). | | `direction` | enum | no | `at_least` or `at_most`. Defaults to the metric’s natural direction (`at_most` for `max_drawdown` and `max_trades_per_day`; `at_least` otherwise). Unknown values return `400` with `details.allowed`. | | `timeframe` | enum | no | `daily`, `weekly`, `monthly` (default), `quarterly`, `custom`. Unknown values return `400` with `details.allowed`. | | `custom_start` / `custom_end` | `YYYY-MM-DD` | only when `timeframe=custom` | Both required when `timeframe=custom`; otherwise sending either field returns `400` (not silently ignored). Each must be a valid date between `2000-01-01` and `2100-12-31`. `custom_end` must be on or after `custom_start`. | | `account_id` | int | no | Mutually exclusive with `copy_group_id`. | | `copy_group_id` | int | no | | ```bash curl -X POST https://app.tradavity.com/api/v1/goals \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "goal_name": "Monthly profit target", "metric": "net_pnl", "target_value": 5000, "timeframe": "monthly" }' ``` ## PATCH /goals/{id} **Scope:** `write:goals` Update `goal_name`, `metric`, `target_value`, `direction`, `timeframe` (with `custom_start`/`custom_end`), `status` (`active` / `paused` / `archived`), or `scope`. Validation rules (non-zero target, enum allow-lists, date format / range / ordering) are identical to POST — see the POST table above. To change the date window on an existing custom goal, send `timeframe: "custom"` together with the new `custom_start` and `custom_end` in the same PATCH (the helper only writes the dates when `timeframe` is in the body too). The `scope` field is a structured string: - `"account_42"` — bind to account 42. - `"group_7"` — bind to copy group 7. - `"all"` — global across all accounts. - `""` (empty) — follow the user’s active selection in the UI. ## DELETE /goals/{id} **Scope:** `write:goals` Hard delete. Period snapshots cascade-delete via FK. --- # Protocols > Protocols (pre-trade checklists, EOD reviews, etc.) and their recorded responses. Full CRUD on both. > Source: https://help.tradavity.com/api-docs/protocols > Category: Resources --- Pre-trade checklists, EOD reviews, weekly retrospectives. Each protocol has sections and items; recorded responses store per-item answers. ## GET /protocols **Scope:** `read:protocols` List protocol templates with section + item counts. | Param | Type | Description | | --- | --- | --- | | `type` | enum | `pre_market`, `pre_trade`, `in_trade`, `post_trade`, `end_of_day`, `weekly`. | | `active_only` | bool | Default false; pass `1` to exclude inactive templates. | ## GET /protocols/{id} **Scope:** `read:protocols` Single protocol with full nested sections + items. Each item carries its `item_type` (`checkbox`, `yes_no_na`, `rating`, `text`, `select`, `number`), label, options, and required flag. ## GET /protocols/{id}/responses **Scope:** `read:protocols` Paginated list of recorded responses to this protocol (metadata only). | Param | Type | Description | | --- | --- | --- | | `from` / `to` | datetime | Filter by completed_at. | | `trade_number` | int | Only responses linked to a specific trade. Copy-trade aware: if the trade is a copy, the response saved on its source trade is also returned (matches the website's protocol reader). | | `status` | enum | `completed` (default), `draft`, `all`. | | `limit` / `offset` | int | Pagination. | ## GET /protocols/{id}/responses/{response_id} **Scope:** `read:protocols` Single response with all per-item answers (each tied back to its `item_id`, `item_type`, `section_id`, and stored `value`). --- ## POST /protocols **Scope:** `write:protocols` Create a protocol (pre-trade checklist, EOD review, etc.) with embedded sections + items in one transaction. Subscription gate: `bill_lim_canAddProtocol` per type; over the cap returns `429 limit_reached`. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `type` | enum | yes | `pre_market`, `pre_trade`, `in_trade`, `post_trade`, `end_of_day`, `weekly`. | | `name` | string (1–100) | yes | | | `description` | string ≤ 500 | no | | | `set_as_default` | bool | no | Only applied when no other active protocol of this type exists for you. | | `sections` | array (1–8) | yes | Each: `{name, items}`. | Each section’s `items` array (1–15 items) holds objects: | Item field | Type | Notes | | --- | --- | --- | | `item_type` | enum | `checkbox`, `yes_no_na`, `rating`, `text`, `select`, `number`. | | `label` | string (1–200) | | | `options` | array of strings | Required for `item_type=select`: 2–10 non-empty entries, each ≤ 80 chars. | ```bash curl -X POST https://app.tradavity.com/api/v1/protocols \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "type": "pre_trade", "name": "Pre-Trade Checklist", "sections": [ { "name": "Pre-flight", "items": [ {"item_type": "checkbox", "label": "Charts are loaded"}, {"item_type": "yes_no_na", "label": "News today?"}, {"item_type": "select", "label": "Bias", "options": ["long","short","neutral"]} ] }, { "name": "Risk", "items": [ {"item_type": "rating", "label": "Conviction (1-5)"}, {"item_type": "number", "label": "R risk"} ] } ] }' ``` ## PATCH /protocols/{id} **Scope:** `write:protocols` Update `name`, `description`, `is_active`, `is_default`. Sections + items are **not** editable here — create a new protocol if you need to restructure (preserves the original protocol’s response history). Setting `is_default: true` clears the default flag on every other protocol of the same type for this user. Setting `is_default: false` only clears it on this protocol. ## DELETE /protocols/{id} **Scope:** `write:protocols` Hard delete. Sections, items, recorded responses, and per-item answers all cascade-delete. ## POST /protocols/{id}/responses **Scope:** `write:protocols` Record a response to a protocol. Optionally link it to a trade. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `item_values` | object | yes | Map of `item_id` → value. Values: checkbox `"1"`/`"0"`; yes_no_na `"yes"`/`"no"`/`"na"`; rating int 1–5; number `"123.45"`; select one of the configured options; text any string. | | `trade_number` | int | no | Link to a trade (resolves to source for copies). | | `status` | enum | no | `completed` (default) or `draft`. Completed responses must include all required items. | ```bash curl -X POST https://app.tradavity.com/api/v1/protocols/80/responses \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "trade_number": 581, "item_values": { "640": "1", "641": "yes", "642": "long", "643": 4, "644": "1.5" } }' ``` Cross-protocol `item_id` values are rejected with `400 validation_error` and `error.details.unknown_item_ids`. Required items missing values on a `completed` response return `400` with `error.details.missing`. Per-item type/range failures (rating outside 1–5, non-numeric number, select value not in options) return `400` with `error.details.item_value_errors` mapping `item_id` to the reason. ## PATCH /protocols/{id}/responses/{response_id} **Scope:** `write:protocols` Edit a previously-recorded response — useful for promoting a draft to `completed`, fixing a typo, or attaching a trade after the fact. Accepted fields: | Field | Type | Notes | | --- | --- | --- | | `status` | enum | `draft` or `completed`. Transitioning to `completed` stamps `completed_at` if not already set. | | `trade_number` | int or null | Attach or detach a trade link. Resolves to source for copies, same as POST. | | `item_values` | object | Replaces the entire per-item answer set. Same validation as POST (type/range, required-when-completed, options-allowlist). | Returns the full re-read response (same shape as `GET /protocols/{id}/responses/{response_id}`). ## DELETE /protocols/{id}/responses/{response_id} **Scope:** `write:protocols` Hard-delete the response and its per-item rows. Returns `204 No Content`. Re-DELETE returns `404 not_found`. --- # Accounts > Trading accounts: list, read, create, edit, archive. Includes balance events, payouts, and snapshots. > Source: https://help.tradavity.com/api-docs/accounts > Category: Resources --- Trading accounts with balance status, manual balance events, payouts, and daily snapshots. ## GET /accounts **Scope:** `read:accounts` List your trading accounts. | Param | Type | Description | | --- | --- | --- | | `include_archived` | bool | Default false. Include archived accounts. | | `broker_key` | string | Filter by broker (e.g. `tradovate`, `ninjatrader`). | | `account_type` | enum | `live`, `demo`, `paper`, `funded`, `live_funded`, `trading_combine`. | ## GET /accounts/{id} **Scope:** `read:accounts` (also `read:balance` to include the balance block) Single account plus its balance status and computed metrics (current balance, high-water mark, drawdown, P&L since start, profit target, max daily loss, max drawdown, deadlines, source preset). ```http GET /api/v1/accounts/192 ``` Pass `?include=basic` to skip the balance block. ## GET /accounts/{id}/balance **Scope:** `read:balance` Same balance block embedded in `GET /accounts/{id}` — exposed at this dedicated path for clients that want just the balance row plus computed metrics (current balance, high-water mark, drawdown) and the prop-firm rule snapshot (profit target, daily/total drawdown caps, payout split, deadlines). Returns `404` if the account has no balance profile configured. ```http GET /api/v1/accounts/192/balance ``` ## GET /accounts/{id}/balance/events **Scope:** `read:balance` Paginated audit log of balance state changes: status changes, rule violations, manual updates, payouts, resets, target adjustments. | Param | Type | Description | | --- | --- | --- | | `limit` | int | 1-200, default 50. | | `offset` | int | 0-based, default 0. | | `event_type` | enum | Filter to a single event type (status_change, rule_violation, payout, etc.). | ## GET /accounts/{id}/balance/payouts **Scope:** `read:balance` Paginated payout history: amount, status (pending / approved / paid / rejected), profit split, balance before/after, total profit at request time. --- ## POST /accounts **Scope:** `write:accounts` Create the account shell. Starting balance, prop-firm rules, and trade-template are configured separately via the balance endpoints (or via the settings UI). | Field | Type | Required | Notes | | --- | --- | --- | --- | | `account_name` | string (1–100) | yes | Unique among your active accounts. | | `account_type` | enum | no | `live` (default), `demo`, `paper`, `funded`, `live_funded`, `trading_combine`. | | `currency` | ISO 4217 | no | Defaults to `USD`. Three letters; lowercase is auto-uppercased server-side, so `"usd"` and `"USD"` are equivalent. | | `broker_key` | string ≤ 64 | no | From the `/brokers` catalog. Unknown keys return `400`. | | `breakeven_offset_low` | decimal ≤ 0 | no | Defaults to 0. | | `breakeven_offset_high` | decimal ≥ 0 | no | Defaults to 0. | Subscription gate: `bill_lim_canAddAccount`. Over-cap returns `429 limit_reached`. ```bash curl -X POST https://app.tradavity.com/api/v1/accounts \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "account_name": "TopStep XFA #2", "account_type": "trading_combine", "currency": "USD", "broker_key": "topstepx" }' ``` ## PATCH /accounts/{id} **Scope:** `write:accounts` Update `account_name`, `account_type`, `currency`, `broker_key`, `breakeven_offset_low`, `breakeven_offset_high`, `trade_template_id`. Send `broker_key: null` to unlink the broker; `trade_template_id: null` to clear the template. Also accepts `is_archived: bool` on its own (cannot be combined with other field updates). Setting `is_archived: true` archives the account; `false` unarchives. Same disconnect cascade as `DELETE /accounts/{id}` applies. Returns the updated account. ## DELETE /accounts/{id} **Scope:** `write:accounts` Archive an account. Disconnects auto-sync, clears tokens, and pauses sync as a side effect — same flow the settings UI runs. Returns `204 No Content`. Re-DELETE on an already-archived account returns `404 not_found`. Use `PATCH /accounts/{id}` with `{ "is_archived": false }` to unarchive. Cannot archive your only active account — returns `409 conflict`. Trades on the archived account still resolve via API (they keep their `account_id` and show `account.is_archived: true`). ```bash curl -X DELETE "https://app.tradavity.com/api/v1/accounts/192" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" ``` --- ## POST /accounts/{id}/balance/events **Scope:** `write:balance` Record a manual balance event. The API only allows `event_type=manual_update` — system-generated event types (status changes, rule violations, payouts) are managed by the platform itself. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `note` | string (1–1000) | yes | What this event records. | | `balance_at_event` | decimal | no | Snapshot of current balance. | | `pnl_at_event` | decimal | no | Snapshot of cumulative pnl. | | `rule_type` | string ≤ 50 | no | e.g. `profit_target`, `max_drawdown`. | | `rule_limit` / `rule_actual` | decimal | no | For rule-related notes. | ```bash curl -X POST "https://app.tradavity.com/api/v1/accounts/192/balance/events" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{"note": "Manual reconciliation: broker statement matches journal."}' ``` If the account has no balance profile set up (no starting balance ever recorded), this returns `400 validation_error` with a message telling the caller to initialize the balance first via the settings UI. ## POST /accounts/{id}/balance/payouts **Scope:** `write:balance` Request a payout. Creates a row with `status=pending`; gross / firm-cut / net amounts are computed from the account’s `profit_split_pct`. The lifecycle (approve, mark paid, reject) is handled in the settings UI — not yet exposed via the API. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `gross_amount` | decimal > 0 | yes | The requested amount before firm cut. | | `note` | string ≤ 500 | no | | > **Payouts are gated by available balance** > The server computes `effective_balance = current_balance - sum(gross of pending/approved/processing payouts)` and rejects requests that exceed it with `400 validation_error` — you cannot stack pending payouts that sum past the balance. The check runs inside a row-locked transaction so two concurrent requests can’t both pass. > > Always send `Idempotency-Key` on payout requests. Without it, a network retry can create a second pending payout that would re-debit the balance when both are paid. ```bash curl -X POST "https://app.tradavity.com/api/v1/accounts/192/balance/payouts" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Idempotency-Key: payout-2026-05-10-monthly" \ -H "Content-Type: application/json" \ -d '{"gross_amount": 2500, "note": "May payout"}' ``` --- # Copy Groups > Copy trading groups: list, read, create, update, delete; replace the member set. > Source: https://help.tradavity.com/api-docs/copy-groups > Category: Resources --- Copy trading groups link multiple accounts so a single decision can flow to all of them. Members carry a quantity multiplier; the master account drives the group when `copy_mode=master`. ## GET /copy-groups **Scope:** `read:copy_groups` List your copy trading groups with member counts and copy mode (`master`, `any_to_all`, `manual`). ## GET /copy-groups/{id} **Scope:** `read:copy_groups` Single group with `members[]`. Each member row carries the membership row `id`, the underlying `account_id` + `account_name` + `is_archived`, the `quantity_multiplier` applied to copied trades, and `sort_order`. --- ## POST /copy-groups **Scope:** `write:copy_groups` · **Plan gate:** Pro | Field | Type | Required | Notes | | --- | --- | --- | --- | | `name` | string (1–100) | yes | | | `copy_mode` | enum | no | `master` (default), `any_to_all`, `manual`. | | `scale_fees` | bool | no | Defaults to `true`. | You set the `master_account_id` via PATCH after adding members — the master must already be in the group. ```bash curl -X POST https://app.tradavity.com/api/v1/copy-groups \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{"name": "TopStep XFA Multi", "copy_mode": "master"}' ``` ## PATCH /copy-groups/{id} **Scope:** `write:copy_groups` Update `name`, `copy_mode`, `master_account_id`, `scale_fees`, `is_active`. Pass `master_account_id: null` to clear the master. The `master_account_id` must point at an account currently in the group; otherwise `400`. ## DELETE /copy-groups/{id} **Scope:** `write:copy_groups` Hard delete. Cascades: - Member rows in `set_copy_group_accounts` (FK CASCADE). - Goals scoped to this group + their snapshots. - Strategies bound to this group have `copy_group_id` nulled (the strategy survives, becomes global). ## PUT /copy-groups/{id}/members **Scope:** `write:copy_groups` Replace the entire member set. Up to 50 members per group. | Member field | Type | Notes | | --- | --- | --- | | `account_id` | int | Required. Must be an account you own. | | `quantity_multiplier` | decimal | Defaults to 1.0. Range 0 < multiplier ≤ 999.99. | | `sort_order` | int | Defaults to the array index. | If an account is already a member of a different copy group, the request returns `409 conflict` with `error.details.conflicting_account_ids`. Each account can only belong to one group at a time. If the existing `master_account_id` is removed by this PUT (no longer a member), the master is auto-cleared. Set a new master via PATCH afterward. ```bash curl -X PUT "https://app.tradavity.com/api/v1/copy-groups/7/members" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "members": [ {"account_id": 192, "quantity_multiplier": 1.0, "sort_order": 0}, {"account_id": 229, "quantity_multiplier": 0.5, "sort_order": 1} ] }' ``` --- # Auto-Sync > Auto-sync connection status, sync history, and manual sync trigger for broker integrations. > Source: https://help.tradavity.com/api-docs/autosync > Category: Resources --- Inspect broker auto-sync connections, read sync history, and trigger a manual sync. ## GET /autosync/connections **Scope:** `read:autosync` List your auto-sync connections. No token material is ever exposed. ## GET /autosync/connections/{id} **Scope:** `read:autosync` Single connection. ## GET /autosync/log **Scope:** `read:autosync` Sync history. --- ## POST /autosync/connections/{id}/sync **Scope:** `write:autosync` · **Plan gate:** Pro Trigger an immediate sync on a broker connection. Routes through the same dupe-detection + import pipeline the settings UI uses, so a manual sync from the API is byte-identical to one triggered from the dashboard. No request body. The connection’s configured Tradavity account_id is the import target. | Status | Code | Meaning | | --- | --- | --- | | 200 | (no error code) | Sync ran. Body `data` contains `connection_id`, `log_id`, `imported`, `skipped`, `total_fetched`, `total_trades`, `total_pnl`, `errors[]`, and a human-readable `message`. | | 400 | `validation_error` | Connection inactive or no broker account selected. | | 401 | `token_expired` | Broker token expired or could not be refreshed. The user needs to reconnect via the settings UI. | | 404 | `not_found` | No connection with that id, or it doesn’t belong to you. | | 429 | `limit_reached` | Pro plan required for sync. | | 429 | `connection_sync_cooldown` | Per-connection cooldown — manual syncs are capped to once every 20 seconds per connection to protect the upstream broker API from amplification. Distinct from the key-level rate limit. Headers: `X-RateLimit-Scope: connection-sync`, `Retry-After: `. `error.details.retry_after_seconds` mirrors the header. Cron-triggered syncs (the 15-minute auto-sync schedule) bypass this gate. | | 502 | `broker_api_error` | Upstream broker API returned an error. Retry later. | | 500 | `sync_failed` | Sync pipeline raised an unexpected error (parser crash, DB transaction abort, etc.). Retrying is safe — the operation is idempotent on the broker side. | ```bash # Replace 12 with one of your connection_ids from GET /autosync/connections curl -X POST "https://app.tradavity.com/api/v1/autosync/connections/12/sync" \ -H "Authorization: Bearer tvty_YOUR_KEY_HERE" \ -H "Idempotency-Key: $(uuidgen)" ``` The Idempotency-Key cache returns the same imported/skipped numbers on replay without hitting the broker again. Useful when a flaky network kills the response but the sync already ran. --- # Stats > Aggregate stats: summary (totals + decisions), by-symbol, by-strategy. All accept the same date and account filters. > Source: https://help.tradavity.com/api-docs/stats > Category: Reports & Calendar --- Aggregate stats over a date range, with parallel *totals* (per-row, real-money correct) and *decisions* (one signal regardless of copies) breakdowns. ## GET /stats/summary **Scope:** `read:trades` Aggregate stats over a date range. Returns `range` (echoed for clarity) plus two parallel breakdowns: - **totals** — per-row counts: `trade_rows`, `total_net_pnl`, `total_gross_pnl`, `total_fees`, `best_trade`, `worst_trade`, `avg_pnl`. Every copy counts as its own row (real-money correct). - **decisions** — grouped by `COALESCE(source_trade_id, trade_id)`: `count`, `wins`, `losses`, `breakevens`, `win_rate_pct`, `avg_win`, `avg_loss`, `expectancy`, `profit_factor`. Use this for win rate (one signal = one decision, regardless of how many accounts copied it). | Param | Type | Description | | --- | --- | --- | | `from` | date | Default: 30 days ago. | | `to` | date | Default: today. | | `range` | enum | Shortcut: `today`, `yesterday`, `7d`, `30d`, `90d`, `mtd`, `ytd`, `all`. Overrides `from`/`to`. | | `account` / `account_id` | int or string | Either alias works. | ### Breakeven offset classification Win / loss / breakeven counts on every stats endpoint honor your account's `breakeven_offset_low` and `breakeven_offset_high` — same logic the in-app stats page uses. A decision P&L inside the BE window is counted as *breakeven*, not as a tiny win or tiny loss. The window in effect is echoed back in the response as `breakeven_offset: {low, high}`. Which offset is used: - **Filter by `account_id`:** that account's offset. - **Filter by `account` name:** resolved account's offset. - **No account filter (all accounts):** the user's first non-archived account's offset (matches the website's group-mode rule). ## GET /stats/by-symbol **Scope:** `read:trades` ## GET /stats/by-strategy **Scope:** `read:trades` `GET /stats/by-symbol` and `GET /stats/by-strategy` accept the same parameter set as `GET /stats/summary`. Both buckets are **decision-grouped**: a single trading decision copied across multiple accounts counts as one entry (with P&L summed across the copies), not as N separate rows. `wins` / `losses` / `breakevens` honor the same BE-offset window described above. --- # Calendar > Daily P&L over a date range, grouped in your user timezone. > Source: https://help.tradavity.com/api-docs/calendar > Category: Reports & Calendar --- ## GET /calendar **Scope:** `read:trades` Daily P&L over a date range, grouped in your user timezone. Returns `days[]` (one row per traded day with `net_pnl`, `trade_count`, `winning_trades`, `losing_trades`) plus `summary` with `traded_days`, `winning_days`, `losing_days`, `flat_days`, `total_trades`, `total_net_pnl`, the user’s `timezone` string, and the active `breakeven_offset: {low, high}`. **Decision-grouped counts.** `trade_count`, `winning_trades`, and `losing_trades` are decision-based: one trading decision copied to N accounts contributes one row, with P&L summed across copies. `net_pnl` is still real money (sum across all rows for that day). `winning_trades` / `losing_trades` and the per-day `winning_days` / `losing_days` / `flat_days` classification honor your account’s breakeven offset window — days landing inside the window are flat, not a tiny win or loss. See the [Stats reference](https://help.tradavity.com/api-docs/stats) for how the offset is picked. | Param | Type | Description | | --- | --- | --- | | `from` | date | Required unless `range` is supplied. | | `to` | date | Required unless `range` is supplied. | | `range` | enum | Shortcut: `today`, `yesterday`, `7d`, `30d`, `90d`, `mtd`, `ytd`, `all`. Overrides `from`/`to`. Invalid values return `400 validation_error`. | | `account` / `account_id` | int or string | Either alias works. | | `include_archived_accounts` | bool | Default false. | --- # Catalog > Identity (/me), usage, brokers, instruments, prop firm presets, scopes, emotions. All read-only. > Source: https://help.tradavity.com/api-docs/catalog > Category: Catalog --- Read-only endpoints that surface identity, usage, and platform-managed reference data. Each one requires `read:catalog` (granted automatically to every key) unless noted otherwise. ## GET /me **Scope:** any (granted automatically) Calling user’s identity, plan tier, preferences (timezone, currency, week start), storage usage, and the metadata of the API key making the request. ```http GET /api/v1/me ``` *Returns:* `user`, `plan`, `preferences`, `storage`, `key`, `rate_tier`. ## GET /usage **Scope:** `read:catalog` This key’s API usage in the last 24 hours: total requests, error count, last-60s count, top paths, and storage. ```http GET /api/v1/usage ``` ## GET /brokers **Scope:** `read:catalog` List of supported brokers with their capabilities (`supports_api`, `supports_autosync`, `supports_csv`, `supports_manual`) and logo file. ## GET /instruments **Scope:** `read:catalog` Reference instruments with multipliers, asset type, exchange, tick size and value. Returns up to 500 rows (use filters to narrow if you hit the cap). Response includes `count` (rows in this page) and `total_count` (rows matching the filter); `has_more: true` indicates the 500-row cap was hit. | Param | Type | Description | | --- | --- | --- | | `asset_type` | enum | `futures`, `forex`, `stocks`, `crypto`, `options`. | | `exchange` | string | Exact match, case-insensitive. | | `search` | string | Partial match on symbol or name. | > **Catalog scope** > The instruments catalog is currently seeded with futures and forex symbols only (~70 rows). Filtering by `asset_type=stocks`, `crypto`, or `options` returns an empty list. `POST /trades` still accepts those asset_type values — the catalog is informational, not gating. Use whichever symbol string is meaningful for your broker. ## GET /prop-firm-presets **Scope:** `read:catalog` Catalog of prop firm challenge rules — profit targets, drawdown rules, trading day requirements, payout schedules, consistency rules. Returns up to 500 rows. | Param | Type | Description | | --- | --- | --- | | `firm_key` | enum | One of the active firm_keys in the catalog (e.g. `topstep`, `apex`, `ftmo`). Unknown values return `400 validation_error` with `error.details.allowed` listing the live set. Note: only prop firms are listed — broker/platform keys like `topstepx` (the execution platform) are not prop firms and won't appear here. | | `market_type` | enum | `futures`, `forex`, `stocks`, `crypto`. Unknown values return `400`. | | `phase` | enum | `evaluation`, `funded`, `live`. | | `account_size` | decimal | Filter to one size (e.g. `50000`). | ## GET /prop-firm-presets/{id} **Scope:** `read:catalog` Detail for one preset by `preset_id`. Same shape as a list row from `GET /prop-firm-presets` — useful when you have an id and want the full rule set without paging. ## GET /scopes **Scope:** `read:catalog` The full scope catalog with description + paired read/write companion for each entry. Use this if you’re building a key-creation UI and want to label scopes from the live API rather than hardcoding them. ## GET /emotions **Scope:** `read:catalog` Catalog of emotion values usable in `trade.emotional_state`. --- # Worked Example: AI Agent Journaling a Trade > Four chained API calls to fully journal a trade — create, tag, screenshot, protocol response. > Source: https://help.tradavity.com/api-docs/worked-example > Category: Guides --- An AI agent reading user voice notes wants to create a fully-journaled trade with a screenshot and tags. Four chained calls. **The numeric IDs in this snippet (account 192, tags 19/109, protocol 80, items 640/641) are illustrative** — always discover real IDs for your user first. ```bash # 0. Discover real IDs (every account/tag/protocol ID below is yours, not ours) curl -s "https://app.tradavity.com/api/v1/accounts" -H "Authorization: Bearer $KEY" curl -s "https://app.tradavity.com/api/v1/tags" -H "Authorization: Bearer $KEY" curl -s "https://app.tradavity.com/api/v1/protocols" -H "Authorization: Bearer $KEY" # For protocol item_ids, fetch the single protocol: curl -s "https://app.tradavity.com/api/v1/protocols/80" -H "Authorization: Bearer $KEY" # 1. Create the trade TRADE=$(curl -s -X POST https://app.tradavity.com/api/v1/trades \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: agent-$(date +%s)-trade" \ -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": "Strong breakout, executed plan." }') TRADE_NUMBER=$(echo "$TRADE" | jq -r .data.trade.trade_number) # 2. Tag it curl -s -X PUT "https://app.tradavity.com/api/v1/trades/$TRADE_NUMBER/tags" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"tag_ids": [19, 109]}' # 3. Attach a chart screenshot curl -s -X POST "https://app.tradavity.com/api/v1/trades/$TRADE_NUMBER/screenshots" \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: agent-$(date +%s)-screenshot" \ -F "file=@/tmp/chart.png" \ -F "label=Entry breakout" # 4. Record a pre-trade protocol response linked to it curl -s -X POST "https://app.tradavity.com/api/v1/protocols/80/responses" \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: agent-$(date +%s)-protocol" \ -H "Content-Type: application/json" \ -d "{\"trade_number\": $TRADE_NUMBER, \"item_values\": {\"640\":\"1\",\"641\":\"yes\"}}" ``` Note the `Idempotency-Key` on every write — if the agent retries any step after a network blip, it returns the cached response without double-creating. --- # Python: Idempotent Client Helper > A drop-in Python helper that adds an Idempotency-Key to every write call automatically. > Source: https://help.tradavity.com/api-docs/python-helper > Category: Guides --- ```python import os import uuid import requests API_KEY = os.environ["TRADAVITY_API_KEY"] BASE_URL = "https://app.tradavity.com/api/v1" HEADERS = {"Authorization": f"Bearer {API_KEY}"} def write(method, path, body=None, idempotency_key=None, params=None, files=None): """POST / PATCH / PUT / DELETE with automatic Idempotency-Key.""" h = dict(HEADERS) if idempotency_key is None: idempotency_key = str(uuid.uuid4()) h["Idempotency-Key"] = idempotency_key if files is None and body is not None: h["Content-Type"] = "application/json" r = requests.request( method, f"{BASE_URL}{path}", headers=h, params=params or {}, json=body if files is None else None, files=files, timeout=30, ) resp = r.json() if r.headers.get("Content-Type", "").startswith("application/json") else None if not r.ok or (resp and not resp.get("ok")): err = resp["error"] if resp else {"code": "http_error", "message": r.text} raise RuntimeError(f"{r.status_code} {err.get('code')}: {err.get('message')}") return resp["data"] if resp else None # Examples trade = write("POST", "/trades", { "account_id": 192, "trade_date": "2026-05-10T14:32:00Z", "symbol": "MNQ", "direction": "long", "net_pnl": 50.0, "fees": 1.24, "trade_quality_grade": "A", }) n = trade["trade"]["trade_number"] write("PUT", f"/trades/{n}/tags", {"tag_ids": [19, 109]}) with open("chart.png", "rb") as f: write("POST", f"/trades/{n}/screenshots", files={"file": ("chart.png", f, "image/png"), "label": (None, "Entry breakout")}) # Read it back to confirm it landed r = requests.get(f"{BASE_URL}/trades/{n}", headers=HEADERS, timeout=30).json() t = r["data"]["trade"] print(f"Trade #{n}: {t['symbol']} {t['direction']} net_pnl={t['net_pnl']}") ``` ---