Accounts
Connecting MT5 accounts, keeping them online, and what each state means.
An account is an MT5 login you have connected to fxapis. Its state tells you whether it can trade right now; its mode decides when it is online.
Modes
Set when you connect the account, and changed any time with
POST /mode.
| Mode | Online | First order | Best for |
|---|---|---|---|
always_on | Once brought online, it stays online — never taken offline for being idle, and brought back by itself within minutes if its terminal stops (a refused login or 2FA waits for you) | Immediate | Accounts that must act the instant something happens — copied signals, a market open |
warm_on_demand | When you trade, then offline by itself when idle | After a short connect | Most integrations, and signal services |
cold | Only when you ask | After you bring it online | Accounts you trade on a schedule |
warm_on_demand is the sensible default (the API's default is cold, so set
it). The first order after a quiet spell waits while the account connects —
about 10 seconds in our measurement — and every order
after that is immediate. To avoid even that wait, bring accounts online ahead of
time with POST /v1/accounts/prepare.
A mode change applies the next time the account comes online. To apply
always_on straight away, bring the account online after switching.
States
Your code can act on four groups. The exact values are what the API returns.
Online — can trade
| State | Meaning |
|---|---|
ready | Online and accepting orders. |
executing | Online; an order is being placed right now. |
Connecting — wait
| State | Meaning |
|---|---|
provisioning, starting, connecting | Coming online and logging in at the broker. |
synchronizing | Logged in; prices are still arriving. Orders are refused until ready, so none goes out against incomplete prices. |
reconnecting | The broker connection dropped and is being restored. |
degraded | Online but unhealthy — slow or stale prices. Usually recovers by itself; restart it if not. |
Offline — not connected to the broker right now
| State | Meaning |
|---|---|
created | Connected, never brought online yet. |
standby | Offline and known to work. A warm_on_demand account comes online on its next order; any account comes online when you ask (/warm). |
cooling | Going offline. |
offline | Disconnected (the stored password has been erased), or its terminal stopped answering (the password is kept — bring it online again). |
Needs attention — a person must fix something
| State | Meaning | What to do |
|---|---|---|
invalid_credentials | The broker rejected the login — a wrong password, or a login that does not exist on that server. | Check problem.otherServers and try the likely one with POST /v1/accounts/{id}/server; or send the corrected trading password to POST /v1/accounts/{id}/password. |
trading_disabled | The broker has disabled trading for this login. | Ask the broker, or the account's owner, to enable trading. (An investor password instead reaches ready and its orders are rejected.) |
needs_2fa | The account requires a second factor. | Turn two-factor off for this account, or use another. |
needs_certificate | The broker requires a client certificate. | Contact us. |
error | Something temporary went wrong. | Retryable — bring it online again. |
The first four will not improve with retries, and repeated failed logins are how a broker locks an account. So we stop trying: bringing one of these online returns 409 until the cause is fixed.
What is wrong, in words: problem
Every account carries problem: null while it is fine or on its way, and
otherwise what is wrong and how to fix it — the same thing the console shows.
"problem": {
"code": "LOGIN_REFUSED",
"title": "The broker refused the login",
"explanation": "JustMarkets-Demo3 did not accept login 2002126135 with this password. Either the password is not the account's trading password …, or this login does not exist on JustMarkets-Demo3 …",
"detail": "the broker refused the login on mt5ldn5.srip.net:8750: -6 Terminal: Authorization failed",
"otherServers": ["JustMarkets-Demo", "JustMarkets-Demo2", "JustMarkets-Live"],
"actions": [
{ "kind": "switch_server", "server": "JustMarkets-Demo" },
{ "kind": "switch_server", "server": "JustMarkets-Demo2" },
{ "kind": "replace_credentials" }
]
}A broker answers "authorization failed" both for a wrong password and for a
login that lives on a different server — and brokers run several servers with
near-identical names. otherServers lists the broker's other servers, most
likely first: servers accounts here have logged in to, then the broker's main
server, then the closest names. Show them to your user, or try the first
with POST /v1/accounts/{id}/server — it reuses the stored password.
When a terminal cannot find the account's server — a name that is not an MT5
server, such as a broker's label for an account type — an account that has
tried and never logged in carries "code": "SERVER_NOT_FOUND", with the
broker's real servers in otherServers and a switch_server action for each.
POST /v1/accounts/{id}/server with the right one keeps the stored password.
Write the server exactly as MetaTrader's File → Login dialog or the broker's
welcome email shows it.
When accounts cannot come online because of a problem on our side, accounts
on their way online (created, standby, starting, offline, error)
carry "code": "PLATFORM_ISSUE" with no actions: nothing about the account
needs changing, and retrying will not help. A failed
POST /v1/accounts/{id}/warm says the same in its message. It clears by
itself once it is fixed; status.fxapis.com has
updates. A refused login, 2FA, a certificate or trading disabled keep their own
codes: those come from the broker.
Alerts in the console
When an account enters one of the states above, the console opens an alert:
the bell at the top of every console page counts the ones you have not read,
Notifications lists them with what the broker said and the one fix (on the
account's page, where the problem card has it), and the account's page shows
the same card. An alert closes by itself once the account leaves that state;
resolved ones stay listed for a week. An error is shown only if it lasts two
minutes — most clear in seconds. Everyone in the workspace sees them; only
owners and admins can make the fix. In-app only: nothing is emailed, and there
is no API for them — integrations use the account.state_changed webhook or
problem.
POST /v1/accounts/{id}/server
Moves the account to another server, keeping the stored password — for a
login added under the wrong server name, so nobody types the password again.
With connect: true (the default) it is brought online at once and the answer
is back in seconds; poll the account or its events. 409 ACCOUNT_EXISTS
when that login is already connected on that server; 409 NO_PASSWORD
when no password is stored (use /password).
curl -X POST https://api.fxapis.com/v1/accounts/$ID/server \
-H "Authorization: Bearer $FXAPIS_KEY" -H "content-type: application/json" \
-d '{"server":"JustMarkets-Demo"}'GET /v1/brokers/servers
A server picker for your own connect form, so the people connecting choose
their server instead of typing it exactly. Search by what they type — a
broker's name (vantage), the start of a server (JustMarkets-De), with or
without dashes and spaces — from two characters, debounced:
curl "https://api.fxapis.com/v1/brokers/servers?q=justmarkets" -H "Authorization: Bearer $FXAPIS_KEY"{ "data": [
{ "name": "JustMarkets-Demo", "company": "Just Global Markets Ltd.", "kind": "demo", "proven": true },
{ "name": "JustMarkets-Live", "company": "Just Global Markets Ltd.", "kind": "live", "proven": false }
] }Servers accounts on fxapis have logged in to come first (proven), then names
starting with what was typed, then the kind asked for, then the broker's main
server. A broker nobody here has used yet is found too: with fewer than five matches
and at least three characters typed, the broker search is asked once (that
answer can take a second or two), and a search that found nothing is not
repeated for 24 hours. limit sets how many come back (1–50, default 20). Always
let people use exactly what they typed — a server that is not listed can
still be connected. Without q, the servers used most here.
GET /v1/accounts/{id}/events
The account's history in words, newest first — added, logging in, refused by
the broker, online, moved to another server, taken offline — each with a
tone (success, info, warning, danger), a title, an optional
detail, and who did it (key, member, staff, system). The console's
activity list is this endpoint. limit up to 200 (default 50).
Endpoints
Full request and response shapes are in the API reference. What follows is what the reference cannot tell you.
POST /v1/accounts
Connects the account and stores its password, encrypted. It does not log in yet.
The same login and server can be connected only once per workspace — a
second copy would mean two places able to trade the same money — so a duplicate
is refused with 409 while it is still connected. A login you disconnected
earlier is reconnected instead: see /disconnect.
If the password cannot be stored safely, the account is not created and you get 503, rather than an account that can never come online.
POST /v1/accounts/{id}/warm
Brings the account online. Returns 202 straight away with a pollUrl; poll
it until the state is ready.
Safe to call repeatedly: for an account that is already online it returns
alreadyRunning: true and does nothing else.
POST /v1/accounts/{id}/cool
Takes the account offline. It stays connected, in standby, and comes back on
the next order or when you ask. It works from any state: an account in
created or a needs-attention state that still has a terminal running has it
stopped, and keeps the state that says what it still needs.
Refused with 409 while an order is being placed — stopping mid-order would leave its result uncertain. Wait a moment and try again.
Open positions are not affected. Stop losses and take profits live at the broker and keep working while the account is offline.
POST /v1/accounts/{id}/restart
Takes the account offline and brings it back — for an account that is online
but misbehaving (degraded, stale prices). Same 409 while an order is in
flight.
POST /v1/accounts/{id}/disconnect
Takes the account offline and erases the stored password. The account and
its history remain, in offline. To reconnect, send the same login and server
to POST /v1/accounts with the password: the same account comes back, with
200.
Use this when a customer leaves. A password you no longer need is one we should no longer hold.
DELETE /v1/accounts/{id}
Deletes the account for good: its terminal stops, its password is erased, and the record goes with its orders, deals and positions. It cannot be undone — disconnect instead if you want the history to stay readable. The response says how much went with it.
Refused with 409 while an order is in flight (ACCOUNT_EXECUTING), while a
multi-account order that includes it has not finished (ACCOUNT_IN_WAVE), and
when our last look found open positions (ACCOUNT_HAS_POSITIONS). Open
positions stay open at the broker with their stops, but once the account is
deleted they cannot be shown or closed here: close them first, or send
?force=true to delete anyway.
Alert hooks stop sending to the account, and a hook left with no accounts is switched off. This month's usage is unaffected. The same login and server can be connected again afterwards, as a new account with a new id.
POST /v1/accounts/{id}/password
Replaces the stored password — after it changed at the broker, or when the
account is in invalid_credentials. Add server to correct the server name.
Any running terminal is stopped first, and waited for (409 while an order
is in flight); the account lands in created. Then bring it online with
POST /v1/accounts/{id}/warm and poll the status: that is a fresh login with
the new password. Nothing needs taking offline in between.
POST /v1/accounts/{id}/mode
Changes the mode. Applies the next time the account comes online.
GET /v1/accounts/{id}/status
The cheap one, made for polling. Every two seconds is fine.
GET /v1/accounts/{id}/figures
Balance, equity, margin, free margin, margin level, credit and open profit, as decimal strings in the account's currency, read from its MetaTrader terminal.
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/figures" -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": { "accountId": "…", "balance": "10000.50000000", "equity": "10037.75000000",
# "margin": "120.00000000", "freeMargin": "9917.75000000", "marginLevel": "8364.79166667",
# "credit": "0.00000000", "profit": "37.25000000", "currency": "USD", "leverage": 500,
# "marginMode": "hedging", "observedAt": "2026-10-15T12:00:00.120Z", "ageMs": 4210,
# "source": "snapshot" } }- Without
fresh, it answers the last read (source: "snapshot"). While the account is online that read is taken every 15 seconds, withobservedAtsaying when.GET /v1/accounts/{id}and the account list carry the same figures, withfiguresObservedAt. An account never read hasnullfigures. - With
fresh=true, the terminal is asked now (source: "live"). The account must beready: offline,executingan order ordegradedis409 ACCOUNT_NOT_READY. A terminal still running an older worker answers501 CAPABILITY_UNAVAILABLEinstead — rare in practice; leave outfreshfor that account meanwhile. Callers within the same second share one read, and only a read that reaches the terminal is charged to the account's live budget (Errors). marginLevelisnullwhile no margin is used. Equity over no margin is undefined, not zero.creditandprofitare filled for every online account;nullonly for one never read.marginModeisnetting(one position per symbol) orhedging(one per order),nulluntil the terminal has reported it.- Webhooks do not carry figures.
account.state_changedfires on a change of state, and balance and equity change without one. Read them here.
Polling well
import time, httpx
def wait_until_ready(client, account_id, timeout=120):
"""Polls until the account is ready, or until it is clear it will not be."""
NEEDS_ATTENTION = {"invalid_credentials", "needs_2fa", "needs_certificate", "trading_disabled"}
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
state = client.get(f"/v1/accounts/{account_id}/status").json()["data"]
if state["state"] == "ready":
return state
if state["state"] in NEEDS_ATTENTION:
# Not retryable: report the reason now instead of timing out later.
raise RuntimeError(f"{account_id} needs attention: {state['state']} — {state['detail']}")
time.sleep(2)
raise TimeoutError(f"{account_id} did not reach ready within {timeout}s (last: {state['state']})")Two habits that save support tickets:
Stop on a "needs attention" state. Polling for two minutes on
invalid_credentials tells you nothing the first response did not.
Allow more time than feels necessary. About 10 seconds in our measurement, and a busy broker can take longer. A 120-second timeout costs nothing when things go well.