Docs

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 full

Test 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"
  }'
FieldMeaning
accountIds1 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, sideThe trade, as for a single order. Every account must know the symbol by that name.
volumeLots per account, as a string.
weightsOptional. Per-account volume in lots, keyed by account id, overriding volume for those accounts.
stopLoss, takeProfitOptional absolute prices, applied to every account's order.
barrierPolicyWhat to do about accounts that are not online in time. Default release-ready.
executeAtOptional 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.
expiresAtOptional time after which to stop waiting for accounts.
label, clientWaveId, commentYour 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 minimum

Use 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 guides

Then 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:

PolicyBehaviourUse it when
release-readySend 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-nothingSend to none unless every account is online.The strategy only makes sense across the whole set — a hedge split across accounts, for example.
waitKeep 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 stateMeaningWhat to do
filledThat account's order filled.Fetch GET /v1/orders/{orderId} for its price and brokerPositionId.
rejectedThat broker refused it. stateDetail says why.Usually margin or a closed market on that account. Tell its owner.
skippedNot 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.
unresolvedWe do not know yet whether it executed.Do not resend. Poll the order; we confirm it with the broker.
pending, preparing, ready, dispatchedStill 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:

AccountsSpread
1017 ms
5026 ms
10030 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.

On this page