Conventions
The rules every endpoint follows. Read this once and the rest of the API behaves the way you expect.
Base URL and versions
- REST:
https://terminal.jafx.com/trading-api/v1. Streaming:wss://terminal.jafx.com/trading-api/v1/stream. MCP:https://terminal.jafx.com/trading-api/mcp. - HTTPS only. Requests and responses are JSON (UTF-8); send
Content-Type: application/jsonwith a body. Bodies are limited to 64 KB. - The version is in the path. Within
v1, changes are additive: new endpoints, new optional fields, new values in lists. Write clients that ignore fields they do not know. Anything breaking comes as a new version, announced in the changelog beforehand. - The machine-readable description is the OpenAPI 3.1 spec.
Authentication
Authorization: Bearer jafx_demo_k7mx2v9qr4tz8w1b6nfa_…Every endpoint needs a key except GET /status. The API is meant for server-side programs; it does not answer cross-origin requests from browsers. See API keys & security.
Numbers
- Money and volume are decimal strings:
"0.01"lot,"10985.05"USD. They are exact; parse them with a decimal type, not a float. - Prices are JSON numbers (
1.11554), with as many decimals as the instrument'sdigits. - Volumes you send must be a multiple of the instrument's
volume_stepand betweenvolume_minandvolume_max(GET /symbols/{symbol}).
Time
- Times are ISO-8601 in UTC:
2026-10-13T09:30:01.242Z. Query parameters such asfromandtoalso accept epoch milliseconds. - Stream quote frames carry
time_ms(epoch milliseconds) instead, to keep high-frequency frames small. - The broker day starts at midnight Europe/Athens. Daily candles and the daily loss limit follow it.
Prices
Instruments, quotes and candles come from one central JAFX reference quote board, the same for every key, and are marked "price_basis": "raw". The board is not your account's own server: your account trades at its own server's prices, with the markup of its account type, so they can differ a little from the board. A raw quote is a reference, not a guaranteed fill price. The broker fills and validates every order at your account's own prices, and refuses an instrument your account cannot trade.
Quotes update at most four times a second per instrument. They are snapshots of the board, not every tick, and candles are built from the same snapshots.
Idempotency
Every write (POST, PATCH, DELETE) needs an Idempotency-Key header: 1–64 characters from A–Z a–z 0–9 . _ : -, unique per intended action. A UUID is ideal.
- Retrying with the same key and the same body returns the original answer, with the header
Idempotent-Replayed: true. It never trades twice. This is what makes it safe to retry after a timeout or a dropped connection. - The same key with a different body answers
409 idempotency_conflict. - The same key while the first call is still running answers
409 request_in_progresswithRetry-After. - Refused before it reached the broker (validation, limits, a trading halt, the broker connection being unavailable): the key is released, so you can retry with the same key once the cause is fixed.
- Answered by the broker (filled, placed, rejected): the key stays bound to that answer for 72 hours. After that, the same key places a new order.
- If JAFX lost its own record of the first call, for example in a server crash, a retry with the same key still reaches the broker as the same request, and the broker answers it with the original order. That answer has a new
request_idand noIdempotent-Replayedheader, but the originalticket.
Generate the key when you decide to place the order, store it with the order in your program, and reuse it for every retry of that order. A new key means a new order.
Held decisions and wait
Most orders are decided in a fraction of a second. Sometimes the broker holds an order while it covers it with a liquidity provider. Write calls wait up to wait seconds for the decision (query parameter, 0–10, default 5), then answer 202 with "status": "pending". The decision then arrives as a request.decided event on the orders stream, and GET /accounts/{account}/requests/{request_id} shows it.
If the connection to the broker fails while an order is in flight, the answer is 202 with "status": "outcome_unknown". Poll the request until it resolves. Do not send the order again under a new key: it may already have executed.
Pagination and windows
- Lists that can be long (
/trades,/transactions) takelimit(1–1000, default 100) and returnnext_cursor. Pass it ascursorto get the next page;nullmeans there is no more. - History is queried by window:
fromdefaults to 7 days beforeto,todefaults to now, and a window may span up to 90 days. Closed trades are ordered by close time, newest first.
Rate limits
Per key: reads 20 per second with bursts of 40, writes 5 per second with bursts of 10. Order and modification limits per minute come on top (see Limits). Every response carries:
RateLimit-Limit: 40
RateLimit-Remaining: 37
RateLimit-Reset: 1Over the limit, the answer is 429 with a Retry-After header in seconds.
All REST and MCP requests from one IP address also share a limit of 50 per second with bursts of 100, whatever the key. It only matters when several keys or programs call from the same address. Over it, the answer is 429 rate_limited with Retry-After, without the RateLimit-* headers.
Errors
Errors are RFC 9457 problem documents (application/problem+json) with a stable code to branch on:
{
"type": "https://terminal.jafx.com/trading-api/problems/volume_limit_exceeded",
"title": "Volume limit exceeded",
"status": 422,
"code": "volume_limit_exceeded",
"detail": "This key allows at most 1.00 lots per order",
"request_id": "req_01JQ8ZJ4X2M7R9T5KN3VW6QH8B"
}Branch on code, not on title or detail, which may be reworded. Validation errors list the fields in details. The full list is on Errors.
Request IDs
Every response has an X-Request-Id header (req_…, an opaque string whose length can vary), repeated as request_id in problem documents. Quote it when you contact support. It identifies the HTTP call; an order's own request_id (26 characters, in order results) identifies the order request for GET /accounts/{account}/requests/{request_id}.
Retries
| What happened | What to do |
|---|---|
| Network error or timeout on a read | Retry with backoff. |
| Network error or timeout on a write | Retry with the same Idempotency-Key. You get the original answer if the first call got through. |
429, 503, 409 request_in_progress, 409 order_busy | Wait for Retry-After, then retry the same call. |
503 service_unavailable on a write | Wait for Retry-After, then retry with the same Idempotency-Key. You get the original answer if the first call got through. |
202 pending or 202 outcome_unknown | Do not resend. Follow the request on the stream or poll it. |
423 trading_halted | Wait until GET /me shows trading back on. |
Other 4xx | Do not retry unchanged; fix the request first. |
Give write calls a client timeout of at least 15 seconds: the server may hold them for up to wait seconds.