A rejected order arrives as a number. 10016. The broker's trade server
knows exactly why it said no, and it tells you in a five-digit MT5 retcode
and one line of text that reads like it was translated twice. Which fix
applies depends entirely on the number, and most of the time the fix is in
your code, not at the broker. So what follows is grouped by the fix.
How a rejection reaches you through the API
When the broker refuses an order sent via POST /v1/accounts/{id}/orders
(or a PATCH or DELETE on a position), you get a 422, never a 5xx:
{
"error": { "code": "order_rejected", "message": "There is not enough money to complete the request" },
"status": "rejected",
"retcode": 10019,
"retcode_message": "There is not enough money to complete the request"
}retcode is MT5's raw trade-server code and retcode_message its documented
text, and both are always in the body of a broker refusal. error.code is the
part you branch on. Where a retcode has an unambiguous meaning we map it to a
specific code: 10014 and 10038 become invalid_volume, 10016 becomes
invalid_stops, 10017, 10018 and 10042 through 10044 become
symbol_not_tradeable. Everything else is order_rejected and you branch on
retcode yourself.
Why 422 and not a 5xx: the request was understood, delivered, and answered.
The refusal is a property of the request (its volume, its price, the margin
it needs), not a fault in the infrastructure. A 5xx tells a client "retry
later" and that is the wrong instruction for a bad stop loss. The 5xx codes
are reserved for the cases where retrying is at least plausible:
502 upstream_error when the terminal is unreachable and
504 terminal_timeout when the broker didn't answer within 10 seconds.
Two more things. The API pre-validates volume and stops against the symbol
spec before the terminal is touched, so the two most common MT5 rejections,
10014 and 10016, mostly never reach a broker: you get the same
422 invalid_volume or 422 invalid_stops back in milliseconds with no
retcode, and the fix is identical. And a rejection is stored under your
Idempotency-Key for 24 hours, so the corrected attempt needs a new key. The
place-orders post has the full sequence.
10016 invalid stops: move them to the right side, further out
Message: "Invalid stops in the request". API code: invalid_stops.
Three causes. The stop or target is on the wrong side of the market (a stop
loss above the bid on a long). It's on the right side but closer than
stops_level points. Or, on a modification, the existing stop is within
freeze_level points of the current price and the broker won't let you touch
it (some servers report that as 10029, "Modification denied because order
or position is frozen").
The fix is arithmetic. Read digits and stops_level from
GET /v1/accounts/{id}/symbols/XAUUSD.m, compute a point as ten to the minus
digits, and keep every stop at least stops_level points from the close
price: bid for a long, ask for a short. On XAUUSD.m with digits: 2 and
stops_level: 30 that's 0.30 in price. Pending prices follow the same rule
plus the side rule: limits below the market for buys and above for sells,
stops the other way round.
A broker 10016 after the API's pre-check passed means price moved between
your quote and the broker's processing, or the broker is enforcing a wider
distance than its published stops_level (they do this around news).
Re-quote and add a margin of your own above stops_level.
10014 and 10038 invalid volume: the step, the bounds, or the close size
Messages: "Invalid volume in the request" and "A close volume exceeds the
current position volume". API code: invalid_volume.
10014 means the volume is below volume_min, above volume_max, or off
volume_step. The API checks all three and refuses before sending, and it
never rounds: 0.123 on a 0.01 step is an error, not 0.12. Floor to the
step yourself. A broker 10014 for a volume that passed the pre-check means
the spec changed since you cached it, or you cached the spec for XAUUSD and
traded XAUUSD.m, which the
suffix post
explains: different symbol, different step, different contract size.
10038 is specific to closing: DELETE /v1/accounts/{id}/positions/{ticket}
asked for more than is open, usually a partial computed from a stale volume
or two partials that raced. Read the current volume from /positions, floor
to volume_step, and send at most that. For a full close, omit volume
entirely.
10019 not enough money: trade smaller
Message: "There is not enough money to complete the request". API code:
order_rejected.
The broker computed the margin this position needs and your free margin
doesn't cover it. Margin is roughly volume times contract_size times price
divided by leverage, converted into the account currency via
currency_margin, and the broker's number is the only one that counts; the
API doesn't pre-compute it, because reproducing every broker's margin model
is a losing game.
The fix is a smaller volume, or closing something first. It is not a retry;
the same order gets the same answer until the account changes. And if you see
it at all, check free_margin before the next order, because an account near
its margin limit is an account near a stop-out. The
margin post covers
the arithmetic.
10018 market closed, 10017 trade disabled, 10042 to 10044: check trade_mode and the session
Messages: "Market is closed", "Trade is disabled", and the three one-sided
ones: "Only long positions are allowed", "Only short positions are allowed",
"Only position closing is allowed". API code for all of them:
symbol_not_tradeable.
trade_mode in the symbol spec tells you most of this before you send:
full means anything goes, long_only and short_only refuse the other
side, close_only lets you exit but not enter (common on CFDs approaching
expiry), disabled means the broker switched the symbol off. The API checks
trade_mode against your side and refuses before the broker sees it.
10018 comes from the broker, because the API doesn't model trading
sessions: weekend, holiday, the daily maintenance break on indices and
metals. The tell is in GET /v1/accounts/{id}/quote: if time is hours
old, nobody is quoting. Don't retry in a loop; schedule the order for the
session open.
10004 requote, 10020 prices changed, 10021 off quotes: re-quote, then set deviation
Messages: "Requote", "Prices changed", "There are no quotes to process the
request". API code: order_rejected.
Three views of the same event. 10004 is an instant-execution broker
offering a new price instead of filling at the one you asked for. 10020 is
a market-execution broker saying price moved further than your deviation
allows before it could match you. 10021 means there was no tradable price
at all at that instant: news spike, rollover, a thin symbol.
The fix is the same for all three. Fetch a fresh bid and ask from
/quote, rebuild the order, send it under a new idempotency key, and
consider a wider deviation (points, default 20). Don't widen it without
limit; deviation is the slippage you've agreed to eat. For 10021, back
off a couple of seconds first. If it persists, that is not a market you want
to be entering anyway.
10022 invalid expiration: the broker's clock, not yours
Message: "Invalid order expiration date in the request". API code:
order_rejected.
You sent expiration on a pending order and the broker compared it with its
own clock, which is server time, not UTC. An expiry comfortably in the future
in UTC can already be in the past on a server running three hours ahead. Some
brokers also restrict expiry modes per symbol and reject any specific
timestamp on symbols that only take good-till-cancelled or end-of-day.
Work out the server's offset first (the
server time post shows how) and
set the expiry well clear of it. If the broker still refuses, omit
expiration and cancel the order yourself with
DELETE /v1/accounts/{id}/orders/{ticket} when you no longer want it.
10027 autotrading disabled: a switch in the terminal
Message: "Autotrading disabled by client". API code: order_rejected.
Nothing in your request causes this. The terminal's AutoTrading toggle is
off, so it refuses to send any algorithmic order. On a terminal you run
yourself, click the button. On MetaKit the terminal is ours and autotrading
is enabled when it starts, so you should never see this; if a connected
account returns 10027 anyway, email [email protected] with the account
id. The sibling 10026, "Autotrading disabled by server", is the broker
disabling algo trading on your account, and only the broker can undo it.
10031 no connection and 10012 timeout: wait, then reconcile
Messages: "No connection with the trade server" and "Request canceled by
timeout". API code: order_rejected.
10031 means the terminal is running but has lost its link to the broker.
Your order went nowhere. Back off and watch the account's status; it
usually flips away from connected shortly, after which you get
409 account_not_connected instead. Resend once it's back.
10012 is more dangerous. The request was sent and the wait for an answer
was abandoned, which is the same ambiguity as 504 terminal_timeout: the
order may or may not have executed. The difference is only who gave up, the
API's 10 second cap or the trade server's own timeout. Handle both
identically: read /positions or wait for the position.opened webhook, and
only send again under a new key if the position isn't there.
The list on one screen
| Retcode | MT5 message | API error.code | Do this |
|---|---|---|---|
| 10016 | Invalid stops in the request | invalid_stops | Right side, at least stops_level away, re-quote |
| 10014 | Invalid volume in the request | invalid_volume | Floor to volume_step, check min and max, refresh spec |
| 10038 | A close volume exceeds the current position volume | invalid_volume | Read current volume, close at most that |
| 10019 | There is not enough money to complete the request | order_rejected | Smaller volume or close something |
| 10018 | Market is closed | symbol_not_tradeable | Wait for the session, check quote time |
| 10017 | Trade is disabled | symbol_not_tradeable | Check trade_mode, ask the broker |
| 10042, 10043, 10044 | Only long / short / closing allowed | symbol_not_tradeable | Respect trade_mode |
| 10004 | Requote | order_rejected | Re-quote, resend with a new key |
| 10020 | Prices changed | order_rejected | Re-quote, consider wider deviation |
| 10021 | There are no quotes to process the request | order_rejected | Back off, re-quote |
| 10022 | Invalid order expiration date in the request | order_rejected | Use server time, or drop expiration |
| 10027 | Autotrading disabled by client | order_rejected | Terminal-side switch; check status, contact support |
| 10031 | No connection with the trade server | order_rejected | Wait for connected, resend |
| 10012 | Request canceled by timeout | order_rejected | Reconcile against /positions before resending |
Log the retcode on every rejection, not just the message. In six months,
when a broker widens a stops level over a weekend and your fills dry up, the
number is what you'll grep for. The complete error table is under
"Conventions" in llms.txt.