A Hosted MetaTrader 5 and MT4 API for Your Trading Bots: No VPS, No Terminal, No EA
Most MetaTrader automation still lives on a rented Windows VPS, because the terminal has to be open for anything to work. The TS MT5 API removes that requirement: a hosted REST and WebSocket API for MetaTrader 5 and MetaTrader 4 accounts, with one base URL and one API key, so your bot can run wherever your code already runs. This is what it is, what you can build with it, the calls you will use first, and where its limits are while it is in beta.
In This Article
1. The Usual Setup, and What It Costs You
If you have automated a MetaTrader account before, the setup is familiar. A Windows VPS runs the terminal around the clock. Your strategy is either an Expert Advisor inside that terminal, or a Python script on the same box using the MetaTrader5 package, which works only on Windows and only while the terminal is open and logged in. Anything else, a bot on your Mac, a cron job on a Linux server, a cloud function, needs a bridge: an EA that talks to your code over a socket or a file, which you also write and maintain.
The cost is not only the monthly VPS bill. You own the uptime: Windows updates, terminal restarts, a terminal that quietly logged out overnight, a bridge that stops answering after a reconnect. For one account and one strategy that is bearable. For two platforms, several accounts, or a small team, it becomes a second job that has nothing to do with the strategy.
2. What the TS MT5 API Is
The TS MT5 API is a hosted REST and WebSocket API for MetaTrader 5 and MetaTrader 4 accounts. You get a base URL and one API key; that is all. The same key serves both platforms: you save each account’s login once under one of your account ids, and from then on your code reads quotes, bars, balance, positions and history, and places and manages orders, over plain HTTPS and JSON. Demo and live accounts both work.
Nothing runs on your side except your own code. There is no VPS to rent, no terminal to keep open, no EA to compile and no Windows box anywhere in the chain. A bot can run on a laptop that sleeps at night, in a cron job on a Linux server, in a cloud function that wakes up on a schedule or a webhook, or on a Raspberry Pi on a shelf. Whatever can make an HTTPS request can trade.
| Terminal on a VPS | TS MT5 API | |
|---|---|---|
| Where your code runs | On the Windows VPS, next to the terminal | Anywhere with HTTPS: laptop, cron job, cloud function, Raspberry Pi |
| Operating system | Windows (the terminal, the MetaTrader5 package) | Any |
| What you keep running | The VPS, the terminal, the EA or bridge | Your own code |
| MT4 and MT5 together | Two terminals, two bridges, two code paths | One key, the same routes |
| Live prices and fills | Terminal callbacks, or polling the bridge | A WebSocket stream, or server-sent events |
| Several accounts | A terminal per account | An account id per login, a session each |
| A disconnect at 3 a.m. | You notice in the morning | The service keeps the broker link and tells your stream when it reconnects; your code reconnects once on 401 no_session |
3. What You Can Build
A signal bot that places and manages orders
The signal can come from anywhere: a TradingView alert hitting your own small web server, a model that runs
every hour, a schedule. The bot opens one session per account and uses POST /orders for market,
limit and stop orders (stop-limit on MT5) with stop-loss, take-profit, a slippage limit and, for pending orders,
an expiry. It moves stops with PATCH /positions/{ticket}, closes the whole position or part
of it with POST /positions/{ticket}/close, and flattens everything in one call with
POST /positions/close. Every order and close takes an Idempotency-Key, so a timeout
or a dropped connection can be retried without placing a second order.
A dashboard or risk monitor
A monitor is read-only and needs no orders at all: GET /account for balance, equity, margin, free
margin and margin level; GET /positions and GET /orders for what is open and working;
GET /history for the deals; POST /quotes for up to 200 prices in one request. Reads
are allowed at 20 per second per account, which is far more than a page refreshing every few seconds needs. On
MT5 you can save the account with its investor password ("role": "investor") so the session cannot
trade even if the key leaks.
Live data over the stream
One WebSocket gives you quotes at the interval you choose, bars as they form, floating P&L, and an event for every position that opens, changes or closes on the account. That is enough to drive a trailing stop, a drawdown cut-off or a live P&L page without polling. If your platform cannot hold a WebSocket, the same stream is available as server-sent events.
One account following another
Read or stream the master’s positions and place the matching orders on a follower with your own code; the two account ids on one key fit a master and a follower. Section 7 lays out the steps. If you would rather not write and run that code, the portal offers the same thing hosted as Trade Sync: terminal-free, for MT4 and MT5 accounts, set up in the dashboard.
4. Three Calls to Get Going
You need a key from the TS MT5 API page (shown once, so copy it),
the base URL and account ids listed there, a demo MT5 login, and Python 3 with the requests
library. The developer guide is the reference for every field
used below.
Save the login, then open a session. Saving is a one-time PUT that stores the
password encrypted; it does not check the login, so a typo shows up at the connect. POST /connect
answers with a session token that you reuse for everything else. A token ends after 15 minutes without a
request or after 12 hours; on 401 no_session, connect once more and carry on.
import os
import uuid
from urllib.parse import quote
import requests
BASE = "https://API_HOST" # your base URL, from the TS MT5 API page
KEY = os.environ["TSMT5_KEY"] # your API key: keep it out of the code
ACCT = "ACCOUNT_ID" # one of the two account ids on your key
# 1. Save the login once. For an MT4 account add "platform": "mt4" (section 6).
r = requests.put(
f"{BASE}/accounts/{ACCT}", headers={"X-API-Key": KEY}, timeout=30,
json={"login": "12345678", "password": os.environ["MT_PASSWORD"], "server": "Broker-Server"},
)
r.raise_for_status() # 201 new, 200 replaced
if r.json().get("server_known") is False:
raise SystemExit("server name not recognised: check it against your MetaTrader login screen")
# 2. Open a session. Allow 60 s: connecting can take tens of seconds.
c = requests.post(
f"{BASE}/connect", headers={"X-API-Key": KEY}, timeout=60,
json={"account_id": ACCT, "magic": 1001, "label": "signal-bot"},
)
c.raise_for_status() # 401, 422, 429 or 503: read the error, never loop
session = c.json()
H = {"Authorization": f"Bearer {session['token']}"}
if not session.get("session_ready", {}).get("trade", True):
raise SystemExit("connected, but orders are not accepted yet: wait and check again")
Place an order and read the book. Brokers name instruments differently (EURUSD,
EURUSDm, EURUSD.), so ask for the broker’s name first and use exactly what comes
back. The Idempotency-Key is what makes a retry safe: the same key with the same body returns the
original result instead of a second order.
# 3. Resolve the broker's symbol name, read a price, place an order, read the book.
res = requests.get(f"{BASE}/symbols/resolve/EURUSD", headers=H, timeout=30)
res.raise_for_status() # a 404 lists the names your broker uses instead
symbol = res.json()["name"]
enc = quote(symbol, safe="") # names can contain # or +, so encode them in URLs
q = requests.get(f"{BASE}/quote/{enc}", headers=H, timeout=30).json() # bid, ask, time, age_seconds
order = {
"symbol": symbol,
"operation": 0, # 0 BUY, 1 SELL, 2-5 limit and stop, 6-7 stop-limit (MT5 only)
"lots": 0.01, # a multiple of lot_step from GET /symbols/{name}
"sl": round(q["ask"] * 0.995, 5),
"tp": round(q["ask"] * 1.005, 5),
"deviation": 30, # the most slippage you accept, in points
}
o = requests.post(
f"{BASE}/orders", json=order, timeout=90,
headers={**H, "Idempotency-Key": str(uuid.uuid4())}, # one key per intended order
)
if o.status_code == 201:
ticket = o.json()["position"]["ticket"]
elif o.status_code == 409 and o.json().get("error") == "not_confirmed":
pass # possibly filled: check positions, never resend as a new order
else:
print("refused:", o.status_code, o.text)
book = requests.get(f"{BASE}/positions", headers=H, timeout=30).json()
if book["book_read"]: # until this is true the list can be incomplete
for p in book["positions"]:
print(p["ticket"], p["symbol"], p["side"], p["lots"], p["profit"])
A 201 for a market order is sent only once the new position shows on the account, so the reply
carries position.ticket, the handle for changing or closing it. Note the book_read
flag on GET /positions: until it is true the list can be incomplete, and an empty list does not
prove you are flat.
409 not_confirmed, any 5xx, a timeout. Repeat the exact request with the same
Idempotency-Key, or read your positions, before you send anything new. The
first-order tutorial has that rule as a
ready-made function.
5. Streaming Quotes and Position Events
Open a WebSocket to wss://API_HOST/stream and send one JSON message within 10 seconds: the session
token, the symbols you want prices for (up to 50, broker names) and the minimum gap between price updates. You
then receive quote events at that pace, plus position_opened,
position_changed and position_closed for every position on the account, whatever
symbols you asked for. Add "ohlc": ["M1", "M5"] for bars as they form and "profit": true
for floating P&L. The example uses the websockets library.
import asyncio
import json
import websockets
async def watch(token, symbols):
async with websockets.connect("wss://API_HOST/stream") as ws:
# The first message must arrive within 10 seconds and carries the token: never put it in the URL.
await ws.send(json.dumps({"token": token, "symbols": symbols, "interval_ms": 500}))
async for raw in ws:
e = json.loads(raw)
if e["type"] == "quote":
print(e["symbol"], e["bid"], e["ask"])
elif e["type"] in ("position_opened", "position_changed", "position_closed"):
print(e["type"], e["ticket"], e.get("symbol"))
elif e["type"] in ("lagged", "reconnected"):
print("re-read GET /positions: events may have been missed")
asyncio.run(watch(session["token"], [symbol]))
The stream reports changes, not state: positions that were already open when you subscribed are not announced.
Read GET /positions first, open the stream, then apply events on top, matched by ticket, and read
the positions again after a lagged or reconnected event. Up to 4 streams can be open
per account, and a quiet stream sends a heartbeat every 15 seconds.
6. MetaTrader 4 Accounts
An MT4 account is added with the same call plus "platform": "mt4". Use the login number as a
string, the trading password (the investor password does not work for MT4 yet) and the server
name exactly as the MT4 app shows it at login, for example Exness-Trial9.
curl -s -X PUT https://API_HOST/accounts/ACCOUNT_ID \
-H 'X-API-Key: YOUR_KEY' -H 'Content-Type: application/json' \
-d '{"platform": "mt4", "login": "12345678", "password": "your-trading-password", "server": "Broker-Demo"}'
The first connect is different from MT5. Every newly added MT4 account goes through a one-time setup on our
side that takes about a minute. Until it is done, POST /connect answers
409 mt4_onboarding_pending with onboarding.state set to "setting_up", and
nothing is sent to your broker. Ask again every 10 seconds; the first 201 means the account is
ready, and every later connect is immediate. If the setup cannot finish, the same 409 carries
onboarding.state: "failed" and a reason such as login_refused or
unknown_server, with the fix spelled out in detail.
import time
def connect_mt4():
"""The first connect waits for a one-time setup of about a minute; later connects are immediate."""
for _ in range(18): # up to about three minutes
c = requests.post(
f"{BASE}/connect", headers={"X-API-Key": KEY}, timeout=60,
json={"account_id": ACCT, "magic": 1001},
)
if c.status_code == 201:
return c.json()
if c.status_code == 409 and c.json().get("error") == "mt4_onboarding_pending":
if c.json().get("onboarding", {}).get("state") == "failed":
raise SystemExit(c.json().get("detail")) # fix the saved details, re-save, connect again
time.sleep(10) # "setting_up": ask again every 10 s, not faster
continue
c.raise_for_status()
raise SystemExit("still setting up: try again in a minute")
session = connect_mt4()
Once connected, three things to remember on MT4:
- Every position is its own ticket. MT4 accounts are always hedging: an opposite-side order opens a second position and never flattens the first. Close with the close endpoints, and when you close part of a position the part you keep gets a new ticket, so read
GET /positionsafter a partial close. - Order types 0 to 5: market, limit and stop. Stop-limit orders (6 and 7) answer
501 unsupported, and so does a symbol your broker trades by “Request execution”. - Everything else is the same. Quotes, bars, positions, working orders, history and the stream work as on MT5. Symbols on MT4 brokers often carry a suffix (
EURUSDm,XAUUSD+), so resolve names first. There is no tick history on MT4: useGET /barswithM1.
7. One Account Following Another
With the pieces above you can make a follower account take the master’s positions, in your own code, under
your own sizing rules. Save the master under one of your two account ids and the follower under the other, open
a session on each and keep two tokens. On MT5, save the master with its investor password and
"role": "investor" so that session can only read; MT4 masters need the trading password for now.
- Baseline. Read the master’s
GET /positionsuntilbook_readis true and remember the tickets. Do the same on the follower. - Watch. Open the master’s stream with an empty
symbolslist (trade events only) and act onposition_opened,position_changedandposition_closed. If you prefer polling, read the master’s positions every few seconds and compare by ticket. - Act on the follower. For an opened position,
POST /orderson the follower with anIdempotency-Keyderived from the master ticket (for examplemirror-555) and a size from your own rule, checked against the follower’sGET /symbols/{name}. For a changed stop,PATCH /positions/{ticket}. For a close,POST /positions/{ticket}/close. Keep a map from master ticket to follower ticket, and store it, because it must survive a restart. - Recover. After
lagged,reconnectedor a restart, re-read both books and reconcile by your map before acting. On409 not_confirmedfrom the follower, read its positions before doing anything else.
What you own in that design: symbol names that differ between the two brokers (resolve on the follower), lot
rules and rounding, the delay between the master’s fill and yours, and the state file. One more detail from
the guide: if an Expert Advisor trades the master under its own magic number, connect your master session with
that same magic, or those positions are not shown to you.
8. The Beta: Free While in Testing
The TS MT5 API is in beta, and the beta is free for testing:
- Every logged-in user gets one free 7-day trial with up to 300 new orders, on demo and live accounts, MT5 and MT4 alike.
- Closing is never capped. Closes, cancels and stop changes do not count toward the 300, and they have their own, larger rate budget, so a busy order loop can never stop you reducing risk.
- A 24-hour wind-down after expiry. When the trial ends, opening stops, but for the next 24 hours you can still connect, read, close, cancel and move stops. Nothing is closed for you, so flatten what you do not want to keep before the wind-down ends.
- Your code can watch its own allowance. Every response carries
X-Trial-Orders-LeftandX-Trial-Seconds-Left. - Keys are issued on the TS MT5 API page once you are logged in, together with your base URL and account ids. The developer guide is the reference for every route, field and error code.
Paid plans are not listed while the product is in beta.
9. Limits, Stated Plainly
- It is a beta. The routes in the guide are the contract; fields the guide does not describe are diagnostic and may change. Keep client timeouts generous: 60 seconds for a connect, 90 for an order or a close. Calls on one account run one at a time, so an order can wait behind another call on the same account.
- MT4 needs the trading password. The investor password does not work for MT4 yet, so a read-only MT4 monitor is not possible today. MT4 also has no stop-limit orders and no tick history, and a broker that needs a newer MetaTrader 4 than we support answers
unsupported_server_versionat setup. - A broker we have not seen may need a one-time server addition. Saving answers
"server_known": falseand connecting answers422 unknown_server(on MT4, the409carriesreason: "unknown_server"and close matches when we have them). Check the name against your MetaTrader login screen; if it is right, contact us with the broker and the exact server name. - One session per strategy process.
POST /connectshares a small budget (one every 5 seconds per key, with a burst of 5), and a failing login retried in a loop can get the account locked by your broker. Connect once, reuse the token, reconnect once on401 no_session. magicdoes not separate strategies. It labels your session and is not sent to your broker. Every session on the same account sees, and can change or close, the same positions. To keep two strategies apart, give each its own account.- History is for reconciliation, not research. Bars go back roughly a year of M1; deals about 30 days and closed positions about 90. It is not a backtesting data source.
- Hosted access only. A base URL, a key and the guide. There is no package to run on your own server.
The short version
- One base URL, one key, MT5 and MT4 accounts, demo and live. Your code runs anywhere with HTTPS.
- Bots, monitors, live streams, and one account following another: all plain REST and one WebSocket.
- MT4: trading password,
"platform": "mt4", a one-minute first connect, a ticket per position. - Beta is free while in testing: 7 days, 300 new orders, closing never capped, 24 hours to flatten.
10. Frequently Asked Questions
Do I need a Windows VPS or a MetaTrader terminal?
No. The TS MT5 API is a hosted service. Your code needs only an HTTPS client and can run on any operating system, in a cron job, in a cloud function or on a Raspberry Pi. You save your MT5 or MT4 login with one call and the service keeps the connection to your broker.
Does one key really cover both MT5 and MT4?
Yes. A key comes with account ids; save an MT5 login under one and an MT4 login under another, with "platform": "mt4". Sessions, orders, positions, history and the stream use the same routes on both. The MT4 differences are listed in the developer guide and summarised in this article.
Is the beta free?
Yes, while it is in testing. Every logged-in user gets one free 7-day trial with up to 300 new orders, on demo and live accounts. Closing is never capped, and after the trial ends there is a 24-hour wind-down in which you can still close, cancel and move stops. Paid plans are not listed while the product is in beta.
Can I make one account follow another?
Yes, in two ways. With your own code: read or stream the master's positions and place the matching orders on the follower, with an Idempotency-Key per master ticket; the two account ids on one key fit a master and a follower. Or hosted, without writing code: Trade Sync in the dashboard, terminal-free, for MT4 and MT5 accounts.
What does an MT4 account need that an MT5 account does not?
The trading password (the investor password does not work for MT4 yet), "platform": "mt4" when you save the login, and about a minute of one-time setup at the first connect: POST /connect answers 409 mt4_onboarding_pending until the account is ready, then 201. Every MT4 position is its own ticket, and the order types are market, limit and stop.
What if my broker's server is not recognised?
Saving the login answers "server_known": false and connecting answers 422 unknown_server (on MT4, the 409 carries reason unknown_server and close matches when we have them). Check the name against the login screen of your MetaTrader app and re-save if it was wrong. If it is right, contact us with the broker and the exact server name: adding a server is a one-time step on our side.
Is my broker password safe?
It is stored encrypted and the API never returns it. Remove it any time with DELETE /accounts/{id}, or with the Delete my saved broker passwords button on the TS MT5 API page. Keep your API key on your server: never put it in a browser, a mobile app or a public repository.
This is a technical overview, not investment advice. Trading involves risk, and orders on a live account are real.
Try the TS MT5 API Free While It Is in Beta
One free 7-day trial per account, up to 300 new orders • MT5 and MT4, demo and live • Hosted, nothing to install
Start the Free TrialRead the developer guide • Need help? Chat with us on WhatsApp