MT5 retcodes explained: why your order was rejected

9 min readMetaKit

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

RetcodeMT5 messageAPI error.codeDo this
10016Invalid stops in the requestinvalid_stopsRight side, at least stops_level away, re-quote
10014Invalid volume in the requestinvalid_volumeFloor to volume_step, check min and max, refresh spec
10038A close volume exceeds the current position volumeinvalid_volumeRead current volume, close at most that
10019There is not enough money to complete the requestorder_rejectedSmaller volume or close something
10018Market is closedsymbol_not_tradeableWait for the session, check quote time
10017Trade is disabledsymbol_not_tradeableCheck trade_mode, ask the broker
10042, 10043, 10044Only long / short / closing allowedsymbol_not_tradeableRespect trade_mode
10004Requoteorder_rejectedRe-quote, resend with a new key
10020Prices changedorder_rejectedRe-quote, consider wider deviation
10021There are no quotes to process the requestorder_rejectedBack off, re-quote
10022Invalid order expiration date in the requestorder_rejectedUse server time, or drop expiration
10027Autotrading disabled by clientorder_rejectedTerminal-side switch; check status, contact support
10031No connection with the trade serverorder_rejectedWait for connected, resend
10012Request canceled by timeoutorder_rejectedReconcile 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.