API
View as Markdown

Conventions

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:

{
  "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:

{
  "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.