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.fieldsis 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 thecursorquery param.meta.has_moretells you when to stop. - All other list endpoints use offset pagination via
limitandoffsetquery params. Defaultlimitis 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 includestotal_countandhas_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_reachedwitherror.detailsshowing the cap. - Unknown fields are rejected. The server validates the payload against an explicit allow-list per endpoint and returns
400 validation_errorwith 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 like2026-05-10is also accepted and stored as midnight UTC. Date-only fields useYYYY-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"returns400 validation_errorinstead of being silently coerced. JSON numbers ("quantity": 1) and number-looking strings ("quantity": "1") are both accepted. - POST returns
201 Createdwith the created resource indata. PATCH and PUT return200 OK. DELETE returns204 No Content(no body). - Optimistic concurrency: there is no
If-Matchheader today. Last write wins for concurrent PATCH on the same resource. Concurrent writes that violate a uniqueness constraint return409 duplicate_name.