TradeRailDocs
Kept in this browser only
Kept in this browser only

Introduction

The TradeRail API gives your software full access to MetaTrader 5 trading accounts: open a session for an account, read balances and live prices, place and manage orders, pull trade history and statistics, and act on many accounts with one call. It is a managed service — there is nothing to install or run.

All requests go to one base URL, over HTTPS:

Base URL
https://api.traderail.cloud

What you need

  • An API key, from your dashboard. Every /v1 request carries it.
  • The MT5 account's login, password and server name — exactly as the broker gave them. You need them to open a session, and again whenever a session has ended (idle limit, disconnect, or failed automatic recovery), so store them securely or be ready to ask the account holder again.

How requests look

Every /v1 endpoint is an HTTP GET with its parameters in the query string, and returns JSON (exceptions: ConnectEx returns its session id as plain text, and CheckConnect returns plain-text ok, or HTTP 403 with an empty body). Calls that act on an account name it with ?id=. The fastest way in is the Quickstart; every endpoint below also has a Try it panel that sends a real request with your key.

Authentication

Send your API key in a request header named ApiKey — not as a query parameter and not in an Authorization header. Every /v1 endpoint requires it, including Ping.

Authenticating a request
curl https://api.traderail.cloud/v1/Ping \
-H "ApiKey: YOUR_API_KEY"

A missing, unknown or deactivated key is answered with HTTP 401:

401 Unauthorized
{
"message": "ApiKey required",
"error": "Unauthorized",
"statusCode": 401
}

Bearer-token endpoints

The copy-trade endpoints under /orchestrator and the real-time stream use a different scheme: Authorization: Bearer <token>. Your ApiKey is not accepted there. Service health is the only endpoint that needs no credentials at all.

Calling from a browser

The API accepts cross-origin requests from any origin, and allows the ApiKey, Authorization and Content-Type headers. Cookies are not used — authenticate with headers only.

Keep your API key secret
  • Anyone holding your key can call the API as you, including placing trades. Keep it on your server and out of public repositories and client-side code you ship to others.
  • If a key leaks, rotate it from the dashboard; the old key stops working at once.

Quickstart

From nothing to a placed order in four requests. Paste your API key into the bar at the top and these snippets fill it in.

  1. Check your key

    cURL
    curl https://api.traderail.cloud/v1/Ping -H "ApiKey: YOUR_API_KEY"
    # {"code":"OK"}
  2. Open a session for an account

    Use the MT5 login, password and server name. The first connect for an account can take a little while; the answer is the account's session id as plain text. Always URL-encode the password — --data-urlencode does it for you.

    cURL
    curl -G https://api.traderail.cloud/v1/ConnectEx \
    -H "ApiKey: YOUR_API_KEY" \
    --data-urlencode "user=700100200" \
    --data-urlencode "password=YOUR_PASSWORD" \
    --data-urlencode "server=PUPrime-Demo"
    # acct-700100200
  3. Read the account and a price

    Pass the session id (or just the login) as ?id=. Symbols can be given by their base name — the API finds the broker's exact symbol for you.

    cURL
    curl "https://api.traderail.cloud/v1/AccountSummary?id=acct-700100200" -H "ApiKey: YOUR_API_KEY"
     
    curl "https://api.traderail.cloud/v1/GetQuote?id=acct-700100200&symbol=EURUSD" -H "ApiKey: YOUR_API_KEY"
  4. Place an order

    A 0.01-lot market buy. Trade calls answer HTTP 200 whatever the outcome, so check code in the body — "OK" means the broker accepted it. See Errors.

    cURL
    curl -G https://api.traderail.cloud/v1/OrderSendTask \
    -H "ApiKey: YOUR_API_KEY" \
    --data-urlencode "id=acct-700100200" \
    --data-urlencode "symbol=EURUSD" \
    --data-urlencode "operation=Buy" \
    --data-urlencode "volume=0.01"

Accounts & sessions

Before you can read from or trade on an account, it needs a live session, opened with ConnectEx. Once it is open, the service keeps it running and restores it automatically if it drops.

Identifying an account

Calls that act on an account take ?id=, which accepts either the session id ConnectEx returned (acct-700100200, lower-case prefix) or the bare login (700100200). Anything else returns HTTP 400 on data and trade calls; the session checks (CheckConnect, ConnectByToken and Reconnect) treat it as no session.

How long it takes

  • Opening a session for an account that is not running yet can take up to about two minutes, and longer when other accounts are being started at the same moment — new sessions start one at a time. Give ConnectEx a client timeout comfortably above two minutes.
  • Calling ConnectEx again with the same password while the session is live returns at once.
  • Requests for the same account are carried out one at a time, in order. Different accounts never wait on each other.

When a session ends

  • Idle limit. If you set idleHours on ConnectEx (or the service has a default), the session stops after that many hours with no calls that touch the account. Reads and trades count as activity; status checks such as CheckConnect do not.
  • Disconnect. POST /orchestrator/disconnect stops a session. GET /v1/Disconnect does not — it always answers OK and changes nothing.
  • Repeated failures. If automatic recovery keeps failing, the account is taken out of recovery.

After a session ends, account calls fail as not connected until you call ConnectEx again with the password and server.

Changing an account's password

While a session is live, ConnectEx with a different password is refused with CREDENTIALS_MISMATCH once a password has been verified for that account, and the running session is left untouched — a mistyped password can never disturb a working account. Until a password has been verified, a repeat ConnectEx on a live session returns the id without checking the password, so a success does not prove the password is correct. To switch to a new password, stop the session first, then connect with the new one.

Symbols

Brokers name the same instrument differently — one calls it EURUSD, another EURUSD.s. Endpoints that take a symbol (GetQuote, PriceHistoryEx, TickHistory and OrderSendTask) let you pass the base name and find the account's own symbol for you, in this order:

  1. An exact match for the account's broker is used as is.
  2. A dot-suffix the broker does not use is dropped — EURUSD.pro becomes EURUSD on a broker that lists plain EURUSD.
  3. Otherwise the broker's dot-suffixed variant is used — XAUUSD becomes XAUUSD.s. When there are several, the shortest wins.
  4. If nothing matches, the name is sent unchanged, and an unknown symbol fails.

Only suffixes that start with a dot are recognised, and matching is case-sensitive. Names like EURUSDm or EURUSD-ECN, aliases such as GOLD, and separators such as EUR/USD are not translated — to target one exact variant, pass the name from SymbolList, which always returns the broker's exact names.

The account's symbol list is loaded on the first symbol call and reused for about ten minutes, so that first call can be a little slower on brokers with many symbols. Where a response names a symbol (OpenedOrders, history rows, TickHistory), it is the broker's exact name, suffix included. GetQuote, PriceHistoryEx and order results do not return the symbol; use SymbolList or TickHistory to learn the exact name.

Times & numbers

Sending times

Wherever an endpoint takes from, to or expiration, it accepts epoch seconds, epoch milliseconds (any whole number above 100,000,000,000), or ISO 8601 such as 2026-10-07T12:00:00Z. A time without a zone is read as UTC, and a bare date means midnight UTC — so to=2026-09-30 stops at the start of 30 September.

A value that cannot be parsed is not rejected
  • It silently falls back to the parameter's default, so double-check formats when results look wrong.

Times you receive

Timestamps on market data and trades follow the broker's trade-server clock, which on many brokers runs a few hours ahead of UTC. Do not treat them as UTC. History windows filter on that same clock, so filter with the timestamps you see in responses.

Numbers

Money values in history and statistics are in the account's deposit currency, rounded to 2 decimal places. Prices for stop loss, take profit and limit orders are absolute prices, volumes are in lots and slippage is in points.

Errors

How a failure is reported depends on the kind of endpoint — check the right thing for each.

Endpoint kindHow a failure looks
Any, bad keyHTTP 401
Reads (account, quotes, history)HTTP 400 for bad input, 502 when the account is not connected or did not answer in time. Body { "code": "ERROR", "message": "…" }.
Trades (OrderSendTask, Modify, Close, Cancel)HTTP 200 for every trade outcome — even when the broker refuses. Check code in the body: "OK" or "ERROR". Only a bad ?id= (400) or a bad key (401) changes the status.
ConnectEx{ "code", "message" } with the status from errorReplyStatusCode (default 405).

Messages are human-readable and may change — branch on the HTTP status and on code, never on the message text.

Connect failures

LOGIN_REJECTEDThe broker refused the login — wrong login, password or server. Usually reported within seconds.
NOT_AUTHORIZEDThe session started but never reached the broker's trade server — wrong credentials, or the broker is unreachable.
CREDENTIALS_MISMATCHThe account is already running under a different password. Nothing was changed.
CONNECT_TIMEOUTThe session did not come up in time. Safe to retry.
CONNECT_FAILEDAny other failure to start the session.

Trade failures

A refused trade returns { "code": "ERROR", "message", "error": { "errorCode", … } }. Branch on error.errorCode, which is always a string ("10019", "-2", "TIMEOUT"); the numeric form, when there is one, is in error.mqlErrorTradeIntCode.

"10004"–"10046"The broker refused the request and nothing was done (e.g. "10019" not enough money, "10018" market closed).
"-2"Invalid or missing parameters.
"-3"Unknown symbol.
"-8"Ticket not found.
"-100"Trading on a live (real-money) account is not enabled for your service.
"UNSUPPORTED"Unknown operation on OrderSendTask. Nothing was sent.
Any other numberA platform error (e.g. "4756"), reported when the broker returned no code. Nothing was done.
"TIMEOUT"Outcome unknown — the order may be live.
"NOT_CONNECTED"The account is not connected, or its connection dropped mid-request — also outcome unknown.

Retrying safely

After TIMEOUT or NOT_CONNECTED on a trade, check OpenedOrders before sending again. A clientOrderId on OrderSendTask guards against duplicates for five minutes, but only while the first attempt is still running or after it succeeded or returned TIMEOUT: a repeat with the same id then returns that first result and sends nothing new. Any other failure, NOT_CONNECTED included, releases the id, so a repeat sends a new order. The ids are kept in memory only, so a service restart forgets them. Modify, close and cancel calls are not de-duplicated.

Sessions & account

Open a session for an MT5 account, check that it is alive, and read its balance and equity.

GET/v1/ConnectEx

Connect an account

Start a session for an MT5 account, or confirm the one already running, and get the account id that every other call uses.

Call ConnectEx with the account's login, password and trade-server name. If the account has no live session, the service signs it in and only answers once the account is connected to the broker's trade server. It waits up to about two minutes for that, and longer in total if other accounts are being started at the same moment, because starts are queued. If the session is already live and you send the same password, the call returns almost immediately and nothing is restarted. On success the body is the account id as plain text (acct-<login>), which you pass as ?id= on every other /v1 call. On failure the body is JSON { "code", "message" } with the HTTP status set by errorReplyStatusCode (405 unless you choose another), so branch on code, not on the status.

ApiKey headerchanges state

Parameters

userinteger (positive; the bare login, not the acct- form)required
The MT5 account login (account number).
passwordstring (URL-encoded)required
The account's MT5 password (master or investor). Used to sign the account in, and to check the password when the account already has a live session.
serverstring (URL-encoded)required
The broker's trade-server name exactly as MT5 shows it.
symbolstring (letters, digits and . _ # / + ! -, up to 64 characters)optional
Optional symbol used while a new session starts. Usually you leave it out and the service picks a suitable symbol for the broker. A value with other characters, or longer than 64 characters, is silently ignored and the broker default is used. If you pass a well-formed symbol the broker does not offer, the session cannot come up and the call fails.
idleHoursnumber (hours, >= 0, fractions allowed)optional
Stops the session automatically after this many hours with no account activity. Fractions are allowed: 0.5 means 30 minutes. 0 means never stop. Leave it out to use the service default. A negative or non-numeric value is ignored and the service default applies.
Default service default
errorReplyStatusCodeinteger (an HTTP status code)optional
The HTTP status used for this call's failure responses (invalid login, not connected, and every sign-in failure). It does not change the 401 for a missing or invalid ApiKey. 0 or a non-numeric value falls back to 405. The JSON body still carries the specific failure code.
Default 405

Returns

The account id as plain text, not JSON. Pass it as ?id= on later /v1 calls.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
405INVALID_REQUESTuser is missing, is not a whole number, or is not positive. Message: "invalid user (login)".
405NOT_CONNECTEDpassword or server was left out and the account has no live session. Without both, ConnectEx only confirms an existing session and never starts one.
405CREDENTIALS_MISMATCHA session for this login is live, or is already starting, and a different password has already been verified for it. The running session is left exactly as it was and nothing is restarted. The message explains how to change the password.
405LOGIN_REJECTEDThe broker refused the sign-in because the login, password or server name is wrong. This usually comes back within seconds, and the message includes the broker's rejection line.
405NOT_AUTHORIZEDThe account started but never reached the broker's trade server within the wait window (about 75 seconds). Either the login, password or server name is wrong, or the broker cannot be reached right now.
405CONNECT_TIMEOUTThe session did not come up before the connect timeout (about two minutes; the message gives the exact number of seconds). Check the login, password and server name, then retry.
405CONNECT_FAILEDThe session could not be started for some other reason. Retry later.
Good to know
  • The success body is plain text such as acct-700100200, not JSON. Read it as a string and do not JSON-parse it.
  • Every failure from this call except the 401 uses the same HTTP status (405 unless you set errorReplyStatusCode), so always branch on the code field of the JSON body. Do not set errorReplyStatusCode to a 2xx value, or failures will look like successes.
  • All parameters go in the query string, so URL-encode the password and server name. Characters such as +, &, # and % otherwise change or cut off the value. Keep these URLs out of your own logs.
  • Before you can connect with a new password, the live session has to stop. GET /v1/Disconnect does not stop anything. Use POST /orchestrator/disconnect, which needs the Bearer token described under Authentication, or let the session reach its idleHours limit. The next ConnectEx then signs in fresh with the new password.
  • The password check on a live session only works once this service has verified a password for that account. If no password has been verified yet, a repeat ConnectEx reports the account as connected without testing the password you sent.
  • Once a password has been verified for an account, any other password is refused with CREDENTIALS_MISMATCH while the session is live. That includes switching between the master and the investor password.
  • A repeat ConnectEx for a live session is only checked when you send both password and server, and even then only the password is compared, not the server name. With either one left out, a live session simply returns its id.
  • A successful ConnectEx that includes password and server replaces the account's idle setting. If you leave out idleHours on such a call, the account goes back to the service default. An empty idleHours= counts as 0, which means never stop. Calls without password and server, and failed calls, leave the idle setting unchanged.
  • Only calls that read from or act on the account count as activity for idleHours, for example AccountSummary, OpenedOrders, GetQuote, history and order calls. CheckConnect, ConnectByToken, Reconnect and a repeat ConnectEx on a live session do not count. Once an idle session has stopped, call ConnectEx again with password and server to bring it back.
  • If several ConnectEx calls for the same login arrive while a start is in progress, they all get the result of that one start, whatever password each of them sent.
  • When many accounts start at once, the starts are queued, so set your HTTP client timeout well above two minutes for this call.
  • If the account's session is already restarting, for example during an automatic recovery, ConnectEx waits for it to come up instead of restarting it again.
  • After LOGIN_REJECTED, NOT_AUTHORIZED or CONNECT_TIMEOUT the failed attempt is cleaned up, so a retry with corrected details starts fresh.
  • Call ConnectEx once and then reuse the id. Call it again when ConnectByToken returns INVALID_TOKEN or a data call returns 502 saying the account is not connected.
curl -G https://api.traderail.cloud/v1/ConnectEx \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "user=700100200" \
--data-urlencode "password=YOUR_PASSWORD" \
--data-urlencode "server=PUPrime-Demo"
Response 200text/plain; charset=utf-8
acct-700100200
Try itAdd your API key above to send a live request
Fill in user, password
GET/v1/ConnectByToken

Check a session id

Report whether the account behind an id has a live session, as a JSON code.

Returns { "code": "OK" } when the account behind ?id= has a live session, and { "code": "INVALID_TOKEN", "message": "token not found or expired" } when it does not. Both answers come back as HTTP 200, so read the code field. Use it to check a stored id before you reuse it. On INVALID_TOKEN, call ConnectEx again with the password and server. Despite its name, this call does not connect anything.

ApiKey header

Parameters

idstringrequired
The account id returned by ConnectEx (acct-<login>), or the bare login.

Returns

{ "code": "OK" } when the session is live. Otherwise { "code": "INVALID_TOKEN", "message": "token not found or expired" }, also with HTTP 200.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key.
Good to know
  • Both outcomes are HTTP 200, so check the code field rather than the status.
  • A missing or malformed id returns INVALID_TOKEN, not an error status.
  • This call does not count as account activity, so polling it will not stop a session with an idleHours limit from being stopped.
curl -G https://api.traderail.cloud/v1/ConnectByToken \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json; charset=utf-8
{
"code": "OK"
}
Try itAdd your API key above to send a live request
GET/v1/Reconnect

Confirm a session

Confirm that an account's session is live and get its id back. Nothing is restarted or reconnected.

Returns { "code": "OK", "terminalInstanceGuid": "acct-<login>" } when the account behind ?id= has a live session. When it has none, it returns HTTP 405 with { "code": "NOT_CONNECTED", "message" }. Despite its name, this call never starts, restarts or reconnects a session. Use ConnectEx for that. terminalInstanceGuid is the same acct-<login> account id that ConnectEx returns.

ApiKey header

Parameters

idstringrequired
The account id returned by ConnectEx (acct-<login>), or the bare login.

Returns

The session is live. terminalInstanceGuid is the account id.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key.
405NOT_CONNECTEDThe account has no live session, or ?id= is missing or malformed. Body: {"code":"NOT_CONNECTED","message":"account not connected to bridge"}.
Good to know
  • The failure status is always 405 here. errorReplyStatusCode is not supported on this endpoint.
  • Reconnect does not recover a dropped session. After a NOT_CONNECTED answer, call ConnectEx with the password and server.
  • This call does not count as account activity for idleHours.
curl -G https://api.traderail.cloud/v1/Reconnect \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json; charset=utf-8
{
"code": "OK",
"terminalInstanceGuid": "acct-700100200"
}
Try itAdd your API key above to send a live request
GET/v1/CheckConnect

Check connection

The lightest session check: plain-text ok when the session is up, otherwise HTTP 403.

Returns HTTP 200 with the plain-text body ok when the account behind ?id= has an active session, and HTTP 403 with an empty body when it does not. It reads no account data and makes no round trip to the broker, so it answers immediately and is safe to call often. It tells you whether the session exists, not whether the account can trade right now, so use a data call such as AccountSummary when you need to confirm that. When you get 403, start a session with ConnectEx.

ApiKey header

Parameters

idstringrequired
The account id returned by ConnectEx (acct-<login>), or the bare login. Anything that does not resolve to a positive whole-number login is treated as having no session.

Returns

The account has an active session.

Errors

401UnauthorizedThe ApiKey header is missing or wrong, or the key belongs to an account that is not active. The body is JSON: {"message": "ApiKey required", "error": "Unauthorized", "statusCode": 401}.
403(empty body)The account has no active session, or ?id= is missing or malformed. The response body is empty.
Good to know
  • A 403 here means there is no active session, not an authentication problem. A bad API key gets 401.
  • The success body is plain text, not JSON.
  • An ok answer does not confirm that the account's connection to the broker's trading server is up, so a session can report ok here while trading calls still fail.
  • This call does not count as account activity, so polling it will not keep a session with an idleHours limit from being stopped for inactivity.
curl -G https://api.traderail.cloud/v1/CheckConnect \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200text/plain; charset=utf-8
ok
Try itAdd your API key above to send a live request
GET/v1/Disconnect

Disconnect (no-op)

Kept for client compatibility. It always answers OK and does not stop or change any session.

GET /v1/Disconnect always returns { "code": "OK" } and has no effect. The session keeps running, its positions stay open, and the same id keeps working. It exists so that clients which call Disconnect at the end of a flow keep working. To actually stop a session, call POST /orchestrator/disconnect, which needs the Bearer token described under Authentication, or have it stop automatically with ConnectEx's idleHours.

ApiKey header

Parameters

No parameters.

Returns

Always returned. Nothing was changed.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key.
Good to know
  • All query parameters, including id, are ignored.
  • Calling it does not clear the password the session was started with. A later ConnectEx with a different password is still refused with CREDENTIALS_MISMATCH.
  • Do not use it to free up an account or close positions. Nothing is stopped and nothing is closed.
curl -G https://api.traderail.cloud/v1/Disconnect \
-H "ApiKey: YOUR_API_KEY"
Response 200application/json; charset=utf-8
{
"code": "OK"
}
Try itAdd your API key above to send a live request
GET/v1/Ping

Ping

Check that the API is reachable and that your API key is accepted.

Returns { "code": "OK" } for any request that carries a valid ApiKey header. It uses no account, so it is the simplest way to confirm connectivity and your key before you start a session. A missing or invalid key returns 401.

ApiKey header

Parameters

No parameters.

Returns

The API is reachable and the key is valid.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
Good to know
  • Ping tells you nothing about any account's session. Use CheckConnect or ConnectByToken for that.
  • Ping needs an API key. For a probe without credentials, use GET /healthz.
curl -G https://api.traderail.cloud/v1/Ping \
-H "ApiKey: YOUR_API_KEY"
Response 200application/json; charset=utf-8
{
"code": "OK"
}
Try itAdd your API key above to send a live request
GET/v1/AccountSummary

Get account summary

Live balance, equity, margin and floating profit/loss for a connected account.

Reads the account's current figures from its live session. All money values are in the account currency, given in currency. Some fields appear under two names for client compatibility: company and accountCompanyName both hold the broker's company name, and server and accountServer both hold the trade-server name. The account needs a live session; without one the call returns 502.

ApiKey header

Parameters

idstringrequired
The account id returned by ConnectEx (acct-<login>), or the bare login.

Returns

Account figures. balance, equity, margin, freeMargin, marginLevel and profit are numbers. leverage and login are integers. currency, company, accountCompanyName, server and accountServer are strings.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key.
400ERROR?id= is missing or is not acct-<login> or a positive login. Body: {"code":"ERROR","message":"account id (?id=) required"}.
502ERRORThe account has no live session (message "account 700100200 is not connected"), did not answer within 5 seconds, or the read failed. Body: {"code":"ERROR","message":"..."}.
Good to know
  • A 502 saying the account is not connected means the session has ended, for example after an idle stop. Call ConnectEx again with the password and server.
  • company and accountCompanyName are the same value, and so are server and accountServer. Read whichever name your client expects.
  • leverage is the right-hand side of the ratio as an integer, so 500 means 1:500.
  • marginLevel is equity divided by margin, as a percentage. It is 0 when no margin is in use.
  • profit is the floating profit or loss of open positions, and equity already includes it.
  • Numbers carry up to 8 decimal places, so round them for display.
  • Each call counts as account activity, which resets the idleHours clock.
curl -G https://api.traderail.cloud/v1/AccountSummary \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json; charset=utf-8
{
"balance": 10000,
"equity": 10152.4,
"margin": 216,
"freeMargin": 9936.4,
"marginLevel": 4700.18518519,
"leverage": 500,
"profit": 152.4,
"currency": "USD",
"accountCompanyName": "Example Markets Ltd",
"company": "Example Markets Ltd",
"accountServer": "ExampleMarkets-Demo",
"server": "ExampleMarkets-Demo",
"login": 700100200
}
Try itAdd your API key above to send a live request
GET/v1/AccountDetails

Get server time-zone details

Returns the server time-zone fields some clients read at start-up. They are always zero.

Returns { "serverTimeZone": 0, "serverTimeZoneInMinutes": 0, "utcOffsetMinutes": 0 } on every request. The values are fixed and are not read from the broker or the account. No id is needed, and one is ignored if sent. The endpoint exists so that clients which look up a server time-zone offset during start-up keep working.

ApiKey header

Parameters

No parameters.

Returns

Fixed time-zone fields. All three are always 0.

Errors

401UnauthorizedThe ApiKey header is missing, wrong or belongs to a deactivated key.
Good to know
  • The values are the same for every broker, so do not use this endpoint to find out a broker's server time zone.
  • It does not need a live session and succeeds even when the account is not connected.
curl -G https://api.traderail.cloud/v1/AccountDetails \
-H "ApiKey: YOUR_API_KEY"
Response 200application/json; charset=utf-8
{
"serverTimeZone": 0,
"serverTimeZoneInMinutes": 0,
"utcOffsetMinutes": 0
}
Try itAdd your API key above to send a live request

Market data

Live quotes, the symbols an account can trade, and historical bars and ticks.

GET/v1/GetQuote

Get a quote

Returns the current bid, ask and last price for one symbol on a connected account.

Returns the latest bid, ask and last price the broker has published for one symbol on a connected account. You can send a base name such as EURUSD or XAUUSD, and it is mapped to this broker's exact symbol name for you (see Symbol names). If no price has arrived yet for the symbol, which is typical the first time a symbol is requested in a session, the API waits briefly and retries once before it answers. The response holds prices only. It has no timestamp, spread or symbol name, so work out the spread as ask - bid and use TickHistory when you need timestamped prices. Quoting a symbol also adds it to the account's active (Market Watch) set, which matters for Symbols on very large brokers.

ApiKey header

Parameters

idstringrequired
The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.
symbolstringrequired
Symbol to quote. A base name like EURUSD is resolved to the broker's exact name (for example EURUSD.s). An exact broker name is used as is. Matching is case-sensitive.

Returns

Latest prices for the symbol: bid, ask and last, all numbers. No other fields are returned.

Errors

401UnauthorizedThe ApiKey header is missing, the key is unknown, or the key is no longer active.
400ERROR?id= is missing or is not a valid account id (message "account id (?id=) required").
502ERROROne of these: the symbol is missing or empty (message "symbol is required"), the broker has no such symbol ("unknown symbol: ..."), the account is not connected, or no price could be read within the time limit.
Good to know
  • A missing or empty symbol is not rejected up front. Instead of a 400, the call returns 502 with the message "symbol is required".
  • Treat a bid or ask of 0 as "no price available" and never as a real price. You can still get 0 after the automatic retry if the broker has not published a price for that symbol.
  • When the market is closed, the quote is the last price the broker published. The response has no timestamp, so this endpoint alone cannot tell a stale quote from a live one.
  • last is the price of the most recent trade. It can be 0 for symbols where the broker publishes only bid and ask, which is typical of forex and CFDs.
  • The resolved broker symbol name is not returned. Use SymbolList, or TickHistory (which returns it), to see the exact name that was used.
  • By default each price read gets about 5 seconds. The first symbol-taking call on an account can take longer, because the account's symbol list is loaded once for name resolution and then cached for up to 10 minutes.
curl -G https://api.traderail.cloud/v1/GetQuote \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "symbol=EURUSD"
Response 200application/json
{
"bid": 1.08412,
"ask": 1.08425,
"last": 0
}
Try itAdd your API key above to send a live request
GET/v1/SymbolList

List symbol names

Returns the exact name of every symbol the broker offers on the account.

Returns a flat JSON array with the name of every symbol the broker makes available to this account. Each name is spelled exactly as the broker spells it, including any suffix such as ".s". Use it to build a symbol picker, or to find a broker's exact spelling before calling GetQuote, PriceHistoryEx or OrderSendTask. The list covers every symbol the broker offers, not only the account's active ones. On some brokers it runs to many thousands of entries. Fetch it once per account and cache it.

ApiKey header

Parameters

idstringrequired
The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.

Returns

Array of exact broker symbol names (strings).

Errors

401UnauthorizedThe ApiKey header is missing, the key is unknown, or the key is no longer active. The body is { "statusCode": 401, "message": "ApiKey required", "error": "Unauthorized" }, so "Unauthorized" is in the error field and there is no code field.
400ERROR?id= is missing or is not a valid account id. The body is { "code": "ERROR", "message": "account id (?id=) required" }.
502ERRORThe account is not connected (message "account <login> is not connected"), the account rejected the request, or the list could not be read within 60 seconds. The body is { "code": "ERROR", "message": "..." }.
Good to know
  • The endpoint has no filter, search or paging parameters. Every call returns the whole list.
  • Names come back in the broker's own order and are not guaranteed to be sorted. Sort them client-side if you display them.
  • The list can include symbols the account can see but is not allowed to trade. Use tradeMode from Symbols to check a symbol before you offer it for trading.
  • On brokers with a very large catalogue, Symbols returns details only for the account's active symbols. On those brokers, not every name from SymbolList has an entry in Symbols.
  • A large list can take a while. The read has up to 60 seconds once it starts before it fails with 502. Requests for the same account run one at a time, so the read may first wait behind earlier calls, and later calls wait while it runs.
  • SymbolList does no name resolution. The names it returns are exactly the ones the broker uses.
curl -G https://api.traderail.cloud/v1/SymbolList \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json
[
"EURUSD.s",
"GBPUSD.s",
"USDJPY.s",
"XAUUSD.s",
"BTCUSD"
]
Try itAdd your API key above to send a live request
GET/v1/Symbols

Get symbol specifications

Returns trading specifications for the account's symbols as an object keyed by exact broker symbol name.

Returns an object whose keys are exact broker symbol names and whose values are each symbol's trading specification. That covers price precision (digits, point, tick size), the base, profit and margin currencies, whether and how the symbol can be traded (tradeMode), and the minimum, maximum and step for order volume. Use it to round prices to the symbol's tick size, and lot sizes to volumeStep, before calling OrderSendTask. On brokers that list more than 4,000 symbols, only the account's active (Market Watch) symbols are included so the call stays responsive. Any symbol you have quoted, charted or traded through the API becomes active, so if one is missing, call GetQuote for it and then fetch Symbols again. The response can be large, so cache it per account.

ApiKey header

Parameters

idstringrequired
The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.

Returns

Object keyed by exact broker symbol name. Each value has these fields: symbol, description, path, digits, point, tickSize, trade_tick_size, currencyBase, currencyProfit, currencyMargin, tradeMode, volumeMin, volumeMax, volumeStep.

Errors

401UnauthorizedThe ApiKey header is missing, the key is unknown, or the key is no longer active.
400ERROR?id= is missing or is not a valid account id (message "account id (?id=) required").
502ERRORThe account is not connected, or the specifications could not be read within 60 seconds.
Good to know
  • The description and path fields are present for compatibility, but they are currently always empty strings.
  • tickSize and trade_tick_size always hold the same value. Note the mixed naming: trade_tick_size is the only field that is not camelCase.
  • tradeMode is the MT5 symbol trade mode: 0 = trading disabled, 1 = long only, 2 = short only, 3 = close only, 4 = full trading.
  • Contract size and tick value are not part of this response.
  • There is no parameter for requesting a single symbol. Every call returns the full map.
  • On brokers that list more than 4,000 symbols, a symbol you have never quoted, charted or traded will be missing from the map even though SymbolList shows it.
  • The call has up to 60 seconds before it fails with 502, and other requests for the same account wait while it runs.
curl -G https://api.traderail.cloud/v1/Symbols \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json
{
"EURUSD.s": {
"symbol": "EURUSD.s",
"description": "",
"path": "",
"digits": 5,
"point": 0.00001,
"tickSize": 0.00001,
"trade_tick_size": 0.00001,
"currencyBase": "EUR",
"currencyProfit": "USD",
"currencyMargin": "EUR",
"tradeMode": 4,
"volumeMin": 0.01,
"volumeMax": 100,
"volumeStep": 0.01
},
"XAUUSD.s": {
"symbol": "XAUUSD.s",
"description": "",
"path": "",
"digits": 2,
"point": 0.01,
"tickSize": 0.01,
"trade_tick_size": 0.01,
"currencyBase": "XAU",
"currencyProfit": "USD",
"currencyMargin": "XAU",
"tradeMode": 4,
"volumeMin": 0.01,
"volumeMax": 50,
"volumeStep": 0.01
}
}
Try itAdd your API key above to send a live request
GET/v1/PriceHistoryEx

Get price history (OHLC bars)

Returns the most recent OHLC bars for a symbol at a chosen timeframe, oldest first.

Returns up to 5,000 of the most recent OHLC bars for one symbol, which is the data behind a price chart. You give the timeframe in minutes, and it is snapped to the nearest standard MT5 timeframe: 1, 5, 15, 30, 60, 240, 1440, 10080 or 43200 minutes, that is M1 to MN1. Bars are ordered oldest to newest, and the window always ends at the current bar. While the market is open, that last bar is still forming, so its high, low and close keep moving until it closes. You cannot ask for a specific date range; to see further back, request more bars or a longer timeframe. Bar times are in the broker's server time and have no time-zone suffix.

ApiKey header

Parameters

idstringrequired
The account to read. Either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.
symbolstringrequired
Symbol to chart. Surrounding whitespace is trimmed. A base name like EURUSD is resolved to the broker's exact name (for example a suffixed variant). An exact broker name is used as is.
timeFrameinteger (minutes)optional
Bar size in minutes. Supported values are 1 (M1), 5 (M5), 15 (M15), 30 (M30), 60 (H1), 240 (H4), 1440 (D1), 10080 (W1) and 43200 (MN1). Decimals are rounded down first. Any other positive number then snaps to the nearest supported value, and an exact tie goes to the shorter timeframe (45 gives M30, 10 gives M5). Zero, negative, empty or non-numeric values fall back to 30, and that includes names such as "H1".
Default 30
timeFrameMinutesinteger (minutes)optional
Alias for timeFrame, with the same meaning and snapping. It is used only when timeFrame is not sent at all. An empty timeFrame= still counts as sent and gives 30-minute bars.
numBarsintegeroptional
How many of the most recent bars to return. Decimals are rounded down. Values below 1 become 1, values above 5000 become 5000, and a non-numeric value falls back to 100.
Default 100
fromstringoptional
Accepted for compatibility but ignored. The newest numBars bars are always returned.

Returns

Array of bars, oldest first. Each bar has time (broker server time, ISO 8601 with no zone), open, high, low, close and volume (tick volume). While the market is open, the last bar is the one still forming. The array can be empty if no bars are available.

Errors

401UnauthorizedThe ApiKey header is missing, the key is unknown, or the key is no longer active (message "ApiKey required").
400ERROR?id= is missing or is not a valid account id (message "account id (?id=) required").
400ERRORsymbol is missing, empty or only whitespace (message "symbol required").
502ERRORThe price history could not be read. Typical causes: the broker has no such symbol ("unknown symbol: ..."), the account is not connected ("account ... is not connected"), the history read failed ("CopyRates failed for ..."), or the read did not finish within 30 seconds.
Good to know
  • timeFrame must be a number of minutes. A name such as H1 or D1 is not understood, and you silently get 30-minute bars.
  • An empty numBars= parameter is read as 0 and returns a single bar. Leave the parameter out entirely to get the default of 100.
  • volume is the bar's tick volume, meaning the number of price updates in the bar. It is not traded lots or notional.
  • Bar times look like ISO 8601 but carry no zone. They use the broker's server clock, which on many brokers runs ahead of UTC, so do not append Z or treat them as UTC.
  • You may get fewer bars than numBars, or an empty array, if the broker holds less history for that symbol and timeframe.
  • The first request for a symbol and timeframe that has not been used recently may be slow, return fewer bars than asked, or fail with 502 while history is still loading. If that happens, retry after a short wait.
  • The history read itself is allowed 30 seconds. The first call for an account can take longer in total, because the symbol name is resolved first.
  • The response does not repeat the symbol or timeframe that was used, so keep track of what you asked for.
curl -G https://api.traderail.cloud/v1/PriceHistoryEx \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "symbol=EURUSD" \
--data-urlencode "timeFrame=30" \
--data-urlencode "numBars=200"
Response 200application/json
[
{
"time": "2026-10-07T14:00:00",
"open": 1.08391,
"high": 1.08447,
"low": 1.08372,
"close": 1.08436,
"volume": 1843
},
{
"time": "2026-10-07T14:30:00",
"open": 1.08436,
"high": 1.08458,
"low": 1.08401,
"close": 1.08415,
"volume": 1527
},
{
"time": "2026-10-07T15:00:00",
"open": 1.08415,
"high": 1.08429,
"low": 1.08398,
"close": 1.08412,
"volume": 412
}
]
Try itAdd your API key above to send a live request
GET/v1/TickHistory

Get tick history

Returns every raw tick for a symbol within a time window of up to 6 hours.

Returns the individual price updates (ticks) for one symbol between from and to. Each tick has bid, ask, last, volume and a time to the millisecond. That is finer than any bar timeframe, which makes ticks useful for spread analysis, back-testing or replaying price action. By default the window is the 15 minutes ending at to, and to defaults to now. A window can be at most 6 hours long: a longer request is cut to the 6 hours ending at to, and the response sets truncated: true. To cover a longer period, page backwards. The response also returns the window actually used (in epoch seconds) and the broker's exact symbol name.

ApiKey header

Parameters

idstringrequired
The account to read. Pass either the session token returned by ConnectEx (acct-<login>) or the bare MT5 login number.
symbolstringrequired
Symbol to read. A base name like EURUSD is resolved to the broker's exact name (for example EURUSD.s). An exact broker name is used as is. A name the broker does not recognise is passed through unchanged and the call fails with 502.
fromstring (epoch seconds, epoch milliseconds, or ISO 8601)optional
Start of the window. Accepts epoch seconds, epoch milliseconds or an ISO 8601 date/time. A date/time without a zone is read as UTC. If from is more than 6 hours before to, it is moved forward to 6 hours before to.
Default 15 minutes before to
tostring (epoch seconds, epoch milliseconds, or ISO 8601)optional
End of the window. Accepts the same formats as from.
Default now

Returns

An object with these fields: symbol, the broker's exact symbol name; ticks, an array ordered oldest first, where each tick has time (epoch seconds), time_msc (epoch milliseconds), bid, ask, last, volume and flags; from and to, the window actually used, in epoch seconds; and truncated, which is true when the requested window was cut to 6 hours.

Errors

401UnauthorizedThe ApiKey header is missing, the key is unknown, or the key is no longer active. The body is { statusCode: 401, message: "ApiKey required", error: "Unauthorized" }.
400ERROR?id= is missing or is not a valid account id (message "account id (?id=) required").
400ERRORsymbol is missing or empty (message "symbol is required").
502ERRORThe ticks could not be read. The message gives the reason: the broker has no such symbol ("unknown symbol: ..."), the account is not connected or its connection dropped during the call, the broker could not supply ticks for the symbol, to fell before 1970, or the read did not finish within 30 seconds.
Good to know
  • Tick times use the broker's server clock, which on many brokers runs ahead of UTC. PriceHistoryEx bar times use the same clock, and the from/to window is matched against it too. The default to is the current UTC time. On a broker N hours ahead of UTC, the default window therefore returns ticks from about N hours ago and none of the latest ones. To target a period precisely, pass from and to as the broker's server wall-clock time with no zone suffix, in the same form PriceHistoryEx returns.
  • When truncated is true, you received only the last 6 hours of the window you asked for. To fetch the rest, repeat the call with the same from and with to set to the from value in the response. If you leave from out, it falls back to 15 minutes before the new to. Use time_msc to drop any tick that appears in both pages at their shared boundary.
  • A from or to value that cannot be parsed is not rejected. It is silently replaced by its default, so check the from and to in the response. Epoch values must be whole numbers: a decimal such as 1791373500.5 counts as unparseable.
  • Ticks are not aggregated, so a busy symbol returns a very large response even inside the 6-hour limit. Keep windows as short as you can. Reading the ticks has a 30-second limit, and past it the call fails with 502. The first call for an account can take longer overall, because the symbol name is resolved first.
  • flags is a bitmask of what changed in the tick: 2 = bid, 4 = ask, 8 = last, 16 = volume, 32 = buy trade, 64 = sell trade.
  • time is in epoch seconds. time_msc is the same instant in epoch milliseconds; use it to order or de-duplicate ticks.
  • last and volume relate to trades and can be 0 on symbols where the broker publishes only bid and ask.
curl -G https://api.traderail.cloud/v1/TickHistory \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "symbol=EURUSD" \
--data-urlencode "from=2026-10-07T11:45:00" \
--data-urlencode "to=2026-10-07T12:00:00"
Response 200application/json
{
"symbol": "EURUSD.s",
"ticks": [
{
"time": 1791373512,
"time_msc": 1791373512184,
"bid": 1.08412,
"ask": 1.08425,
"last": 0,
"volume": 0,
"flags": 6
},
{
"time": 1791373512,
"time_msc": 1791373512731,
"bid": 1.08413,
"ask": 1.08425,
"last": 0,
"volume": 0,
"flags": 2
},
{
"time": 1791373514,
"time_msc": 1791373514055,
"bid": 1.08413,
"ask": 1.08426,
"last": 0,
"volume": 0,
"flags": 4
}
],
"from": 1791373500,
"to": 1791374400,
"truncated": false
}
Try itAdd your API key above to send a live request

Trading

Place, modify, close and cancel orders, and list what is open right now.

GET/v1/OpenedOrders

List open positions and pending orders

Returns every open position and every working pending order on the account in one call.

Use this to see everything currently live on the account: filled positions with their floating profit, and pending (limit/stop) orders that have not triggered yet. Positions come back in positionInfos and pending orders in openedOrders. Both arrays are empty, not missing, when there is nothing open. Several fields are deliberately duplicated under two names (for example lots/volume, openPrice/priceOpen, stopLoss/sl, expertId/magic) so you can read whichever naming your client prefers; the values are identical. Call this to reconcile state after a trade call came back with error.errorCode TIMEOUT: the outcome of that request is unknown, so check here whether the order or position exists before you retry. NOT_CONNECTED usually means nothing was sent, but if the connection dropped while the request was in flight the outcome is unknown as well, so checking here is the safe move.

ApiKey header

Parameters

idstringrequired
The account to read. Either the session token returned by ConnectEx ("acct-<login>") or the bare MT5 login number.

Returns

An object with two arrays. positionInfos[]: ticket, positionTicket and identifier (all the position ticket); symbol; orderType and type ("Buy" or "Sell"); lots and volume (open volume in lots); openPrice and priceOpen; priceCurrent and closePrice (the current price the position would close at); openTime and timeSetup (Unix seconds); stopLoss and sl, takeProfit and tp (0 when not set); swap; profit (floating P/L in account currency, swap reported separately); expertId and magic; comment. openedOrders[]: ticket; symbol; orderType and type ("BuyLimit", "SellLimit", "BuyStop", "SellStop", "BuyStopLimit" or "SellStopLimit"); lots, volume and volumeCurrent (all the remaining volume); openPrice and priceOpen (the order price); stopLoss and sl, takeProfit and tp; stopLimitPrice (0 unless a stop-limit order); timeSetup (Unix seconds); expertId and magic; comment.

Errors

400ERRORThe id query parameter is missing or is neither "acct-<login>" nor a positive integer login. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing or is not a valid key. This body has no code field; "Unauthorized" is its error field. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe positions or orders could not be read: the account is not connected (message "account <login> is not connected"), it did not answer in time, or it reported a failure. Body: {"code":"ERROR","message":"..."}.
Good to know
  • openTime and timeSetup are plain Unix timestamps in seconds (numbers, not ISO strings), stamped by the broker's trade server. Many brokers run that clock ahead of UTC, so do not treat them as exact UTC.
  • symbol is the broker's own symbol name, which may carry a suffix (for example XAUUSD.s) even if you placed the order using the base name XAUUSD.
  • Pending orders do not include their expiration time in this response.
  • Positions carry no commission field; profit is the floating P/L and swap is reported separately.
  • A position's ticket is the value to pass as ticket to OrderModifyTask, OrderCloseTask and OrderCloseByTask. A pending order's ticket is the value to pass to OrderModifyTask, OrderCancelTask or OrderCloseTask.
  • If an entry in openedOrders is not one of the six pending types, orderType and type fall back to an upper-case raw value such as BUY, SELL or OTHER, so do not hard-fail on an unexpected value.
  • Unlike the trade endpoints, this read endpoint reports an unreachable account with HTTP 502, not with an HTTP 200 error body.
curl -G https://api.traderail.cloud/v1/OpenedOrders \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200"
Response 200application/json
{
"positionInfos": [
{
"ticket": 812345678,
"positionTicket": 812345678,
"identifier": 812345678,
"symbol": "EURUSD",
"orderType": "Buy",
"type": "Buy",
"lots": 0.1,
"volume": 0.1,
"openPrice": 1.0825,
"priceOpen": 1.0825,
"priceCurrent": 1.08312,
"closePrice": 1.08312,
"openTime": 1791363600,
"timeSetup": 1791363600,
"stopLoss": 1.0795,
"sl": 1.0795,
"takeProfit": 1.0875,
"tp": 1.0875,
"swap": -0.12,
"profit": 6.2,
"expertId": 8100003,
"magic": 8100003,
"comment": "breakout-1"
},
{
"ticket": 812345702,
"positionTicket": 812345702,
"identifier": 812345702,
"symbol": "XAUUSD",
"orderType": "Sell",
"type": "Sell",
"lots": 0.05,
"volume": 0.05,
"openPrice": 2401.35,
"priceOpen": 2401.35,
"priceCurrent": 2398.9,
"closePrice": 2398.9,
"openTime": 1791365400,
"timeSetup": 1791365400,
"stopLoss": 0,
"sl": 0,
"takeProfit": 2385,
"tp": 2385,
"swap": 0,
"profit": 12.25,
"expertId": 0,
"magic": 0,
"comment": ""
}
],
"openedOrders": [
{
"ticket": 812345730,
"symbol": "EURUSD",
"orderType": "BuyLimit",
"type": "BuyLimit",
"lots": 0.2,
"volume": 0.2,
"volumeCurrent": 0.2,
"openPrice": 1.079,
"priceOpen": 1.079,
"stopLoss": 1.075,
"sl": 1.075,
"takeProfit": 1.088,
"tp": 1.088,
"stopLimitPrice": 0,
"timeSetup": 1791367200,
"expertId": 0,
"magic": 0,
"comment": "dip-buy"
}
]
}
Try itAdd your API key above to send a live request
GET/v1/OrderSendTask

Place an order

Places a market order or a pending limit, stop or stop-limit order on the account.

Set operation to Buy or Sell for a market order, which is filled at the current ask (Buy) or bid (Sell); any price you pass is ignored for market orders. Set it to a pending type (BuyLimit, SellLimit, BuyStop, SellStop, BuyStopLimit, SellStopLimit) to place a working order, which then requires a positive price. Every trade outcome, including a broker rejection, comes back as HTTP 200, so branch on the body's code field ("OK" or "ERROR"); only a missing or invalid id (400) or a bad API key (401) changes the HTTP status. Pass a clientOrderId to make retries safe: a repeat of the same id within 5 minutes returns the first result instead of opening a second position. If you get errorCode TIMEOUT or NOT_CONNECTED, the order may or may not be live; check OpenedOrders before sending again.

ApiKey headerchanges state

Parameters

idstringrequired
The account to trade on: "acct-<login>" or the bare MT5 login (a positive integer).
operationstringrequired
Order type. Matching is case-insensitive, leading/trailing spaces and underscores are ignored, and a legacy "Tmt5OrderType" prefix is stripped, so Buy, BUY, buy_limit, BUY_STOP_LIMIT and Tmt5OrderTypeSellStop are all accepted. Market: Buy, Sell. Pending: BuyLimit, SellLimit, BuyStop, SellStop, BuyStopLimit, SellStopLimit. Anything else (including spaces or hyphens inside the name, such as "Buy Limit" or "buy-limit") returns errorCode UNSUPPORTED.
One of Buy Sell BuyLimit SellLimit BuyStop SellStop BuyStopLimit SellStopLimit
symbolstringrequired
Symbol to trade. You can send the base name (EURUSD, XAUUSD); it is matched to the broker's own name, including any suffix such as XAUUSD.s. An exact broker name is used as-is.
volumenumberrequired
Order volume in lots. Must be greater than 0; the broker enforces the symbol's minimum, maximum and step.
pricenumberoptional
Pending orders only, and required for them (must be > 0). For limit and stop orders it is the order price. For stop-limit orders it is the stop (activation) price. Ignored for market orders.
stopLimitPricenumberoptional
Stop-limit orders only: the price of the limit order that is placed once price reaches the stop price given in price. Ignored for market orders.
stoplimitnumberoptional
Alias of stopLimitPrice. If both are sent, stopLimitPrice wins.
stoplossnumberoptional
Stop-loss as an absolute price (not points). Omit or send 0 for no stop-loss. Applies to market and pending orders.
takeprofitnumberoptional
Take-profit as an absolute price (not points). Omit or send 0 for no take-profit. Applies to market and pending orders.
slippageintegeroptional
Maximum price deviation for a market order, in points (the symbol's smallest price increment). Not used for pending orders.
Default 10
expertIDintegeroptional
Magic number stamped on the order and on the resulting position. Shown as expertId/magic in OpenedOrders, so you can tag and later find your strategy's trades.
Default 0
expertIdintegeroptional
Alias of expertID. Used only when expertID is absent.
Default 0
magicintegeroptional
Alias of expertID. Used only when expertID and expertId are absent.
Default 0
commentstringoptional
Free-text comment stored on the order and shown in OpenedOrders. Empty values are not sent.
expirationTypestringoptional
Pending orders only. Case-sensitive. "Today" or "Day": the order expires at the end of the trading day (any expiration you also send is ignored). "Specified" or "SpecifiedDay" together with a valid expiration: the order expires at that time; without one it is good-till-cancelled. Any other value, including "GTC" or no value, means good-till-cancelled, unless a valid expiration is also given, in which case the order expires at that time.
One of GTC Today Day Specified SpecifiedDay
Default GTC
expirationstringoptional
Pending orders only. When the order should expire: Unix seconds, Unix milliseconds (any all-digit value above 100000000000 is treated as milliseconds), or an ISO-8601 datetime. A datetime with no zone, such as 2026-10-08T12:00:00, is read as UTC. An unparseable value is ignored.
clientOrderIdstringoptional
Your idempotency key, scoped to this account. While the first request with this id is running, and for 5 minutes after it started, any repeat returns that first result without sending another order. Leading and trailing spaces are trimmed; an empty value disables de-duplication.
clientOrderIDstringoptional
Alias of clientOrderId. Used only when clientOrderId is absent.
clientIdstringoptional
Alias of clientOrderId. Used only when clientOrderId and clientOrderID are absent.

Returns

Success: { ticket, order, deal, volume, price, retcode, code: "OK", comment }. ticket is the order ticket (or the deal ticket if no order ticket was returned). order and deal are the order and deal tickets the broker reported; a filled market order carries a deal ticket, a newly placed pending order has deal 0. For a filled market order, volume and price are the executed volume and price. retcode is the broker's trade return code (10008 placed, 10009 done, 10010 partially done). comment is the broker's comment on the operation, not your order comment. Failure, still HTTP 200: { code: "ERROR", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }, where mqlErrorTradeIntCode is present only for numeric codes. Example broker rejection: {"code":"ERROR","message":"not enough money: No money","error":{"errorCode":"10019","errorMessage":"not enough money: No money","mqlErrorTradeIntCode":10019,"mqlErrorTradeCode":"10019"}}. Example bad operation: {"code":"ERROR","message":"unsupported operation: Teleport","error":{"errorCode":"UNSUPPORTED","errorMessage":"unsupported operation: Teleport","mqlErrorTradeCode":"UNSUPPORTED"}}.

Errors

20010004-10046The broker refused the trade. body.code is "ERROR"; error.errorCode is the trade return code as a string (e.g. "10019" not enough money, "10014" invalid volume, "10016" invalid stops, "10018" market closed) and error.mqlErrorTradeIntCode is the same number. If the broker returned no trade return code, errorCode is instead the trading platform's numeric error code. Nothing was placed.
200UNSUPPORTEDoperation is missing or not one of the accepted values. Nothing was sent.
200-2Invalid parameters, for example volume missing or not greater than 0, symbol missing, or a pending order without a positive price (message "pending order requires a positive price").
200-3The symbol does not exist on this account's broker (message "unknown symbol: <symbol>").
200-100The account is a real-money (live) account and trading on live accounts is not enabled for the service. Nothing was sent.
200TIMEOUTThe service did not learn the outcome in time. The order may be live at the broker; check OpenedOrders before retrying.
200NOT_CONNECTEDThe account is not connected, or its connection dropped while the order was in flight. In the second case the order may have reached the broker.
200ERRAn unclassified failure with no specific code.
400ERRORThe id query parameter is missing or invalid. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing or not a valid key. Body: {"statusCode":401,"message":"ApiKey required","error":"Unauthorized"}.
Good to know
  • A broker rejection is reported as HTTP 200 with body.code "ERROR"; only a bad id (400) or bad API key (401) changes the HTTP status.
  • error.errorCode is always a string ("10019", "-2", "TIMEOUT"); the numeric form is in error.mqlErrorTradeIntCode, which is omitted entirely for non-numeric codes such as TIMEOUT, NOT_CONNECTED and UNSUPPORTED.
  • The error message is the trade result text (for example "not enough money"), optionally followed by ": " and the broker's own comment; return codes without a known text read "trade error <retcode>", which shows "trade error 0" when the broker returned no return code at all. Treat the message as human-readable only and branch on errorCode.
  • A clientOrderId is remembered for 5 minutes from when the first request started. A repeat that arrives while the first request is still running waits for it and receives exactly the same result, whatever that turns out to be; no new order is sent.
  • A definite failure (any errorCode other than TIMEOUT, including broker rejections, -2, -3, -100 and NOT_CONNECTED) releases the clientOrderId as soon as it is known, so a later retry with the same id sends a fresh attempt.
  • After a TIMEOUT, repeats of the same clientOrderId keep returning that TIMEOUT for the rest of the 5 minutes, even if the order was actually filled; reconcile with OpenedOrders rather than waiting for the replay to change.
  • NOT_CONNECTED also releases the clientOrderId, even when the connection dropped mid-request and the order may have been placed, so check OpenedOrders before retrying in that case.
  • The de-duplication key is the account plus the clientOrderId only. Reusing an id with different parameters within 5 minutes returns the earlier order's result and does not place the new order.
  • clientOrderId is not sent to the broker and does not appear on the order, in OpenedOrders or in history. Use comment or expertID if you need to tag orders.
  • De-duplication is not guaranteed to survive a service restart or redeploy, so do not rely on it beyond short-term retries.
  • stoploss and takeprofit are absolute prices, not distances in points.
  • A non-numeric value for stoploss, takeprofit, slippage, stopLimitPrice or expertID is silently ignored rather than rejected, so a typo can place an order without its stop-loss. A non-numeric price on a pending order is dropped too, which then fails with -2.
  • For a stop-limit order, price is the stop (activation) price and stopLimitPrice is the price of the limit order placed after activation.
  • expirationType is case-sensitive: "today" or "specified" in lower case fall through to good-till-cancelled unless expiration is also set.
  • expiration is passed to the broker unchanged, and brokers evaluate it against their trade-server clock, which often runs ahead of UTC.
  • The fill policy (fill-or-kill, immediate-or-cancel or return) is chosen automatically from what the symbol supports; you cannot set it.
  • If expertID is omitted, the order is stamped with magic number 0.
  • On a live (real-money) account, a request that passes the id and operation checks returns errorCode -100 unless live trading has been enabled for the service; no order is sent.
  • Because this is a GET request, make sure no proxy, prefetcher or HTTP retry layer replays it automatically; send a clientOrderId on every order.
curl -G https://api.traderail.cloud/v1/OrderSendTask \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "operation=Buy" \
--data-urlencode "symbol=EURUSD" \
--data-urlencode "volume=0.1"
Response 200application/json
{
"ticket": 812345678,
"order": 812345678,
"deal": 812345690,
"volume": 0.1,
"price": 1.08318,
"retcode": 10009,
"code": "OK",
"comment": "Request executed"
}
Try itAdd your API key above to send a live request
Fill in id, operation, symbol, volume
GET/v1/OrderModifyTask

Modify a position or pending order

Changes the stop-loss and take-profit of an open position, or the price, stop-loss, take-profit or stop-limit price of a pending order.

Pass the ticket of an open position or a working pending order. For a position, only stoploss and takeprofit can change. For a pending order, price, stoploss, takeprofit and stoplimit can change, and its expiration and time-in-force are kept. A field you omit keeps its current value, and sending 0 clears a stop-loss or take-profit. Trade outcomes, including rejections, timeouts and a disconnected account, return HTTP 200 with body.code "OK" or "ERROR". Only a missing or invalid id (400) or a bad ApiKey (401) uses a non-200 status.

ApiKey headerchanges state

Parameters

idstringrequired
The account: "acct-<login>" or the bare MT5 login.
ticketintegerrequired
Ticket of the open position or pending order to change, as listed in OpenedOrders. Positions are matched first.
stoplossnumberoptional
New stop-loss price. Omit to keep the current one; send 0 to remove it.
takeprofitnumberoptional
New take-profit price. Omit to keep the current one; send 0 to remove it.
pricenumberoptional
Pending orders only: new order price (the stop price for a stop-limit order). Omit to keep the current one. Ignored for positions.
stoplimitnumberoptional
Stop-limit orders only: new limit price. Omit to keep the current one. Ignored for positions.

Returns

Same trade result shape as OrderSendTask: { ticket, order, deal, volume, price, retcode, code: "OK", comment } on success. On failure the body is { code: "ERROR", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. errorCode is a string, and mqlErrorTradeIntCode is present only when the code is numeric. For a modify, deal, volume and price are normally 0, and ticket (order, or else deal, or else 0) can be 0 too.

Errors

20010004-10046body.code "ERROR", error.errorCode is the trade server retcode: the broker refused the change, for example "10016" for invalid stops or "10025" when the new values equal the current ones. Nothing changed.
2004-digit platform errorbody.code "ERROR": the request was refused before it reached the trade server, so no retcode exists. error.errorCode is the platform's runtime error number (for example "4756"). Nothing changed.
200-2body.code "ERROR", error.errorCode "-2": ticket is missing, non-numeric, or 0 or less (message "ticket is required").
200-8body.code "ERROR", error.errorCode "-8": no open position or pending order has this ticket (message "ticket not found: <ticket>").
200-100body.code "ERROR", error.errorCode "-100": the account is a live (real-money) account and live trading is not enabled for the service.
200TIMEOUTbody.code "ERROR", error.errorCode "TIMEOUT": the outcome was not learned in time, and the change may or may not have been applied. Read OpenedOrders to see the current values.
200NOT_CONNECTEDbody.code "ERROR", error.errorCode "NOT_CONNECTED": the account is not connected, or the connection dropped mid-request.
400ERRORThe id query parameter is missing or is neither "acct-<login>" nor a positive login. Body: { code: "ERROR", message: "account id (?id=) required" }.
401UnauthorizedThe ApiKey header is missing or not a valid key. Body: { statusCode: 401, message: "ApiKey required", error: "Unauthorized" }, with no code field.
Good to know
  • Do not treat ticket > 0 as the success test for a modify; check body.code === "OK" (or a retcode of 10008, 10009 or 10010).
  • On failure body.code is always "ERROR"; the specific reason is in error.errorCode, which is a string even for numeric codes such as "10016" or "-8".
  • Omitting stoploss keeps the existing stop-loss, but stoploss=0 removes it; the same applies to takeprofit.
  • The stop-limit parameter here is named stoplimit only; the stopLimitPrice spelling accepted by OrderSendTask is ignored on this endpoint.
  • Volume, expiration and order type cannot be changed here; cancel and re-place the order instead.
  • A non-numeric or empty stoploss, takeprofit, price or stoplimit is silently ignored, so that field keeps its current value.
  • A fractional ticket is truncated to a whole number before lookup, so ticket=1.5 looks up ticket 1.
  • Stop-loss and take-profit are absolute prices, not distances in points.
  • On a live (real-money) account without live trading enabled, every call returns error code "-100", whatever the other parameters are.
curl -G https://api.traderail.cloud/v1/OrderModifyTask \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "ticket=812345678" \
--data-urlencode "stoploss=1.081" \
--data-urlencode "takeprofit=1.09"
Response 200application/json
{
"ticket": 0,
"order": 0,
"deal": 0,
"volume": 0,
"price": 0,
"retcode": 10009,
"code": "OK",
"comment": "Request executed"
}
Try itAdd your API key above to send a live request
Fill in id, ticket
GET/v1/OrderCloseTask

Close a position

Closes an open position in full or in part at market, or cancels a pending order if you pass a pending-order ticket.

Pass a position ticket to close it at the current market price: a Buy position closes at the bid and a Sell position at the ask. Add lots to close only part of the position; leave it out to close all of it. If the ticket belongs to a pending order instead of a position, that order is cancelled, so this one endpoint can close or cancel anything that is open. Once the request is authenticated and carries a valid id, the reply is always HTTP 200: body.code is "OK" on success or "ERROR" on failure, and the reason is in body.error.errorCode.

ApiKey headerchanges state

Parameters

idstringrequired
The account: "acct-<login>" or the bare MT5 login.
ticketintegerrequired
Position ticket to close (OpenedOrders positionInfos[].ticket), or pending-order ticket to cancel (OpenedOrders openedOrders[].ticket).
lotsnumberoptional
Volume to close, in lots, for a partial close. Leave it out, or send 0, a negative value or more than the open volume, to close the whole position. Ignored when cancelling a pending order.
slippageintegeroptional
Maximum price deviation allowed for the closing trade, in points. Ignored when cancelling a pending order.
Default 10

Returns

Same trade-result shape as OrderSendTask. For a close, ticket is the closing order ticket (or the deal ticket if no order ticket was returned), deal is the closing deal, and volume and price are the volume closed and the price it closed at. A pending-order cancellation uses the same shape, but no deal takes place. retcode is 10009 (done), 10010 (partially done) or 10008 (placed) on success. On failure the body is { code: "ERROR", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }, still with HTTP 200.

Errors

200<broker retcode>The broker refused the close or cancel. Examples: "10018" market closed, "10036" position already closed, "10014" invalid volume. The code is in error.errorCode, and as a number in error.mqlErrorTradeIntCode. If the broker gives no retcode, a platform error number is returned instead. Nothing was closed.
200-2ticket is missing, not a number, zero or negative (message "ticket is required").
200-3The position's symbol could not be selected for trading (message "unknown symbol: <symbol>").
200-8No open position or pending order has this ticket (message "position or order not found: <ticket>").
200-100The account is a live (real-money) account and live trading is not enabled for the service.
200TIMEOUTThe outcome was not learned in time. The position may already be closed, so check OpenedOrders before retrying.
200NOT_CONNECTEDThe account is not connected, or the connection dropped mid-request.
400ERRORThe id query parameter is missing or invalid (message "account id (?id=) required").
401UnauthorizedThe ApiKey header is missing or not a valid key (message "ApiKey required").
Good to know
  • A lots value of 0, a negative value or more than the open volume is not an error: it closes the whole position, so double-check the value before sending a partial close.
  • lots is sent to the broker as given (only capped at the open volume). If the broker does not accept that volume, the call fails with a broker retcode such as 10014 and nothing is closed.
  • If you pass a pending-order ticket, that order is cancelled and lots and slippage are ignored.
  • OrderCloseTask has no clientOrderId de-duplication. After a TIMEOUT, read OpenedOrders before calling again. A retry of a partial close closes another lots of the same position, and a retry of a full close that already went through fails with -8.
  • Use positionInfos[].ticket from OpenedOrders as the ticket for a position (the same value is also returned as positionTicket and identifier). For a pending order, use openedOrders[].ticket.
  • A failure's code is in body.error.errorCode (a string). body.code is just "ERROR" for every failure.
curl -G https://api.traderail.cloud/v1/OrderCloseTask \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "ticket=812345678"
Response 200application/json
{
"ticket": 812345801,
"order": 812345801,
"deal": 812345812,
"volume": 0.1,
"price": 1.08305,
"retcode": 10009,
"code": "OK",
"comment": "Request executed"
}
Try itAdd your API key above to send a live request
Fill in id, ticket
GET/v1/OrderCancelTask

Cancel a pending order

Cancels (deletes) a working pending order by its ticket.

Use this to remove a limit, stop or stop-limit order that has not triggered yet. It only accepts tickets of pending orders that are still working. To close an open position, use OrderCloseTask. If the order has already triggered, expired or been cancelled, it is no longer a working pending order and the call fails with error.errorCode "-8". The HTTP status is 200 whether the cancel succeeds or fails, so check body.code: "OK" means the order was removed, and "ERROR" means it was not.

ApiKey headerchanges state

Parameters

idstringrequired
The account: "acct-<login>" or the bare MT5 login.
ticketintegerrequired
Ticket of the pending order to cancel, from OpenedOrders.openedOrders[].ticket.

Returns

On success, the same trade result shape as OrderSendTask: { ticket, order, deal, volume, price, retcode, code: "OK", comment }. ticket equals order, and deal is 0 for a cancel. On failure: { code: "ERROR", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. errorCode and mqlErrorTradeCode are strings (for example "-8" or "TIMEOUT"). mqlErrorTradeIntCode is the same code as a number and is present only when the code is numeric.

Errors

20010004-10046The broker refused to cancel the order. errorCode is usually the broker's trade return code; if the broker gave none, it is the platform's error number instead.
200-2ticket is missing, non-numeric, zero or negative (message "ticket is required").
200-8No working pending order has this ticket. This includes an open position's ticket and an order that already triggered, expired or was cancelled (message "pending order not found: <ticket>").
200-100The account is a live (real-money) account and live trading is not enabled for the service.
200TIMEOUTThe outcome was not learned in time, so the order may or may not have been cancelled. Check OpenedOrders before retrying.
200NOT_CONNECTEDThe account is not connected, or the connection dropped mid-request.
400ERRORThe id query parameter is missing or invalid (message "account id (?id=) required").
401UnauthorizedThe ApiKey header is missing or not a valid key. The body is { statusCode: 401, message: "ApiKey required", error: "Unauthorized" }.
Good to know
  • A position ticket is not accepted here and returns errorCode "-8". Use OrderCloseTask for positions.
  • An order can trigger between your read of OpenedOrders and this call. If you then get "-8", the order has probably become a position, so read OpenedOrders again.
  • A decimal ticket is truncated to a whole number rather than rejected, so always pass the integer ticket exactly as OpenedOrders returned it.
  • Failures still return HTTP 200, so always branch on body.code rather than on the HTTP status.
curl -G https://api.traderail.cloud/v1/OrderCancelTask \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "ticket=812345730"
Response 200application/json
{
"ticket": 812345730,
"order": 812345730,
"deal": 0,
"volume": 0,
"price": 0,
"retcode": 10009,
"code": "OK",
"comment": "Request executed"
}
Try itAdd your API key above to send a live request
Fill in id, ticket
GET/v1/OrderCloseByTask

Close a position by an opposite one

Closes a position against an opposite position on the same symbol (MT5 Close By), on hedging accounts only.

On a hedging account you can hold a Buy and a Sell on the same symbol at the same time. Close By settles one against the other in a single trade operation instead of closing each separately. Pass the position to close as ticket and the opposite position as closeByTicket (or its alias ticketBy). The call always answers HTTP 200 once the account id is valid, with body.code "OK" on success or "ERROR" on failure. The specific failure reason is in body.error.errorCode.

ApiKey headerchanges state

Parameters

idstringrequired
The account: "acct-<login>" or the bare MT5 login.
ticketintegerrequired
Ticket of the position to close.
closeByTicketintegerrequired
Ticket of the opposite-direction position on the same symbol used to close it. You may send ticketBy instead; one of the two is required.
ticketByintegeroptional
Alias of closeByTicket. Used only when closeByTicket is not sent at all; an empty closeByTicket= is not replaced by it.

Returns

Same trade result shape as OrderSendTask. On success: { ticket, order, deal, volume, price, retcode, code: "OK", comment }, where ticket is order (or deal when order is 0). On failure, still HTTP 200: { code: "ERROR", message, error: { errorCode, errorMessage, mqlErrorTradeIntCode, mqlErrorTradeCode } }. Here errorCode and mqlErrorTradeCode are the failure code as a string. mqlErrorTradeIntCode is the same code as a number, and is present only when the code is numeric. Example failure: {"code":"ERROR","message":"invalid request","error":{"errorCode":"10013","errorMessage":"invalid request","mqlErrorTradeIntCode":10013,"mqlErrorTradeCode":"10013"}}

Errors

200<broker retcode>The broker or trading platform refused the Close By, for example on a netting account or when the two positions are not opposite positions on the same symbol. body.code is "ERROR", and error.errorCode holds the MT5 trade retcode, or the platform error code when no retcode was given.
200-2ticket, or closeByTicket/ticketBy, is missing, non-numeric, zero or negative (body.code "ERROR", error.errorCode "-2").
200-100The account is a live (real-money) account and trading on live accounts is not enabled for the service (body.code "ERROR", error.errorCode "-100").
200TIMEOUTThe outcome was not learned in time (body.code "ERROR", error.errorCode "TIMEOUT"). The Close By may still have executed, so check OpenedOrders before retrying.
200NOT_CONNECTEDThe account is not connected, or its connection dropped mid-request (body.code "ERROR", error.errorCode "NOT_CONNECTED").
200ERRThe request could not be delivered to the account (body.code "ERROR", error.errorCode "ERR").
400ERRORThe id query parameter is missing or invalid; the body is { "code": "ERROR", "message": "account id (?id=) required" }.
401UnauthorizedThe ApiKey header is missing or not a valid key; the body is { "message": "ApiKey required", "error": "Unauthorized", "statusCode": 401 }.
Good to know
  • Close By only works on hedging accounts; on a netting account the request is rejected and the rejection code is returned in error.errorCode.
  • Both tickets must be open positions on the same symbol in opposite directions; pending-order tickets are not accepted.
  • The service does not check the two positions first; any mismatch comes back as the broker's rejection code.
  • A failed Close By still returns HTTP 200, so always check body.code and do not rely on the HTTP status.
  • On a live (real-money) account without live trading enabled, the call returns -100 before the tickets are checked.
  • A TIMEOUT does not mean the Close By failed; check OpenedOrders before sending it again.
  • Decimal ticket values are truncated to whole numbers, so always send the exact integer ticket.
curl -G https://api.traderail.cloud/v1/OrderCloseByTask \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "ticket=812345678" \
--data-urlencode "closeByTicket=812345755"
Response 200application/json
{
"ticket": 812345820,
"order": 812345820,
"deal": 812345831,
"volume": 0.1,
"price": 1.0825,
"retcode": 10009,
"code": "OK",
"comment": "Request executed"
}
Try itAdd your API key above to send a live request
Fill in id, ticket, closeByTicket

History & analytics

Closed trades, deals, pending-order history and performance statistics.

GET/v1/OrderHistory

List closed trades

Returns up to 1,000 closed trades from a time window, newest close first.

Returns the account's closed trades within a time window, defaulting to the last 90 days. You get one row per position, newest close first. Each row merges the position's opening and closing deals into one trade, with volume-weighted open and close prices, open and close times, closed volume, and net profit after swap and commission. A trade is in the window when its last closing deal is inside it, so a position opened before from but closed inside the window is included. The response holds at most the 1,000 most recent trades. Use OrderHistoryPagination if a window can hold more than that or you need a different sort order.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive. A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included, so a trade that just closed on a broker clock running ahead of UTC is not cut off. A date with no time (2026-09-30) means midnight at the start of that day. If you send a value that cannot be parsed, the window ends at the current time without the two-day allowance.
Default now

Returns

An object with two keys, orderHistory and orders, that hold the same array of closed-trade rows. ticket, positionTicket and orderTicket all hold the position id. orderType and type are "Buy" or "Sell". lots, volume and closeLots are the total closed volume. openPrice/priceOpen and closePrice/priceClose/priceCurrent are volume-weighted average entry and exit prices. openTime/positionOpenTime is the first entry and closeTime/positionCloseTime the last exit, both written as YYYY-MM-DDTHH:mm:ss with no zone. profit is net (gross + swap + commission) and grossProfit is before swap and commission. magic, magicNumber and expertId hold the position's magic number. accountLogin is the numeric login. stopLoss and takeProfit are always 0, and state is always "filled".

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or reported a failure. Body: {"code":"ERROR","message":"..."}.
Good to know
  • orderHistory and orders always hold the same rows, so read whichever key your code expects.
  • The response stops at the 1,000 most recent trades and does not tell you when older trades were left out. Use OrderHistoryPagination for complete exports.
  • sort, ascending, pageNumber and ordersPerPage are ignored here. Rows always come newest close first.
  • ticket, positionTicket and orderTicket all hold the position id, not an order ticket. Pass it to HistoryDealsByPositionId to see the individual deals and their real order tickets.
  • stopLoss and takeProfit are always 0 because the history does not keep them, and state is always "filled".
  • profit is net of swap and commission and grossProfit is before them. Both are in the account's deposit currency and rounded to 2 decimal places.
  • openPrice and closePrice are volume-weighted averages and are not rounded, so they can carry floating-point digits. Round them to the symbol's digits before you display them.
  • Volumes (lots, volume, closeLots) are rounded to 2 decimal places.
  • The search starts two days before from, so a trade opened earlier than that comes back without its opening leg. openPrice is then 0, openTime is the time of the first closing deal, the opening commission is missing from profit, and orderType shows the side of the closing deal, which is the opposite of the trade's direction. Move from back to cover the open if you need these fields.
  • A position closed in several parts is a single row: lots is the total volume closed so far, closePrice is the volume-weighted average exit and closeTime is the last exit. A position that is still partly open already appears with the volume closed so far.
  • magic, magicNumber and expertId are the first non-zero magic number found on the position's deals, or 0.
curl -G https://api.traderail.cloud/v1/OrderHistory \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-09-01T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59"
Response 200application/json
{
"orderHistory": [
{
"ticket": 512340871,
"positionTicket": 512340871,
"orderTicket": 512340871,
"symbol": "EURUSD",
"orderType": "Buy",
"type": "Buy",
"lots": 0.1,
"volume": 0.1,
"closeLots": 0.1,
"openPrice": 1.08425,
"priceOpen": 1.08425,
"closePrice": 1.08712,
"priceClose": 1.08712,
"priceCurrent": 1.08712,
"openTime": "2026-09-14T09:31:07",
"positionOpenTime": "2026-09-14T09:31:07",
"closeTime": "2026-09-15T14:02:45",
"positionCloseTime": "2026-09-15T14:02:45",
"stopLoss": 0,
"takeProfit": 0,
"profit": 27.58,
"grossProfit": 28.7,
"swap": -0.42,
"commission": -0.7,
"magic": 0,
"magicNumber": 0,
"expertId": 0,
"accountLogin": 700100200,
"state": "filled"
}
],
"orders": [
{
"ticket": 512340871,
"positionTicket": 512340871,
"orderTicket": 512340871,
"symbol": "EURUSD",
"orderType": "Buy",
"type": "Buy",
"lots": 0.1,
"volume": 0.1,
"closeLots": 0.1,
"openPrice": 1.08425,
"priceOpen": 1.08425,
"closePrice": 1.08712,
"priceClose": 1.08712,
"priceCurrent": 1.08712,
"openTime": "2026-09-14T09:31:07",
"positionOpenTime": "2026-09-14T09:31:07",
"closeTime": "2026-09-15T14:02:45",
"positionCloseTime": "2026-09-15T14:02:45",
"stopLoss": 0,
"takeProfit": 0,
"profit": 27.58,
"grossProfit": 28.7,
"swap": -0.42,
"commission": -0.7,
"magic": 0,
"magicNumber": 0,
"expertId": 0,
"accountLogin": 700100200,
"state": "filled"
}
]
}
Try itAdd your API key above to send a live request
GET/v1/OrderHistoryPagination

Page through closed trades

Returns closed trades from a time window one page at a time, sorted by close or open time.

Returns the same closed-trade rows as OrderHistory, split into zero-based pages. You choose the page size and whether to sort by close or open time, ascending or descending. There is no 1,000-row cap, so use this endpoint to export a whole window. The response gives the number of pages but no total row count. Each page request rebuilds the window from scratch, so pin both from and to when you need pages that stay consistent while trades are still closing.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive, matched against each trade's close time. The comparison uses the trade timestamps as they appear in the rows (the broker's server time, shown without a zone suffix), so a date-time without a zone suffix is compared as written. If the value is empty or cannot be parsed, the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive, matched against each trade's close time on the same clock as from. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included. If you send a value that cannot be parsed, it is treated as now without that two-day allowance.
Default now
sortstringoptional
Sort key. Only the exact, case-sensitive value CloseTime sorts by close time. Any other value sorts by open time.
One of CloseTime OpenTime
Default CloseTime
ascendingbooleanoptional
Pass true (any letter case) for oldest first. Any other value, or leaving it out, gives newest first.
Default false
pageNumberintegeroptional
Zero-based page index. Fractions are rounded down, negative values are treated as 0, and a non-numeric value gives page 0.
Default 0
ordersPerPageintegeroptional
Page size. Fractions are rounded down, values are clamped to the range 1-1000, and a non-numeric value gives the default of 100.
Default 100

Returns

pagesCount is the number of pages for this page size and is at least 1. pageNumber is the page served. itemsPerPage is the page size actually used after clamping. orders and orderHistory hold the same array of closed-trade rows (see OrderHistory for the row fields). The example shows the second page (pageNumber=1) of a three-trade window with ordersPerPage=2.

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), its history did not arrive within 30 seconds, or the account reported a failure. Body: {"code":"ERROR","message":"..."}.
Good to know
  • A window with no trades returns pagesCount 1 and an empty orders array, not 0 pages.
  • Asking for a page past the last one returns an empty orders array, not an error.
  • There is no total-count field. The total is at most pagesCount × itemsPerPage, and you know the exact count once you have read the last page.
  • sort is case-sensitive: closetime or CLOSETIME silently sorts by open time.
  • ascending only recognises the word true. Values such as 1 or yes give newest-first order.
  • If you send ordersPerPage with an empty value (ordersPerPage=), it is read as 0 and clamped to a page size of 1. Either give a number or leave the parameter out.
  • Each page is rebuilt from the account history when you request it. If a trade closes between two page requests, rows can shift between pages unless both from and to are fixed.
  • openTime and closeTime are the broker's server time with no zone suffix, and from/to are compared directly against them, so give your window boundaries on that same clock rather than converting them to true UTC.
  • Do not send a to value you are unsure parses. An unparseable to behaves like now but drops the two-day allowance, so trades closed in the last few hours can disappear.
  • A position that is only partly closed already appears as a row, with lots, volume and closeLots giving the volume closed so far. All closing fills of a position are merged into one row stamped with the latest close time.
  • Rows carry the same caveats as OrderHistory: stopLoss and takeProfit are always 0, and prices are unrounded volume-weighted averages.
  • If a trade's opening fill is more than two days before from, the row comes back without its opening leg: openPrice is 0, orderType is inverted, and openTime shows the first closing fill's time.
curl -G https://api.traderail.cloud/v1/OrderHistoryPagination \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-09-01T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59" \
--data-urlencode "sort=CloseTime" \
--data-urlencode "ascending=false" \
--data-urlencode "pageNumber=0" \
--data-urlencode "ordersPerPage=100"
Response 200application/json
{
"pagesCount": 2,
"pageNumber": 1,
"itemsPerPage": 2,
"orders": [
{
"ticket": 512339120,
"positionTicket": 512339120,
"orderTicket": 512339120,
"symbol": "XAUUSD",
"orderType": "Sell",
"type": "Sell",
"lots": 0.05,
"volume": 0.05,
"closeLots": 0.05,
"openPrice": 2412.37,
"priceOpen": 2412.37,
"closePrice": 2398.91,
"priceClose": 2398.91,
"priceCurrent": 2398.91,
"openTime": "2026-09-15T15:20:11",
"positionOpenTime": "2026-09-15T15:20:11",
"closeTime": "2026-09-16T10:05:33",
"positionCloseTime": "2026-09-16T10:05:33",
"stopLoss": 0,
"takeProfit": 0,
"profit": 66.8,
"grossProfit": 67.3,
"swap": 0,
"commission": -0.5,
"magic": 0,
"magicNumber": 0,
"expertId": 0,
"accountLogin": 700100200,
"state": "filled"
}
],
"orderHistory": [
{
"ticket": 512339120,
"positionTicket": 512339120,
"orderTicket": 512339120,
"symbol": "XAUUSD",
"orderType": "Sell",
"type": "Sell",
"lots": 0.05,
"volume": 0.05,
"closeLots": 0.05,
"openPrice": 2412.37,
"priceOpen": 2412.37,
"closePrice": 2398.91,
"priceClose": 2398.91,
"priceCurrent": 2398.91,
"openTime": "2026-09-15T15:20:11",
"positionOpenTime": "2026-09-15T15:20:11",
"closeTime": "2026-09-16T10:05:33",
"positionCloseTime": "2026-09-16T10:05:33",
"stopLoss": 0,
"takeProfit": 0,
"profit": 66.8,
"grossProfit": 67.3,
"swap": 0,
"commission": -0.5,
"magic": 0,
"magicNumber": 0,
"expertId": 0,
"accountLogin": 700100200,
"state": "filled"
}
]
}
Try itAdd your API key above to send a live request
GET/v1/TradeStats

Get trading statistics

Summarises the closed trades in a time window: counts, volume, profit totals, averages and the best and worst trade.

Calculates performance figures from the account's closed trades in a time window (default: the last 90 days). The trades are built and selected exactly as in OrderHistory, but all of them are used, not only the 1,000 most recent. Every money figure is net of swap and commission, in the account's deposit currency, and rounded to 2 decimal places. Use it for dashboards and account summaries. For win rate or similar ratios, divide the counts it returns yourself.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive, matched against each trade's close time. A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive. If you leave it out, trades stamped up to two days ahead of the current UTC time are still included. If you send a value that cannot be parsed, now is used, but without that two-day allowance.
Default now

Returns

trades: number of closed trades in the window. lots: their total closed volume. summary.totalProfit: sum of net profit. summary.dayProfit, weekProfit and monthProfit: net profit of the window's trades that closed within the last 24 hours, 7 days and 30 days. profitability.wonTrades and lostTrades: trades with net profit above and below zero. averageWin.averageUsd: mean net profit of winning trades. averageLost.averageUsd: mean net loss of losing trades, as a positive number. bestTrade and worstTrade: the highest and lowest net profit, each with the close time as date. longsWon.all and shortsWon.all: number of Buy and Sell trades. The example covers three trades, calculated at 2026-09-16T12:00:00 UTC.

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or reported a failure. Body: {"code":"ERROR","message":"..."}.
Good to know
  • Despite their names, longsWon.all and shortsWon.all count every Buy and every Sell trade, winners and losers alike.
  • Despite the name averageUsd, no currency conversion is done: both averages are in the account's deposit currency.
  • averageLost.averageUsd is a positive number (the size of the average loss), while worstTrade.profit keeps its sign.
  • A trade whose net profit is exactly 0 counts toward trades but toward neither wonTrades nor lostTrades.
  • With no trades in the window, bestTrade and worstTrade are {"profit": 0, "date": null}. If every trade lost, bestTrade is the smallest loss and is negative.
  • Trade times are recorded on the trading server's clock, which is often a few hours ahead of UTC. bestTrade.date and worstTrade.date are that server time, written without a zone suffix. from, to and the rolling periods are compared against those times, so trades near a window edge can fall on a different side than a pure-UTC calculation would put them.
  • dayProfit, weekProfit and monthProfit are rolling 24-hour, 7-day and 30-day periods ending now, not calendar periods, and only count trades already inside the window. If to is in the past, or from is less than 30 days ago, they do not cover the full period.
  • There are no win-rate, profit-factor or drawdown fields. For win rate, use profitability.wonTrades / trades.
  • If a trade was opened roughly two days or more before from, its opening fill can be missing from the calculation. The trade is then counted on the opposite side in longsWon and shortsWon, and its opening commission is left out of the totals. Start the window earlier if that matters.
curl -G https://api.traderail.cloud/v1/TradeStats \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-09-01T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59"
Response 200application/json
{
"trades": 3,
"lots": 0.35,
"summary": {
"totalProfit": 67.98,
"dayProfit": 1.18,
"weekProfit": 67.98,
"monthProfit": 67.98
},
"profitability": {
"wonTrades": 2,
"lostTrades": 1
},
"averageWin": {
"averageUsd": 47.19
},
"averageLost": {
"averageUsd": 26.4
},
"bestTrade": {
"profit": 66.8,
"date": "2026-09-13T10:05:33"
},
"worstTrade": {
"profit": -26.4,
"date": "2026-09-16T11:48:02"
},
"longsWon": {
"all": 1
},
"shortsWon": {
"all": 2
}
}
Try itAdd your API key above to send a live request
GET/v1/TradeStatsEquityHistory

Get the balance curve (wrapped)

Returns the account's running balance and realised-P/L curve for a time window, wrapped in a charts object.

Returns the same points as EquityHistory, wrapped under a charts key. The curve is rebuilt from the account's deal history and sorted oldest first. You get a point for every closing fill, every deposit, withdrawal or other balance operation, and every opening fill that carried a commission or other charge. Opening fills with no charge are skipped. Each point carries the running balance and the running realised P/L (trading results only) at that moment. Use it to draw a balance or realised-P/L chart for a period. It covers closed results and account operations only, so floating P/L on open positions is not included.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive. It is compared with each point's time. A date-time without a zone suffix is read as UTC. A value that cannot be parsed is ignored and the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive. If you leave it out, points stamped up to two days after the current UTC time are still included. If you pass a value that cannot be parsed, the window ends at the current time with no allowance after it.
Default now

Returns

{ charts: [...] }, where each element is { time, balance, realizedPL }. time has the form YYYY-MM-DDTHH:mm:ss with no zone, on the broker's trade-server clock. balance and realizedPL are running totals rounded to 2 decimal places. The example uses from=2026-09-13T00:00:00 on an account funded with 10,000 on 2026-09-01. That deposit is more than two days before from, so it is not in the running total. The first point still includes the commission of an opening fill on 2026-09-12, which falls inside the two-day lead-in.

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or reported a failure. Body: {"code":"ERROR","message":"..."}.
Good to know
  • The only difference from EquityHistory is the {"charts": [...]} wrapper. The points are identical.
  • balance starts at 0 two days before from and adds up only the deals since then. It matches the real account balance only when the window starts before the account's first deposit. With the 90-day default it is usually an offset curve.
  • If the window runs up to now, you can anchor the curve by shifting every point by AccountSummary.balance minus the last point's balance. This is exact only when no credit operations fall in the window, because credit adjustments are added to balance here.
  • realizedPL counts trading results only (profit, swap and commission on fills). Deposits, withdrawals and other balance operations move balance but not realizedPL.
  • Point times are on the broker's trade-server clock, not UTC, and from/to are compared directly with them. For window edges that line up with the times you see, pass bare date-times on that same clock.
  • A closing fill that nets exactly zero still produces a point, with an unchanged balance.
  • GET /v1/TradeUnrealizedPL exists only for compatibility. It needs no id and always returns {"data": []}. Read floating P/L from the profit fields of AccountSummary and OpenedOrders instead.
curl -G https://api.traderail.cloud/v1/TradeStatsEquityHistory \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-09-13T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59"
Response 200application/json
{
"charts": [
{
"time": "2026-09-13T10:05:33",
"balance": 66.8,
"realizedPL": 66.8
},
{
"time": "2026-09-14T09:31:07",
"balance": 66.45,
"realizedPL": 66.45
},
{
"time": "2026-09-15T14:02:45",
"balance": 94.38,
"realizedPL": 94.38
},
{
"time": "2026-09-16T07:12:40",
"balance": 93.68,
"realizedPL": 93.68
},
{
"time": "2026-09-16T11:48:02",
"balance": 67.98,
"realizedPL": 67.98
}
]
}
Try itAdd your API key above to send a live request
GET/v1/EquityHistory

Get the balance curve

Returns the account's running balance and realised P/L as an array of points, one per deal that moves the account, for a time window.

Replays the account's deals in time order and returns a point for each one: trade exits, deposits, withdrawals, credit and other cash operations. Opening fills are included only if they charged a fee (commission or swap). Each point holds the running balance and running realised P/L at that moment, oldest first. The window only decides which points are returned. Running totals carry on across the start of the window, so the first point continues the curve rather than restarting at zero. Despite the name, this is a balance curve: floating P/L on open positions is not included.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive, compared against each point's time. A date-time without a zone suffix is read as UTC. Numbers above 100000000000 are treated as milliseconds. If the value cannot be parsed it is ignored and the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive. If you leave it out, points stamped up to two days after the current UTC time are still returned, so recent deals on brokers whose server clock runs ahead of UTC are not lost. If you supply it, points after it are dropped exactly. A value that cannot be parsed is treated as now, without the two-day allowance.
Default now

Returns

A JSON array of { time, balance, realizedPL }, oldest first. time is YYYY-MM-DDTHH:mm:ss on the broker's server clock, with no zone suffix. balance is the running total of every deal replayed (trades, deposits, withdrawals, credit and other cash operations). realizedPL is the running total of trading deals only (profit + swap + commission). Both are numbers rounded to 2 decimal places. In this example the window starts before the account's first deposit and there is no credit, so balance follows the account's real balance.

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or reported a failure while reading its history. Body: {"code":"ERROR","message":"..."}.
Good to know
  • balance and realizedPL both start at 0 two days before from, not at the account's opening balance. They track the real account only if that point is before the account's first deposit, so for older accounts the default 90-day window gives a curve shifted by a constant.
  • To anchor the curve when you leave out to, add AccountSummary.balance minus the last point's balance to every point. This does not work when you set to, because the last point then is not the latest balance.
  • Credit operations and other non-trade cash movements move balance just like deposits and withdrawals, while only trading deals move realizedPL. AccountSummary.balance does not include credit, so on an account that has received credit the curve sits above it by the credit total.
  • Point times are the broker's server time, not UTC, and from and to are compared against those times directly. Expect an offset of a few hours from UTC on most brokers.
  • An opening fill with no commission or swap produces no point. One that charges commission produces a small step down. A break-even exit still produces a point even though balance does not change.
  • There is one point per deal and no down-sampling, so several points can share a timestamp, and long windows on active accounts return large arrays.
  • Points are selected by deal time, not by trade close time, so this window filter is not the same as OrderHistory's.
  • This endpoint returns a bare array. TradeStatsEquityHistory returns the same points wrapped as {"charts": [...]}.
  • GET /v1/TradeUnrealizedPL always returns {"data": []}. For floating P/L, read the profit fields of AccountSummary and OpenedOrders.
curl -G https://api.traderail.cloud/v1/EquityHistory \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-08-25T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59"
Response 200application/json
[
{
"time": "2026-09-01T08:00:00",
"balance": 10000,
"realizedPL": 0
},
{
"time": "2026-09-12T15:20:11",
"balance": 9999.75,
"realizedPL": -0.25
},
{
"time": "2026-09-13T10:05:33",
"balance": 10066.8,
"realizedPL": 66.8
},
{
"time": "2026-09-14T09:31:07",
"balance": 10066.45,
"realizedPL": 66.45
},
{
"time": "2026-09-15T14:02:45",
"balance": 10094.38,
"realizedPL": 94.38
},
{
"time": "2026-09-16T07:12:40",
"balance": 10093.68,
"realizedPL": 93.68
},
{
"time": "2026-09-16T11:48:02",
"balance": 10067.98,
"realizedPL": 67.98
}
]
Try itAdd your API key above to send a live request
GET/v1/HistoryDealsByPositionId

List the deals of a position

Returns every deal (fill) recorded for one position id, as the account records them.

Returns all deals that belong to a single position: the entry, any partial exits and the final exit, each with its own price, volume, commission, swap and profit. Use it to drill into a closed-trade row (its ticket is the position id) or to check how an open position was filled. It covers the whole life of the position and takes no time window. Rows come back as the account records them: times are epoch seconds rather than date strings, type and entry are upper-case codes, and profit is per deal and gross.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
positionIdintegerrequired
The position id: the ticket of a closed-trade row from OrderHistory, or of an open position. Required unless you send ticket instead.
ticketintegeroptional
Alias for positionId, read only when positionId is absent. If positionId is present but invalid, ticket is ignored.

Returns

A JSON array of deals. Each has ticket (deal ticket), order (the order that produced the deal), position, time (epoch seconds), type (BUY, SELL, BALANCE, CREDIT or OTHER), entry (IN, OUT, INOUT, OUT_BY or OTHER), symbol, volume, price, commission, swap, profit (gross, for this deal only), magic and comment.

Errors

400ERRORpositionId is not a positive integer, or, when positionId is absent, ticket is missing or not a positive integer. Body: {"code":"ERROR","message":"positionId (or ticket) required"}. This check runs before the id check.
400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or could not load the position's history. Body: {"code":"ERROR","message":"..."}.
Good to know
  • time is an integer number of epoch seconds on the broker's trade-server clock, not the YYYY-MM-DDTHH:mm:ss string the closed-trade endpoints use. Format it as UTC and you get the same wall-clock value those endpoints show.
  • A closing deal's type is the opposite of the position's direction: a Buy position is opened by a BUY deal and closed by a SELL deal. Read the position's direction from the IN deal.
  • profit on each deal is gross. The trade's net result is the sum of profit + swap + commission over all its deals. When the closed-trade row covers all of the position's deals, this matches that row's profit, which is rounded to 2 decimals.
  • The order field on each deal is the real order ticket that produced it. Closed-trade rows from OrderHistory put the position id in orderTicket, ticket and positionTicket, so use this endpoint when you need the actual order tickets.
  • The field names here (single lower-case words) differ from the camelCase rows of OrderHistory, so do not reuse the same parser for both.
  • The API does not re-sort the deals. Sort by time on your side if your code depends on the order.
curl -G https://api.traderail.cloud/v1/HistoryDealsByPositionId \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "positionId=512340871"
Response 200application/json
[
{
"ticket": 498177302,
"order": 512340871,
"position": 512340871,
"time": 1789378267,
"type": "BUY",
"entry": "IN",
"symbol": "EURUSD",
"volume": 0.1,
"price": 1.08425,
"commission": -0.35,
"swap": 0,
"profit": 0,
"magic": 0,
"comment": ""
},
{
"ticket": 498190846,
"order": 512361577,
"position": 512340871,
"time": 1789480965,
"type": "SELL",
"entry": "OUT",
"symbol": "EURUSD",
"volume": 0.1,
"price": 1.08712,
"commission": -0.35,
"swap": -0.42,
"profit": 28.7,
"magic": 0,
"comment": ""
}
]
Try itAdd your API key above to send a live request
GET/v1/PendingOrderHistory

List past pending orders

Returns the limit, stop and stop-limit orders in the account's order history within a time window.

Lists the pending orders in the account's order history together with the state each one was recorded in. Market buy and sell orders are left out. An order is in the window when its completion time (time_done, or time_setup if it has no completion time) is inside it; the default window is the last 90 days. Rows come back as the account records them: snake_case field names, epoch-second times and upper-case type codes. For pending orders that are still working, use OpenedOrders.

ApiKey header

Parameters

idstringrequired
The account to read: the session token returned by ConnectEx (acct-<login>) or the bare login number.
fromstring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
Start of the window, inclusive, matched against time_done (or time_setup). A date-time without a zone suffix is read as UTC. If the value cannot be parsed it is ignored and the default is used.
Default 90 days before now
tostring (epoch seconds, epoch milliseconds, or ISO-8601 date-time)optional
End of the window, inclusive. If you leave it out, orders stamped up to two days ahead of the current UTC time are still included.
Default now

Returns

A JSON array of orders. Each has ticket, symbol, type, state, volume_initial (requested volume), price_open (entry price), sl, tp, price_stoplimit, time_setup and time_done (epoch seconds), magic and comment.

Errors

400ERRORThe id query parameter is missing, or is neither acct-<login> nor a positive login number. Body: {"code":"ERROR","message":"account id (?id=) required"}.
401UnauthorizedThe ApiKey header is missing, or is not a valid, active key. Body: {"message":"ApiKey required","error":"Unauthorized","statusCode":401}.
502ERRORThe account has no live session (message "account <login> is not connected"), did not answer within 30 seconds, or could not load its order history. Body: {"code":"ERROR","message":"..."}.
Good to know
  • type is one of BUY_LIMIT, SELL_LIMIT, BUY_STOP, SELL_STOP, BUY_STOP_LIMIT or SELL_STOP_LIMIT. Any other non-market order kind, such as a close-by order, shows as OTHER.
  • state is one of started, placed, canceled, partial, filled, rejected, expired, request_add, request_modify, request_cancel or unknown. canceled is spelled with one l.
  • These rows use snake_case names (price_open, time_done) and the values are not reformatted, so they look different from the OpenedOrders rows (priceOpen, timeSetup) for the same order.
  • symbol is the account's own symbol name and may include a broker suffix (for example XAUUSD.s).
  • time_setup and time_done are integer epoch seconds on the broker's trade-server clock. Format them as UTC to get the same wall-clock values the other history endpoints show.
  • The default for from is 90 days before the current time, not 90 days before to. If you send only a to that is more than 90 days in the past, the window is empty and you get [], so always send from together with to.
  • volume_initial is the volume originally requested. There is no filled-volume field.
  • price_stoplimit is 0 unless the order is a stop-limit order.
  • The response is not paginated and has no symbol filter, and the API does not re-sort the rows. Filter and sort on your side.
curl -G https://api.traderail.cloud/v1/PendingOrderHistory \
-H "ApiKey: YOUR_API_KEY" \
--data-urlencode "id=acct-700100200" \
--data-urlencode "from=2026-09-01T00:00:00" \
--data-urlencode "to=2026-09-30T23:59:59"
Response 200application/json
[
{
"ticket": 512355018,
"symbol": "EURUSD",
"type": "BUY_LIMIT",
"state": "filled",
"volume_initial": 0.1,
"price_open": 1.0825,
"sl": 1.078,
"tp": 1.0905,
"price_stoplimit": 0,
"time_setup": 1789027200,
"time_done": 1789134130,
"magic": 0,
"comment": ""
},
{
"ticket": 512357730,
"symbol": "XAUUSD",
"type": "SELL_STOP",
"state": "canceled",
"volume_initial": 0.05,
"price_open": 2385.5,
"sl": 2401,
"tp": 2350,
"price_stoplimit": 0,
"time_setup": 1789203600,
"time_done": 1789489800,
"magic": 20260912,
"comment": ""
}
]
Try itAdd your API key above to send a live request

Copy-trade & system

Act on many accounts with one call, and check the health of the service.

POST/orchestrator/connect

Connect an account

Start a managed session for an MT5 account and wait until it is signed in to the broker.

Brings an MT5 account online and returns only once it is signed in to the broker's trade server and ready for API calls. If the account is already online, the call returns straight away with alreadyConnected: true and the running session is not restarted, although the idleHours, server and symbol you send replace the ones on record for that account. Starting a new session usually takes tens of seconds and can take up to about two minutes, so set your HTTP client timeout to match. After a successful connect the service keeps the account online and restarts the session automatically if it drops. That continues until you call POST /orchestrator/disconnect, the account's idle window runs out, or automatic recovery gives up. Use the returned id (acct-<login>) or the bare login as ?id= on /v1 calls.

Bearer tokenchanges state

Parameters

userinteger (JSON number, must be > 0)required
The MT5 account login (account number). Send it as a JSON number. A quoted string such as "700100200" is rejected with 400.
passwordstring (non-empty, max 256 characters)required
The MT5 account password, either the master or the investor (read-only) password. It is used to sign in to the broker, is never written to logs and is not kept in plain text afterwards. Use the master password if you intend to trade.
serverstring (non-empty, max 128 characters)required
The broker's MT5 trade server name, exactly as MetaTrader 5 shows it.
idleHoursnumber (0 to 720, fractions allowed)optional
Stop this account's session after this many hours with no API activity on it. 0 means never stop it. If you leave the field out, the service default applies. Fractions are allowed, so 0.5 means 30 minutes. Every connect call sets this value again, including a call for an account that is already online.
Default service default
symbolstring (max 64 characters, letters, digits and . _ # / + ! - only)optional
Optional. Overrides the default symbol the account session opens on. It must be a symbol that exists on this broker exactly as written. If it does not exist, the session never becomes ready and the connect fails. Most callers should leave it out.

Returns

The account is online. id is the account id to use with ?id= on /v1 calls. login echoes the account number. status is always "connected". alreadyConnected is true when the account was already online and the call answered immediately. It is false when the account came online during this call, which includes waiting for a session that was already starting.

Errors

400Bad RequestThe body fails validation. Examples: user is not a positive integer JSON number, password or server is empty, idleHours is outside 0-720, or symbol contains invalid characters. Body: {"statusCode":400,"message":["user must be an integer number", ...],"error":"Bad Request"}.
400LOGIN_REJECTEDThe broker explicitly rejected the login, password or server name. This usually comes back within seconds. Body: {"code":"LOGIN_REJECTED","message":"..."}.
400NOT_AUTHORIZEDThe session started but did not reach the broker's trade server within the wait window, which is about 75 seconds by default. Either the login, password or server name is wrong, or the broker is unreachable right now. Body: {"code":"NOT_AUTHORIZED","message":"..."}.
401UnauthorizedThe Authorization: Bearer header is missing, malformed or does not carry the service session token. An ApiKey is not accepted on this endpoint. Body: {"message":"Unauthorized","error":"Unauthorized","statusCode":401}.
409CREDENTIALS_MISMATCHThe account is already online, or its session is still starting, under a different password than the one sent. The running session is left untouched. To switch passwords, call POST /orchestrator/disconnect first and then connect again. Body: {"code":"CREDENTIALS_MISMATCH","message":"..."}.
500Internal Server ErrorThe account session could not be started. The body is a plain JSON string that describes the failure.
501Not ImplementedSessions cannot be started on this deployment. The body is a plain JSON string.
503Service UnavailableAccount session management is not enabled on this deployment. The body is a plain JSON string.
504CONNECT_TIMEOUTThe account did not come online within the connect time limit, which is about two minutes by default. Check the login, password and server name. Body: {"code":"CONNECT_TIMEOUT","message":"..."}.
Good to know
  • This endpoint requires the service session token in an Authorization: Bearer header. Your ApiKey and portal tokens are rejected here with 401, so use GET /v1/ConnectEx if you authenticate with an ApiKey.
  • Set your HTTP client timeout well above two minutes. A new session can take up to about two minutes to come online after it gets its turn to start. Sessions start one at a time across the service, so time spent waiting for that turn is added on top.
  • Concurrent connect requests for the same login are merged into a single attempt, and every caller gets the same outcome.
  • A 200 with alreadyConnected: true confirms the account is online. The password is compared only when the service has a verified record for that session, so do not use a repeat connect as a password check.
  • If the account is already online and you send a different password, you get 409 CREDENTIALS_MISMATCH and the running session is not touched. Call POST /orchestrator/disconnect, then connect again with the new password.
  • A repeat connect for an account that is already online replaces its idle window. If you leave idleHours out, the account goes back to the service default, so send the same idleHours every time you connect.
  • Send numbers as JSON numbers. Unknown body fields are silently ignored, not rejected, so a typo in an optional field name such as idleHours fails quietly.
  • When idleHours (or the service default) runs out with no API activity on the account, its session is stopped. Calls for that account then fail as not connected until you connect again. The idle check runs about once a minute, so the stop happens shortly after the window elapses, not to the second.
  • If automatic recovery of a dropped account fails five times in a row, the service stops trying and the account must be connected again with this call.
  • The same operation is available with ApiKey authentication as GET /v1/ConnectEx. That variant returns the id as plain text. On failure it returns {code, message} with the HTTP status set by errorReplyStatusCode (default 405), keeping the codes above and using CONNECT_FAILED for any other failure.
curl -X POST https://api.traderail.cloud/orchestrator/connect \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user": 700100200,
"password": "YOUR_PASSWORD",
"server": "PUPrime-Demo"
}'
Response 200application/json
{
"id": "acct-700100200",
"login": 700100200,
"status": "connected",
"alreadyConnected": false
}
Try itAdd your token above to send a live request
Fill in user, password
POST/orchestrator/disconnect

Disconnect an account

Stop an account's session and take it out of automatic recovery.

This is the call that actually takes an account offline. It stops the account's session and removes it from automatic recovery, and the account stays offline until you connect it again. GET /v1/Disconnect is a deliberate no-op and stops nothing, so use this endpoint when you mean it. The call also clears the record of which password last verified for the account, which makes it the way to move an account to a changed broker password: disconnect, then connect with the new one. The response comes back once the stop has been attempted, which can take several seconds.

Bearer tokenchanges state

Parameters

userinteger (JSON number, must be > 0)required
The MT5 account login (account number) to take offline. Send it as a JSON number.

Returns

The stop has been attempted and the account is no longer kept online. id is the account id, login echoes the account number, and status is always "disconnected".

Errors

400Bad Requestuser is missing or is not a positive integer JSON number. Body: {"statusCode":400,"message":[...],"error":"Bad Request"}.
401UnauthorizedThe Authorization: Bearer header is missing, malformed or wrong.
503Service UnavailableAccount session management is not enabled on this deployment. The body is a plain JSON string.
Good to know
  • The call is idempotent. A login with no running session still gets 200 with status "disconnected".
  • A 200 response does not by itself prove the session has ended. To confirm, check that the login no longer appears in running or connected in GET /orchestrator/status.
  • Disconnecting closes no trades. Open positions and pending orders stay on the broker account exactly as they were.
  • No password is needed to disconnect, but connecting the account again with POST /orchestrator/connect does need one.
  • The Bearer token is service-wide, so this call can stop any account on the service. Treat the token accordingly.
curl -X POST https://api.traderail.cloud/orchestrator/disconnect \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user": 700100200
}'
Response 200application/json
{
"id": "acct-700100200",
"login": 700100200,
"status": "disconnected"
}
Try itAdd your token above to send a live request
Fill in user
POST/orchestrator/bulk-order

Place an order on many accounts

Place the same market order on many accounts at once (copy-trade fan-out).

Places one market order (Buy or Sell, same symbol, same volume) on many accounts in a single request, sending it to all of them in parallel so they fill at about the same moment. WARNING: if you leave out logins, send it as null or send an empty array, the order is placed on EVERY account currently connected to the service, not only the accounts you meant. Always send an explicit logins list unless that is really what you want. The request returns 200 even when some or all accounts fail, so check the top-level ok and failed counts and each results[] item. The symbol is matched to each account's broker symbol name, and the name actually used comes back on each item.

Bearer tokenchanges state

Parameters

operationstring (enum, case-sensitive)required
Order direction. Must be exactly "Buy" or "Sell".
One of Buy Sell
symbolstring (non-empty, max 64 characters)required
The base symbol, for example XAUUSD or EURUSD. It is matched per account to that broker's exact symbol name, so XAUUSD may be placed as XAUUSD.s on a broker that adds a suffix. A symbol that cannot be matched is sent as given.
volumenumber (> 0)required
Volume in lots, sent as a JSON number, not a string. The same volume is placed on every target account, with no per-account scaling.
slnumberoptional
Optional stop-loss as an absolute price, applied the same on every account. 0 is treated as not set.
tpnumberoptional
Optional take-profit as an absolute price, applied the same on every account. 0 is treated as not set.
commentstring (max 31 characters)optional
Optional order comment, attached to every order.
loginsarray of integersoptional
The account logins to place the order on, as JSON integers. If you leave it out, send null or send an empty array, the order goes to EVERY account currently connected to the service.
Default ALL currently connected accounts

Returns

A bulk result. requested is the number of target accounts. ok is how many succeeded. failed is requested minus ok. ms is the wall-clock time in milliseconds for the whole fan-out. results has one item per target login, in the same order as the logins. Each item has login, symbol (the broker symbol name actually used) and ok. When the broker replied, the item also has ticket (the order ticket, or the deal ticket if no order ticket came back, else 0) and price (execution price). A failed item adds error: the broker's comment or "retcode <n>" when the broker replied, or a reason such as "account <login> is not connected" when the order was never answered. An item that got no broker reply carries only login, symbol, ok and error.

Errors

400Bad RequestThe body fails validation. Examples: operation is not exactly "Buy" or "Sell", symbol is empty or longer than 64 characters, volume is not a positive JSON number (a string such as "0.01" is rejected), comment is longer than 31 characters, or logins is not an array of integers. Body: {"statusCode":400,"message":[...],"error":"Bad Request"}.
401UnauthorizedThe Authorization: Bearer header is missing, malformed or not the service session token. A customer API key or portal token is not accepted here. Body: {"statusCode":401,"message":"Unauthorized","error":"Unauthorized"}.
503Service UnavailableAccount session management is not enabled on this deployment. The body is the plain JSON string "orchestrator is disabled".
Good to know
  • Leaving out logins, sending logins: null or sending logins: [] places the order on EVERY account currently connected to the service. The connected list in GET /orchestrator/status shows exactly which accounts that would hit.
  • Unknown body fields are silently ignored, not rejected. A typo such as "login" or "accounts" in place of "logins" is dropped, and the order then goes to every connected account.
  • Failures on individual accounts never change the HTTP status. A 200 can mean every account failed, so always check ok, failed and each item's ok.
  • There is no idempotency key on this endpoint. Retrying a request that timed out on your side places the orders again on every account. Unlike GET /v1/OrderSendTask, which accepts clientOrderId, nothing here de-duplicates.
  • logins is not de-duplicated. A login listed twice gets two orders.
  • Listed logins that are not connected are not started by this call. Their item fails with "account <login> is not connected".
  • An item is ok only when the broker reports the order fully executed and returns a non-zero ticket. An item with ok: false and a non-zero ticket (a partial fill, for example) means the broker did act, so check the account before you retry.
  • Each account has its own time limit for the trade reply (10 seconds by default). A slow account shows up as a failed item and does not fail the whole request, but the order was already sent, so a timed-out item may still have executed. Check the account's positions before you retry it.
  • To match the symbol, the service needs the account's symbol list. If that list has not been fetched in the last ten minutes, it is fetched first, which can make that account's item noticeably slower. This applies the first time and again roughly every ten minutes.
  • This endpoint places market orders only. Pending orders are not supported here.
curl -X POST https://api.traderail.cloud/orchestrator/bulk-order \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": "Buy",
"symbol": "XAUUSD",
"volume": 0.01,
"logins": [
700100200,
700100201,
700100202
]
}'
Response 200application/json
{
"requested": 3,
"ok": 2,
"failed": 1,
"ms": 412,
"results": [
{
"login": 700100200,
"symbol": "XAUUSD.s",
"ok": true,
"ticket": 118820044,
"price": 2401.35
},
{
"login": 700100201,
"symbol": "XAUUSD",
"ok": true,
"ticket": 408815223,
"price": 2401.42
},
{
"login": 700100202,
"symbol": "XAUUSD",
"ok": false,
"error": "account 700100202 is not connected"
}
]
}
Try itAdd your token above to send a live request
Fill in operation, symbol, volume
POST/orchestrator/bulk-close

Close all positions on many accounts

Close every open position on many accounts at once.

Closes every open position, in full, on each target account. Accounts are handled in parallel, and within one account positions are closed one after another. WARNING: if you leave out logins, or send it as an empty array or null, EVERY account currently connected to the service is flattened, not only the accounts you meant. Every position is closed whatever its symbol or however it was opened, not only positions placed through bulk-order. The request returns 200 with per-account outcomes even when some closes fail, so check each results[] item.

Bearer tokenchanges state

Parameters

loginsarray of integersoptional
The account logins whose positions should be closed. If you leave it out, send null, send an empty array or send an empty body {}, positions are closed on EVERY account currently connected to the service.
Default ALL currently connected accounts

Returns

A per-account summary. requested is the number of target accounts. ok is how many accounts had every position closed. failed is requested minus ok. ms is the wall-clock time in milliseconds for the whole call. results has one item per target account (in the order you listed them, when you pass logins): login, ok, closed (how many positions were closed on that account) and error (only when the account's positions could not be read at all, in which case closed is absent).

Errors

400Bad Requestlogins is present but is not an array of integers. Body: {"statusCode":400,"message":[...],"error":"Bad Request"}.
401UnauthorizedThe Authorization: Bearer header is missing, malformed or wrong. Body: {"message":"Unauthorized","error":"Unauthorized","statusCode":401}.
503Service UnavailableAccount session management is not enabled on this deployment. The body is the plain JSON string "orchestrator is disabled".
Good to know
  • Leaving out logins, sending logins: null, sending logins: [] or sending an empty body closes positions on EVERY account currently connected to the service. There is no symbol filter.
  • Unknown or misspelled body fields are silently ignored, not rejected. A body such as {"login": 700100200} or {"accounts": [700100200]} counts as an empty body and closes positions on EVERY connected account, so always send the field as logins.
  • Only open positions are closed, each in full. Pending orders are left in place.
  • An account with no open positions reports ok: true with closed: 0.
  • When some closes on an account fail, its item has ok: false and closed set to the number that did close, with no error text. Read the account's open positions again before retrying, because a close that timed out may still have gone through at the broker.
  • If the broker fills a close only in part, that position is counted as failed and not counted in closed, and the rest of its volume stays open.
  • An item carries error only when the account's positions could not be read at all, for example because the account is not connected.
  • Positions opened while the call is running are not included. Each account's position list is read once, at the start.
  • The call takes longer the more positions an account has, because positions within one account are closed one at a time.
curl -X POST https://api.traderail.cloud/orchestrator/bulk-close \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"logins": [
700100200,
700100201,
700100202
]
}'
Response 200application/json
{
"requested": 3,
"ok": 2,
"failed": 1,
"ms": 1380,
"results": [
{
"login": 700100200,
"ok": true,
"closed": 2
},
{
"login": 700100201,
"ok": true,
"closed": 0
},
{
"login": 700100202,
"ok": false,
"error": "account 700100202 is not connected"
}
]
}
Try itAdd your token above to send a live request
GET/orchestrator/status

Get session status

Snapshot of running, connected and automatically-recovered account sessions on the service.

Returns a snapshot of the service's account sessions. It shows which logins have a running session, which are connected and ready for API calls, which the service is currently keeping online, and any running sessions it is not managing. Use it to confirm that a connect or disconnect took effect, or to see exactly which accounts a bulk-order or bulk-close with no logins would hit (the connected list). counts gives the size of each list. A login can leave the managed list without a disconnect call: when it reaches its idle timeout, or when automatic recovery gives up on it.

Bearer token

Parameters

No parameters.

Returns

enabled: whether account session management is switched on. windows: whether the service host runs Windows. platform: the host operating system identifier (for example "linux"). canLaunch: whether this host can start account sessions. running: one entry per running session, with login, pid (an internal identifier, useful only for support diagnostics) and startedAt (epoch milliseconds). connected: logins that are connected and ready for API calls. desired: logins the service is currently keeping online and will try to restart automatically if they drop. orphans: logins with a running session that the service is not keeping online. counts: the length of each list.

Errors

401UnauthorizedThe Authorization: Bearer header is missing, malformed or wrong.
Good to know
  • The connected list is exactly the set of accounts that bulk-order and bulk-close target when you leave out logins.
  • A login can appear in running but not in connected while its session is still starting, or after it has lost its link.
  • A login drops out of desired, and its session is stopped, once it has had no API activity for its idle timeout (idleHours on POST /orchestrator/connect, or the service default). Call POST /orchestrator/connect again to bring it back.
  • If automatic recovery repeatedly fails to bring a login back, the service stops trying and removes it from desired. Check the account and call POST /orchestrator/connect again.
  • Logins in orphans are not restarted automatically if they drop. Call POST /orchestrator/disconnect to stop one, or POST /orchestrator/connect to bring it under management.
  • startedAt is epoch milliseconds. After a service restart it shows when the service resumed tracking the session, which can be later than the session's real start.
  • This endpoint still returns 200 when session management is switched off. Check enabled rather than relying on an error.
  • The snapshot covers the whole service, not only the accounts you connected.
curl -G https://api.traderail.cloud/orchestrator/status \
-H "Authorization: Bearer YOUR_TOKEN"
Response 200application/json
{
"enabled": true,
"windows": false,
"platform": "linux",
"canLaunch": true,
"running": [
{
"login": 700100200,
"pid": 48213,
"startedAt": 1791363600000
},
{
"login": 700100201,
"pid": 48377,
"startedAt": 1791363655000
}
],
"connected": [700100200, 700100201],
"desired": [700100200, 700100201],
"orphans": [],
"counts": {
"running": 2,
"connected": 2,
"desired": 2,
"orphans": 0
}
}
Try itAdd your token above to send a live request
GET/healthz

Health check

Liveness check for the service, with the number of connected accounts.

A liveness check you can call without authentication, suitable for uptime monitors and load-balancer probes. While the service is up and able to serve accounts, it returns 200 with status "ok" and the number of accounts currently connected. Otherwise it returns 503 with status "unavailable". It reports a count only, never logins.

No auth

Parameters

No parameters.

Returns

status is always "ok" on a 200. accounts is the number of accounts currently connected to the service.

Errors

503unavailableThe service cannot currently serve accounts. The body is exactly {"status":"unavailable"}, with no other fields.
Good to know
  • A 200 says nothing about any one account, which can be offline while /healthz is healthy. To confirm a specific account, use the per-account /v1 checks such as GET /v1/CheckConnect with your ApiKey. GET /orchestrator/status also shows this, but it requires the Bearer session token.
  • The response is JSON, not plain text, on both 200 and 503.
  • /healthz is not under /v1 and needs no ApiKey or Bearer token.
  • accounts can drop to 0 while /healthz still returns 200; a healthy service with no connected accounts is not an error.
curl -G https://api.traderail.cloud/healthz
Response 200application/json
{
"status": "ok",
"accounts": 2
}
Try it

Real-time

Push updates over WebSocket instead of polling.

WS/events

Account event stream

Stream live quotes, deal executions and order changes for one or more accounts over a single WebSocket.

Use one WebSocket (wss://api.traderail.cloud/events) to receive real-time activity for any number of accounts. Once connected you get a welcome frame. Send watch with an account login to receive that account's deal and order events, and send subscribe with symbols to also receive quote events for them. Every event the server pushes includes the accountId it came from, so one connection can carry many accounts. Subscriptions belong to the connection, so after you reconnect you must send your watch and subscribe messages again. Nothing is buffered while you are disconnected, so use the history endpoints to catch up after a reconnect.

Bearer token

Parameters

tokenstringqueryoptional
The same master session token as a query parameter, for clients that cannot set headers on a WebSocket, such as browsers. The server uses it when there is no Authorization header, or when that header does not start with Bearer (case-sensitive) or carries an empty token.
eventstringbodyrequired
Name of the client message. Each client frame is one JSON text message shaped as {"event": "<name>", "data": {...}}. ping needs no data.
One of watch unwatch subscribe unsubscribe ping
data.idinteger (account login; a numeric string is also accepted)bodyoptional
The account login. Required for watch, unwatch, subscribe and unsubscribe. It must be a positive integer. The acct-<login> form is not accepted here.
data.symbolsarray of stringbodyoptional
Used by subscribe and unsubscribe: the broker's exact symbol names, including any suffix. Each entry is trimmed. Empty entries, non-strings and names longer than 32 characters are dropped silently. If you send unsubscribe with no symbols, an empty list, or a list in which every entry is dropped, all quote subscriptions for that account are removed.
data.symbolstringbodyoptional
A single symbol, as an alternative to symbols. It is read only when symbols is not an array.

Returns

After the upgrade, every frame is one JSON text message. Lines starting with > are sent by the client and lines starting with < are sent by the server. Server-to-client frames come in three kinds. welcome arrives once after a successful connect. Replies to your messages are watching, subscribed, unsubscribed, pong and error; they have the shape {event, data} and no accountId. Pushed events are quote, deal and order, with the shape {event, accountId, data}. quote data: {symbol, bid, ask, last, time}. deal data: {ticket, order, position, symbol, type (BUY|SELL|BALANCE|CREDIT|OTHER), entry (IN|OUT|INOUT|OUT_BY|OTHER), reason (CLIENT|MOBILE|WEB|EXPERT|SL|TP|SO|ROLLOVER|VMARGIN|SPLIT|OTHER), volume, price, commission, swap, profit, time, magic, comment}. order data: {ticket, symbol, transaction (add|update|delete), type (BUY|SELL|BUY_LIMIT|SELL_LIMIT|BUY_STOP|SELL_STOP|BUY_STOP_LIMIT|SELL_STOP_LIMIT|OTHER), price, volume}. All time values are Unix epoch seconds.

Errors

1008unauthorizedThe session token is missing, not in Bearer <token> form (when sent as a header), or wrong. The WebSocket upgrade still succeeds, then the server immediately closes the socket with close code 1008 and reason unauthorized. No welcome frame is sent.
Good to know
  • Authentication uses the single master session token, sent as an Authorization: Bearer <token> header or as a ?token=<token> query parameter. Customer portal tokens (tk_...) and ApiKey values are not accepted on this socket.
  • The Bearer prefix is case-sensitive. When the Authorization header carries a non-empty Bearer token, ?token= is ignored, so a wrong header token is rejected even when the query token is correct. A header in any other form, such as lowercase bearer ..., is skipped and the query token is used instead.
  • Browsers cannot set headers on a WebSocket, so browser code has to use ?token=. Treat that URL as a secret.
  • A bad token does not fail the HTTP handshake: the socket opens and is then closed with code 1008. Wait for the welcome frame before you treat the connection as authenticated.
  • The session token is not tied to particular accounts: an authenticated connection can watch any login.
  • Account ids in messages must be the numeric login, as a number or a numeric string. The acct-<login> form that /v1 accepts is rejected here with {"event":"error","data":{"message":"id required"}}, and the connection stays open.
  • Watching an account is not checked: any positive login is accepted, and you receive nothing until that account has an active session.
  • subscribe also starts watching the account. Its subscribed reply lists every symbol this connection is subscribed to on that account, not only the ones you just added.
  • unsubscribe with no symbols, an empty symbols list, or a list in which every entry is invalid clears all quote subscriptions for the account. You keep receiving its deal and order events. unwatch stops both.
  • Symbols must be the broker's exact names, including any suffix (for example XAUUSD.s). Unlike the /v1 endpoints, this socket does not map base names such as XAUUSD to the broker's name. Use /v1/SymbolList to look up exact names.
  • An unknown symbol is still acknowledged in subscribed but never produces quotes.
  • The subscribed reply is sent at once and does not confirm the account accepted the subscription. If the account is not connected yet, quotes start when its session comes up, and subscriptions are restored automatically if the account's session restarts.
  • Quotes are sampled, not streamed tick by tick. Each symbol gets at most about one quote every 50 ms, carrying the latest tick, so some intermediate ticks are never delivered.
  • Right after a subscription change on the account, including one made by another connection, you may get the current quote again even if the price has not moved. While the market is closed, that quote's time can be old.
  • last is often 0 for symbols that do not report a last-trade price, such as most FX pairs.
  • One trade can produce several order events (add, update, delete) as well as a deal event. Do not rely on a fixed order of events.
  • Occasionally a deal event carries only ticket, order, symbol, type, volume and price, without entry, position, profit or reason. Code that reads deal events must handle these missing fields.
  • If the account has an idle timeout (ConnectEx idleHours, or the server default), events and quotes delivered on this socket do not count as API activity. An account that is only streamed can be stopped when the timeout expires. Its events then stop, and this socket gets no message about it.
  • Malformed JSON and unknown event names are ignored silently. The server sends no reply to them.
  • The server sends no heartbeats. On an idle connection, send {"event":"ping"} from time to time; the reply is {"event":"pong","data":{}}.
  • The path must be exactly /events. Other paths, including /events/, are not upgraded.
# WebSocket: use a client such as wscat, then send the watch frame
wscat -c "wss://api.traderail.cloud/events?token=YOUR_TOKEN"
# > {"event":"watch","data":{"id":700100200}}
Response 101application/json
< {"event":"welcome","data":{"ok":true}}
> {"event":"watch","data":{"id":700100200}}
< {"event":"watching","data":{"accounts":[700100200]}}
> {"event":"subscribe","data":{"id":700100200,"symbols":["EURUSD","XAUUSD"]}}
< {"event":"subscribed","data":{"id":700100200,"symbols":["EURUSD","XAUUSD"]}}
< {"event":"quote","accountId":700100200,"data":{"symbol":"EURUSD","bid":1.08452,"ask":1.08461,"last":0,"time":1791374400}}
< {"event":"quote","accountId":700100200,"data":{"symbol":"XAUUSD","bid":2412.35,"ask":2412.62,"last":0,"time":1791374400}}
< {"event":"order","accountId":700100200,"data":{"ticket":96000457,"symbol":"EURUSD","transaction":"add","type":"BUY_LIMIT","price":1.0825,"volume":0.1}}
< {"event":"deal","accountId":700100200,"data":{"ticket":95000123,"order":96000456,"position":94000789,"symbol":"XAUUSD","type":"SELL","entry":"OUT","reason":"TP","volume":0.1,"price":2412.35,"commission":0,"swap":0,"profit":42.5,"time":1791374405,"magic":0,"comment":"[tp 2412.35]"}}
> {"event":"unsubscribe","data":{"id":700100200,"symbols":["XAUUSD"]}}
< {"event":"unsubscribed","data":{"id":700100200,"symbols":["EURUSD"]}}
> {"event":"unwatch","data":{"id":700100200}}
< {"event":"watching","data":{"accounts":[]}}
> {"event":"ping"}
< {"event":"pong","data":{}}
> {"event":"watch","data":{"id":"acct-700100200"}}
< {"event":"error","data":{"message":"id required"}}
Try it — live socketclosed
Add your token above to connect