Accounts and lifecycle
The account lifecycle: states, modes, and what to poll.
An account is an MT5 login we hold credentials for. Its state says what is true of it right now; its mode says how eagerly we keep a terminal running for it.
Modes
Set at creation, changed with /mode.
| Mode | A terminal is running | First order | Idle cost |
|---|---|---|---|
always_on | Always | Immediate | Highest |
warm_on_demand | While in use | After a warm-up | Low |
cold | Only when you ask | After an explicit warm | None |
always_on is for accounts that must react to something outside their own
control — a copied signal, a market open. The account rests at ready rather
than standby, which is precisely what is being paid for.
warm_on_demand is the sensible default for most integrations. The first order
after an idle period waits for the terminal; the rest do not.
cold is for accounts you drive on a schedule and are content to warm
yourself.
Changing mode does not move a running terminal. Switching to always_on takes
effect on the next warm, so the call stays fast and predictable — warm the
account afterwards to apply it now.
States
created ─→ provisioning ─→ standby ─→ starting ─→ connecting ─→ synchronizing ─→ ready ⇄ executing
│
standby ←──────────── cooling ←─────────────────────────┘Resting
| State | Meaning |
|---|---|
created | Stored, never connected. Nothing is running. |
provisioning | Being prepared. Brief. |
standby | Connected and known-good; no terminal running. |
offline | Disconnected. The credential has been erased. |
Coming up
| State | Meaning |
|---|---|
starting | A runtime is starting. No broker contact yet. |
connecting | Logging in at the broker. |
synchronizing | Logged in; symbols and prices still filling. |
synchronizing is not cosmetic. The terminal will answer, and the prices behind
it are incomplete. An order sent here goes out against data that is not there,
so only ready accepts one.
Working
| State | Meaning |
|---|---|
ready | Logged in, synchronized, accepting orders. |
executing | An order is in flight. |
cooling | Shutting down cleanly. |
Unhappy
| State | Needs a human | Meaning |
|---|---|---|
degraded | No | Connected but unhealthy — stale quotes, a slow terminal. May recover on its own. |
reconnecting | No | The broker connection dropped; we are re-establishing it. |
error | No | Something transient failed. Retryable. |
invalid_credentials | Yes | The broker rejected the login. |
trading_disabled | Yes | Login succeeded, trading is refused. |
needs_2fa | Yes | A second factor is required. Not supported headlessly. |
needs_certificate | Yes | The broker requires a client certificate. |
The four that need a human will not improve by being retried, and repeated failed logins are how a broker locks an account — so warming one returns 409 rather than trying again. Fix the underlying thing and reconnect.
Endpoints
Full request and response shapes are in the API reference. What follows is what the reference cannot tell you.
POST /v1/accounts
Stores the account and seals the password. Does not log in.
Duplicate login + server for one tenant is rejected with 409. Two
records for one broker login would mean two execution authorities for the same
money, which is the split-brain the runtime leases exist to prevent — so it is
refused at the front door too.
If the credential cannot be stored, the account is not created and you get 503. A half-made account that can never connect is worse than no account.
POST /v1/accounts/{id}/warm
Starts a terminal. Returns 202 with a pollUrl.
Idempotent in the way that matters: warming an already-running account returns
alreadyRunning: true and starts nothing. Exactly one terminal runs per
account, enforced in the database rather than by a check — two terminals on one
broker account would both accept an order, and the customer would get a double
position.
POST /v1/accounts/{id}/cool
Stops the terminal. The account stays connected, in standby.
Refuses with 409 while executing. Stopping mid-order turns a known
outcome into an unknown one, which then has to be reconciled against the
broker — and a retry on top of that is a duplicate trade. Wait for the order to
settle.
Open positions are untouched. A stop loss or take profit attached at the broker keeps working with no terminal running. What stops is anything conditional on our side.
POST /v1/accounts/{id}/restart
Cool then warm, for a terminal that is connected but misbehaving — degraded,
stale quotes, a stuck sync. Same 409 while an order is in flight.
POST /v1/accounts/{id}/disconnect
Stops the terminal and deletes the stored password. The account record and
its history remain, in offline. Reconnecting means sending the password again.
This is the endpoint for a customer leaving. A disconnected account that still holds a usable password is a credential nobody is watching.
POST /v1/accounts/{id}/mode
Changes the mode. Takes effect on the next warm.
GET /v1/accounts/{id}/status
The cheap one, for polling. Every two seconds is fine.
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_HUMAN = {"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_HUMAN:
# Not a timeout and not retryable. Fail now, with the reason, rather
# than spending two minutes to report something less useful.
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 things that separate a good client from one that generates support tickets:
Stop on a terminal state. Polling for two minutes on invalid_credentials
tells you nothing that the first response did not.
Give it longer than feels necessary. Fifteen seconds is typical, thirty is not unusual, and a broker under load can take longer. A timeout of 120 seconds costs nothing when things go well.