API reference
Every Forex & Multi-Asset Trading endpoint — 65 admin routes with the permission each gates on, 18 customer routes, three WebSocket streams, the enums, the tables and the compliance gates.
Two API surfaces. Everything under /api/admin/forex-trading carries an
explicit permission and writes an audit entry under the ADMIN_FX module.
Everything under /api/forex-trading is scoped to the calling user and carries
no permission at all — it is gated by ownership, by the geo-block list, by
the trade_forex KYC feature and by fxTradingEnabled instead.
Conventions that will catch you out
The platform pins the HTTP status at 200 and puts the real outcome in the body. CORS handling in the uWebSockets layer forces it; the code you need is in the response payload. A client branching on the status code will read a geo-blocked account creation, a rejected order and a refused withdrawal as successes.
Money columns on these tables are DOUBLE, but anything that reaches you
through a DECIMAL column elsewhere in the platform arrives as a string.
Coerce before arithmetic.
Sizes are base units, not lots. amount on an order, a position and a trade
deal is in base units; lots are amount / metadata.contractSize. A 1-lot
EUR/USD order is amount: 100000.
Symbols are CURRENCY/PAIR. EUR/USD, XAU/USD, AAPL/USD. There is no
separate symbol column — it is composed from fx_instrument.currency and
.pair, which are unique together.
Admin endpoints
65 routes. Grant the permissions under Users → Roles & Permissions; nothing is granted by default and Super Admin bypasses the check.
Risk desk
Accounts
Positions, orders and the ledger
Withdrawals
Market-data providers
Instruments
There is no create and no delete for instruments — see Adding a symbol.
Symbol groups
Session calendars
Economic calendar and market news
The durability rules on those eight routes are in The economic calendar and news feeds.
A-book execution
Every route here is view / create / edit / delete on
forex_trading.execution. Treat edit.forex_trading.execution as Super-Admin
adjacent: it enables a hedge venue, flattens an orphan and books a late fill,
all of which move real money at a real broker.
Settings
fxTradingExternalRoutingEnabled is rejected with a 403 by the PUT no matter
what permissions the caller holds — it is writable only through the core system
settings page, where the protected-keys check enforces Super Admin.
Three screens — Instruments, Economic Calendar and Market News — are gated in
the frontend permission manifest on view.forex_trading.instrument, while the
routes behind two of them check view.forex_trading.economic_event and
view.forex_trading.news. A role granted only the finer data key can be
refused the page; a role granted only the instrument key can open the page and
get an empty table. Grant both.
Customer endpoints
18 REST routes under /api/forex-trading, plus three WebSocket routes. None
carries a permission. Five are public — the market data ones — and the rest
require a session and verify that the caller owns the account in the request.
Accounts and money
Deposit and withdraw both take amount in the account currency and a
walletType. The withdrawable amount is
max(0, min(balance, equity − usedMargin)), computed inside the same row lock
as the debit, and is refused outright below the tier's margin-call level or
while the account's metrics are stale.
Orders
A MARKET order fills instantly against the desk at the marked-up price. A pending order rests until the risk engine triggers it on a later tick; new levels on a modify must not be instantly triggerable at the current mark. Pending orders never reserve margin, which is why cancelling one moves nothing.
Positions
amount is passedMarket data — public
chart takes symbol and interval (required), plus from/to or
duration. It is cache-first with session-aware back-fill — a closed-session
range is left empty rather than fabricated.
calendar takes from, to, currency (comma-separated) and impact
(LOW,MEDIUM,HIGH); rows with no currency are always included. news takes
limit, category and symbol, where symbol matches either leg or the
closed-up pair (EURUSD) against headline and summary text, or an operator's
relatedSymbols tag — and never falls back to the unfiltered feed, so an empty
array is a legitimate answer.
WebSocket
Three routes, all behind the same reverse-proxy /api path as the REST API. If
your proxy does not upgrade WebSocket connections the terminal renders once and
then stops moving.
| Route | Auth | Subscribe payload | Carries |
|---|---|---|---|
/api/forex-trading/ticker |
none | {type: "tickers"} |
Pins every visible ACTIVE instrument on the tick engine, returns a full snapshot, then per-symbol deltas |
/api/forex-trading/market |
none | the selected symbol | Ticker, OHLCV and quoting/session status frames for one instrument, with an immediate snapshot on subscribe |
/api/forex-trading/order |
required | {type: "orders"|"positions", accountId, userId} |
The caller's own order and position updates, plus engine sweep updates |
The ticker snapshot fills gaps with a TTL-cached REST quote for instruments the tick engine has never seen — every US stock for about 17 hours a day and the whole weekend — so a closed market shows a price instead of a dash. Those snapshot values are display-only and are never fed into the tick engine.
On /api/forex-trading/order the server enforces that the cookie-authenticated
user owns both the userId in the payload and the account. Client-supplied ids
are never trusted.
Enums an integrator must branch on
| Column | Values |
|---|---|
fx_account.type |
DEMO, LIVE |
fx_account.marginMode |
HEDGING, NETTING |
fx_order.side, fx_position.side |
BUY, SELL |
fx_order.type |
MARKET, LIMIT, STOP, STOP_LIMIT |
fx_order.timeInForce |
GTC, GTD, DAY |
fx_order.status |
OPEN, TRIGGERED, ROUTING, FILLED, CANCELLED, REJECTED, EXPIRED |
fx_order.routing, fx_position.routing |
INTERNAL, EXTERNAL |
fx_position.status |
OPEN, CLOSED, LIQUIDATED |
fx_position.closeReason |
MANUAL, SL, TP, TRAILING, STOP_OUT, GAP, ADMIN, DELISTED, CORPORATE_ACTION |
fx_deal.kind |
OPEN, CLOSE, PARTIAL_CLOSE, SWAP, COMMISSION, DIVIDEND, ADJUSTMENT, NBP_CORRECTION, DEPOSIT, WITHDRAW, WITHDRAW_REVERSAL |
fx_instrument.status |
INACTIVE, ACTIVE, CLOSE_ONLY, HALTED, DELISTED |
fx_instrument.assetClass |
FOREX, STOCK, COMMODITY, INDEX, CRYPTO |
fx_economic_event.source, fx_market_news.source |
PROVIDER, MANUAL |
fx_economic_event.impact |
LOW, MEDIUM, HIGH |
ROUTING only appears on A-book installs: the order intent is journaled and the
broker confirmation has not arrived. LIQUIDATED is a stop-out; the position
also carries closeReason: STOP_OUT.
closeReason is the one enum where the column accepts more than the backend
writes. Only MANUAL, SL, TP, TRAILING, STOP_OUT and ADMIN are ever
produced; GAP, DELISTED and CORPORATE_ACTION are validator members with no
writer, so a branch on them is dead code. See
Running the desk.
Tables and who writes them
Fourteen tables, all MySQL. ScyllaDB is not used by this extension.
| Table | Written by |
|---|---|
fx_provider |
Provider PUT and status PUT; self-healed on the provider list GET |
fx_execution_provider |
Execution provider PUT, status PUT, symbols POST, verify POST; self-healed on the execution provider list GET; the hedge monitor cron writes venue balance, NAV and margin onto the row, and the reconciler advances its sync cursor |
fx_instrument |
Instrument PUT, status PUT, import POST. Nothing else — no create, no delete |
fx_symbol_group |
Group POST/PUT/DELETE; created by import |
fx_session_calendar |
Calendar POST/PUT/DELETE; created by import |
fx_account_group |
No route writes this table. SQL only — see Account tiers |
fx_account |
Customer account POST, deposit, withdraw, risk-ack; admin account PUT and adjust; the risk engine and the reconciler refresh equity and used margin |
fx_order |
Customer order POST/PUT/DELETE; the risk engine on trigger, fill, reject and expiry; the reconciler's expiry sweep |
fx_position |
Customer position close and PUT; admin force-close; the risk engine on SL/TP/trailing/stop-out; the swap cron's accrual |
fx_deal |
Only the account service, through applyBalanceChange. Append-only, and every write carries an idempotency key |
fx_routing_rule |
Routing rule POST/PUT/status/DELETE |
fx_execution_alert |
The alert dispatcher (any severity, from any engine path) and the ack POST |
fx_economic_event |
Economic-event POST/PUT/DELETE and the 15-minute sync (PROVIDER rows only) |
fx_market_news |
News POST/PUT/DELETE and the 15-minute sync (PROVIDER rows only) |
Withdrawals are not an fx table. A request writes a PENDING row on the
core transaction table with type = 'FX_TRADING_WITHDRAW'; the admin queue is
a filtered view of it. Deposits are recorded as FX_TRADING_DEPOSIT.
Processing that request does not write a second row of some refund type.
Approving flips the PENDING row to COMPLETED and credits the wallet through
the wallet service, which books its own COMPLETED FX_TRADING_WITHDRAW row
for the credit. Rejecting flips the row to REJECTED and writes nothing new
on transaction at all — the money goes back as a WITHDRAW_REVERSAL deal on
fx_deal, and the request's consumed daily and monthly withdrawal allowance is
credited back on fx_account. FX_TRADING_WITHDRAW_REFUND exists only as a
wallet-service operation type, is not a value the transaction.type ENUM
accepts, and would persist as FX_TRADING_DEPOSIT if anything called it —
nothing does. Do not match on it.
The balance is a denormalisation of fx_deal. The reconciler verifies that
Σ pnl == balance every five minutes and raises a critical ledger drift
alert by email to every Super Admin when it does not. A direct
UPDATE fx_account SET balance sets that alarm off within five minutes and
leaves you unable to distinguish a real accounting fault from your own edit.
Compliance gates, and where they are deliberately absent
Three independent gates run on the customer surface. Knowing which routes are not gated is the point of this section.
| Gate | Applied at |
|---|---|
Geo block (fxTradingGeoBlockList) |
GET /account (a blocked user is not auto-provisioned and gets an empty list), POST /account, POST /account/{id}/deposit, POST /order, PUT /order/{id} |
KYC feature (trade_forex) |
POST /order only, and only for LIVE accounts. DEMO is exempt — it is the funnel |
fxTradingEnabled |
POST /account (LIVE only), POST /account/{id}/deposit, POST /account/{id}/withdraw, POST /order (LIVE accounts only) |
POST /position/{id}/close, PUT /position/{id}, DELETE /order/{id} and
POST /account/{id}/withdraw do not run the geo gate, deliberately. Adding a
country to the block list must not trap an existing customer's funds — they can
still flatten their book and take their money out. If you need to stop that too,
block at your edge or CDN; this gate is application-level.
Two more properties of the geo gate worth knowing before you rely on it. It
resolves the country from approved KYC data, the self-declared profile country
and the CDN cf-ipcountry header, and a match on any signal blocks. And a
user with no resolvable country is allowed — the platform has no mandatory
country capture before KYC, so blocking unknowns would lock out every pre-KYC
demo user.
The KYC gate and the geo gate both fail open when their own lookups fail: settings unavailable, KYC tables absent, user row unreadable. Geo data being unavailable does not take trading down.
Next
- Settings and reference — every setting key, environment variable, permission key, scheduled job and notification template
- Running the desk — what the admin routes look like as screens
- External execution — the A-book routes in context