Skip to main content
Developers

Streaming

One WebSocket connection carries live quotes, candles, account updates and order events, so your program never has to poll.

URL: wss://terminal.jafx.com/trading-api/v1/stream

Connect and authenticate

Send your key in the Authorization header when you connect. If your WebSocket library cannot set headers, send an auth frame within 5 seconds instead:

JSON
{"op": "auth", "key": "jafx_demo_…"}

The server answers with ready:

JSON
{"op": "ready", "key_id": "k7mx2v9qr4tz8w1b6nfa", "account": 123456, "scopes": ["market:read", "account:read", "trade"]}

A missing or invalid key closes the connection with code 4401.

Subscribe

JSON
{"op": "subscribe", "channels": ["quotes:EURUSD,XAUUSD", "candles:EURUSD:M1", "account", "orders"]}

The server confirms what you are now subscribed to:

JSON
{"op": "subscribed", "channels": ["quotes:EURUSD,XAUUSD", "candles:EURUSD:M1", "account", "orders@1791879125359"]}

orders@1791879125359 means the orders channel is at sequence number 1791879125359: the account's next order event gets a higher one. Unsubscribe the same way with "op": "unsubscribe"; "quotes" on its own removes every quote subscription.

ChannelScopeWhat you receive
quotes:SYM,SYM,…market:readThe current quote at once, then every change, at most four times a second per instrument. Unknown instruments are skipped.
candles:SYM:TFmarket:readThe forming bar every second it changes. When a new bar starts, the finished one is sent again with "complete": true. Timeframes: M1 M5 M15 M30 H1 H4 D1 W1.
accountaccount:readA snapshot of the account summary at once, then at most one update a second while it changes.
ordersaccount:readPosition, pending-order and request events for the key's account, numbered with seq.

Frames

Quotes

JSON
{"ch": "quotes", "data": {"symbol": "EURUSD", "bid": 1.11554, "ask": 1.11555, "time_ms": 1791883801242, "price_basis": "raw"}}

Candles

JSON
{"ch": "candles", "symbol": "EURUSD", "timeframe": "M1",
 "data": {"time": "2026-10-13T09:30:00.000Z", "open": 1.11549, "high": 1.11558, "low": 1.11546, "close": 1.11554, "tick_volume": 41, "complete": false}}

Account

JSON
{"ch": "account", "data": {"account": 123456, "group": "demo-usd", "mode": "demo", "currency": "USD", "leverage": 100,
 "balance": "10000.00", "credit": "0.00", "equity": "10003.20", "margin": "223.11", "free_margin": "9780.09",
 "floating_profit": "3.20", "margin_level": 4483.53, "as_of": "2026-10-13T09:30:02.114Z"}}

Orders

JSON
{"ch": "orders", "seq": 1791879125360, "event": "position.opened", "time": "2026-10-13T09:29:47.690Z",
 "data": {"ticket": 5001, "symbol": "EURUSD", "side": "buy", "volume": "0.02", "open_price": 1.11555, "stop_loss": 1.11,
          "take_profit": null, "profit": "-0.02", "origin": "api", "client_tag": "breakout-v2", "from_ticket": null}}
eventdata
request.decided{account, request_id, kind, status, ticket} when an order request made through the API is decided. It arrives before the position or order it created.
position.openedThe new position (same fields as GET …/positions), including positions opened in the web terminal.
position.modifiedThe position after a change of stop loss, take profit or volume.
position.closed{ticket, symbol}. The close price and result are in GET …/trades.
order.placed, order.modifiedThe pending order (same fields as GET …/orders).
order.triggered{ticket} when a pending order fills; position.opened follows.
order.cancelled{ticket, symbol} when a pending order is cancelled, expires or is removed.

Control frames

FrameMeaning
{"op": "heartbeat", "time": …}Sent every 20 seconds.
{"op": "pong", "time": …}The answer to your {"op": "ping"}.
{"op": "error", "code": …, "message": …}A problem that does not end the connection, for example insufficient_scope when a channel needs a scope the key lacks (that channel is skipped).
{"op": "resync_required", "seq": …}The events you asked to replay can't be replayed; reload positions and orders over REST and carry on from this seq.

Reconnecting without missing events

Remember the seq of the last orders frame you processed. After reconnecting, subscribe with since_seq and the server first replays what you missed, in order, before its subscribed answer:

JSON
{"op": "subscribe", "channels": ["orders"], "since_seq": 1791879125360}

Events are kept for 5 minutes, including while you are disconnected. You get resync_required instead when you were away longer, when the server restarted in between, or when since_seq is not a sequence number the stream gave you. Then reload positions and pending orders over REST, and carry on from the new seq.

Sequence numbers only go up, but they are not a count: they have 13 digits, and they jump after a resync_required. Store them as 64-bit integers; they don't fit in 32 bits.

  • Reconnect with backoff: 1 s, 2 s, 4 s and so on, up to 30 s.
  • The server sends a heartbeat every 20 seconds and WebSocket pings, which most libraries answer for you. If you hear nothing for 45 seconds, treat the connection as dead and reconnect.

Close codes

CodeMeaningWhat to do
4401The key is missing, invalid, expired, revoked or suspended. A key revoked mid-stream closes its streams within 5 seconds.Stop. Fix the key before reconnecting.
4409Replaced by a newer connection with the same key.Check whether another copy of your program is running.
1001The server is restarting.Reconnect with backoff.
1006, othersNetwork trouble.Reconnect with backoff.

Limits

  • 2 connections per key. A third connection replaces the oldest, which closes with 4409, so a reconnect is never locked out by a connection that died silently.
  • Up to 100 quote instruments and 20 candle series per connection.
  • Frames you send are limited to 16 KB.
  • Slow readers: if more than 1 MB is waiting to be sent to you, quote, candle and account frames are skipped until you catch up; order events are never skipped. Above 4 MB the connection is closed.

Example client

A client that reconnects with backoff and replays missed order events:

Python
import asyncio, json, os
import websockets

URL = "wss://terminal.jafx.com/trading-api/v1/stream"
KEY = os.environ["JAFX_API_KEY"]

async def run():
    last_seq, delay = None, 1
    while True:
        try:
            async with websockets.connect(URL, additional_headers={"Authorization": f"Bearer {KEY}"}) as ws:
                delay = 1
                sub = {"op": "subscribe", "channels": ["orders", "quotes:EURUSD"]}
                if last_seq is not None:
                    sub["since_seq"] = last_seq
                await ws.send(json.dumps(sub))
                async for raw in ws:
                    frame = json.loads(raw)
                    if frame.get("ch") == "orders":
                        last_seq = frame["seq"]
                        print(frame["event"], frame["data"])
                    elif frame.get("op") == "resync_required":
                        last_seq = frame["seq"]   # reload positions over REST here
        except websockets.ConnectionClosed as e:
            if e.rcvd and e.rcvd.code in (4401, 4409):
                raise SystemExit(f"stream closed with {e.rcvd.code}")
        except OSError as e:
            print("connection failed:", e)
        await asyncio.sleep(delay)
        delay = min(delay * 2, 30)

asyncio.run(run())

Automated and AI-driven trading can lose money quickly; nothing in these docs is investment advice.

JAFX (PTY) LTD · FSCA FSP No. 52058 · Trading leveraged derivatives with JAFX carries a high risk of losing money rapidly. It is not suitable for everyone.