Place one order across many MT5 accounts
Send the same MetaTrader 5 trade to many accounts in one API call — up to 500 on Enterprise (Starter 10, Scale 100): per-account volumes, barrier policies for accounts that are not online, scheduled execution with executeAt and expiresAt, per-account results, cancelling, and dispatch spread.
A multi-account order places one trade on many MetaTrader 5 accounts at the same moment — a signal service entering for every subscriber, a fund manager trading a set of managed accounts, a copier fanning out a master's trade. You make one request; every account gets its own order, sent together.
In the API these are called execution waves (/v1/execution-waves).
POST /v1/execution-waves one trade, many accounts → 201 at once
GET /v1/execution-waves/{id} poll until state = settled
GET /v1/orders/{orderId} any one account's order, in fullTest with demo accounts
Every key reaches real brokers — fx_test_ is only a label. Build and test with broker demo
accounts first.
The request
curl -sS -X POST "https://api.fxapis.com/v1/execution-waves" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Idempotency-Key: wave:signal_9931" \
-H "Content-Type: application/json" \
-d '{
"accountIds": [
"0d7c1f5e-4b6a-4c1e-9f0a-2d9e8c7b6a51",
"5b2e8a10-93c4-4f7d-8e61-0a9c2b4d6e13",
"c41d7e22-6f0b-4a95-b3e8-7d1f5a2c9b80"
],
"symbol": "EURUSD",
"side": "buy",
"volume": "0.01",
"weights": { "5b2e8a10-93c4-4f7d-8e61-0a9c2b4d6e13": "0.05" },
"stopLoss": "1.12900",
"takeProfit": "1.14200",
"barrierPolicy": "release-ready",
"label": "London open — EURUSD long",
"clientWaveId": "signal_9931"
}'| Field | Meaning |
|---|---|
accountIds | 1 to 500 account ids — up to your plan's accounts per multi-account order: Starter 10, Scale 100, Enterprise 500. An id listed twice is traded once. Every id must belong to your workspace, or the whole request is refused with 404 ACCOUNT_NOT_FOUND. |
symbol, side | The trade, as for a single order. Every account must know the symbol by that name. |
volume | Lots per account, as a string. |
weights | Optional. Per-account volume in lots, keyed by account id, overriding volume for those accounts. |
stopLoss, takeProfit | Optional absolute prices, applied to every account's order. |
barrierPolicy | What to do about accounts that are not online in time. Default release-ready. |
executeAt | Optional RFC 3339 time at which to send. Omit it to send as soon as every account is online — waiting up to 60 seconds for accounts still logging in. |
expiresAt | Optional time after which to stop waiting for accounts. |
label, clientWaveId, comment | Your own references; comment goes on each MT5 order. |
The response is 201 straight away, with the order in state planned and
one entry per account in legs. The work happens in the background: poll
GET /v1/execution-waves/{id}. Reference:
Place one trade on many accounts.
Multi-account orders are a plan feature; a plan without them answers 402
FEATURE_NOT_IN_PLAN, and a request larger than the plan allows answers
402 QUOTA_EXCEEDED before anything is created.
Sizing with weights
weights values are volumes, not multipliers. With "volume": "0.01" and
"weights": {"B": "0.05"}, account B trades 0.05 lots and every other account
trades 0.01.
A common way to size by account balance is to compute each account's lots on your side and send them all as weights:
from decimal import Decimal, ROUND_DOWN
def lots_for(balance: Decimal, reference_balance: Decimal, reference_lots: Decimal) -> str:
raw = reference_lots * balance / reference_balance
lots = raw.quantize(Decimal("0.01"), rounding=ROUND_DOWN) # the symbol's volume step
return str(max(lots, Decimal("0.01"))) # at least the minimumUse Decimal (or your language's decimal type) and send strings. Round down
to the symbol's volume step — rounding up quietly increases every account's
risk. A weight that is not a usable volume is refused with 400
INVALID_REQUEST naming the account.
Bring the accounts online first
Each account's order can only be sent while that account is online, and a
broker login takes about ten seconds in our tests. A multi-account order brings offline
accounts online itself: with no executeAt, it waits for them — up to 60
seconds — and sends to everyone together as soon as they are all ready (or as
soon as the only ones left need a person, such as a wrong password). After the
minute, the barrier policy decides about the stragglers: skipped
(release-ready), waited for until expiresAt (wait), or the whole order
held (all-or-nothing).
When the moment matters — a signal whose price will not wait a minute — bring
the accounts online before you create the order, and wait until they are
ready:
session.post(f"{API}/v1/accounts/prepare", json={"accountIds": account_ids}) # up to 200 per call
# poll GET /v1/accounts/{id}/status until "ready" for each, as in the language guidesThen create the multi-account order with executeAt at the moment you want to
enter, or without it to send as soon as every account is ready. See
Signals → prepare. For
accounts that must act at any moment, the always_on mode keeps them online
all the time — see Accounts → Modes.
Barrier policies
The barrier is the moment the orders are released. barrierPolicy decides
what happens when some accounts are not online at that moment:
| Policy | Behaviour | Use it when |
|---|---|---|
release-ready | Send to the accounts that are online; skip the rest (skipped). | The entry is time-sensitive. A partial entry beats a late one. This is the default. |
all-or-nothing | Send to none unless every account is online. | The strategy only makes sense across the whole set — a hedge split across accounts, for example. |
wait | Keep waiting past executeAt until every account is online, up to expiresAt. | Coverage matters more than the price. |
all-or-nothing is about sending, not about the result. A broker can still
reject one account's order after all were sent — for margin, say — and no API
can make a hundred independent brokers fill as one transaction.
If expiresAt passes and the policy still will not send, the order ends
abandoned with nothing sent. With release-ready or wait, reaching
expiresAt sends to whichever accounts are ready.
Following it to the end
import time
def follow(wave_id, timeout=300):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
wave = session.get(f"{API}/v1/execution-waves/{wave_id}").json()["data"]
if wave["state"] in ("settled", "cancelled", "abandoned"):
return wave
time.sleep(1)
raise TimeoutError(wave_id)
wave = follow(wave["id"])
print(wave["state"], wave["summary"], f"spread {wave['dispatchSpreadMs']} ms")
for leg in wave["legs"]:
print(leg["accountId"], leg["state"], leg["volume"], leg["orderId"], leg["stateDetail"])The order moves planned → preparing → armed → releasing → settled: accepted,
bringing accounts online, waiting for the moment, sending, and every account has
a result. cancelled and abandoned mean nothing was sent.
{
"state": "settled",
"dispatchSpreadMs": 30,
"summary": { "total": 100, "filled": 97, "rejected": 2, "skipped": 0, "unresolved": 1 }
}A multi-account order never reports a single success or failure. Each leg has its own state:
| Leg state | Meaning | What to do |
|---|---|---|
filled | That account's order filled. | Fetch GET /v1/orders/{orderId} for its price and brokerPositionId. |
rejected | That broker refused it. stateDetail says why. | Usually margin or a closed market on that account. Tell its owner. |
skipped | Not sent — the account was not online at the barrier, or trading is disabled for it. | Place a single order later if it still makes sense. |
unresolved | We do not know yet whether it executed. | Do not resend. Poll the order; we confirm it with the broker. |
pending, preparing, ready, dispatched | Still in progress. | Keep polling. |
Each leg's order is an ordinary order: it appears in GET /v1/orders, is
confirmed against the broker like any other, and is closed like any other —
with POST /v1/accounts/{id}/positions/{ticket}/close,
account by account. There is no multi-account close.
Scheduling with executeAt and expiresAt
{
"executeAt": "2026-10-01T07:00:00Z",
"expiresAt": "2026-10-01T07:00:30Z",
"barrierPolicy": "release-ready"
}The orders are sent at executeAt, not before — even if every account is
ready early. Bring the accounts online first, as above, so they are ready when
the moment comes. Times are RFC 3339; use UTC. A warm_on_demand account goes
offline after 15 idle minutes, so for an executeAt further ahead than that,
prepare the accounts shortly before it, or use always_on for them.
Cancelling
curl -sS -X POST "https://api.fxapis.com/v1/execution-waves/$WAVE_ID/cancel" \
-H "Authorization: Bearer $FXAPIS_KEY"Possible only before anything is sent: a scheduled order waiting for its
executeAt, or one waiting for accounts. Once orders are going out, cancelling
returns 409 WAVE_ALREADY_RELEASING — an order that has left for a broker
cannot be recalled, and cancelled would claim that nothing was sent.
Dispatch spread
dispatchSpreadMs is the time between the first account's order being sent
and the last. It is the number to watch: as it grows, your accounts are getting
different prices. Every multi-account order reports its own; these are from our
benchmark against an instant broker, so a real broker adds its own latency on top:
| Accounts | Spread |
|---|---|
| 10 | 17 ms |
| 50 | 26 ms |
| 100 | 30 ms |
Measured on our side against a broker answering instantly. Each broker adds its own latency on top — about 100 ms for a round trip to a London broker — and brokers fill at their own prices, so fills across accounts are close but never identical.
Retrying safely
Send an Idempotency-Key with every multi-account order. Sending the same key
again returns the order that key created — 200 with idempotent-replay: true
— rather than creating a second one, which would be a duplicate trade on every
account at once. Use one key per decision, such as wave:signal_9931. See
Idempotent MT5 orders.
Creating multi-account orders has its own rate limit per workspace — 10 a minute
with a burst of 2 — separate from single orders and reads; a 429
RATE_LIMITED carries retry-after. See Errors.
Related
- Build an MT5 trade copier — multi-account orders as the fan-out.
- Signals and click-to-trade — when each member approves their own trade instead.
- Trading → Multi-account orders and the API reference.
Connect MT5 accounts by login, password and server
How to connect a MetaTrader 5 account to the fxapis API: finding the exact server name, trading versus investor password, two-factor sign-in, handling credentials safely in your app, checking them immediately, needs-attention states, and disconnecting.
Build an MT5 trade copier with the API
Copy trades from a master MetaTrader 5 account to many follower accounts over HTTP: detect the master's deals by polling, fan out with multi-account orders, size by balance ratio, mirror closes and stop-loss / take-profit changes, and stay idempotent per master deal.