Signals and click-to-trade
A complete integration for apps whose members approve each trade: connecting their accounts, instant execution on approval, showing their trades, and closing.
This guide is for products where a signal goes out and each member decides, with a click, whether to take the trade — and you want that click to execute at once, on the member's own MetaTrader 5 account, without keeping every account online all day.
It covers the whole integration, end to end. Every call is a normal API call with your key, made from your backend — never from your app or the browser.
member connects MT5 account ──► POST /v1/accounts (once)
signal published ──► nothing yet
member opens the signal ──► POST /v1/accounts/prepare (their account comes online)
member approves ──► POST /v1/accounts/{id}/orders/market
member views their trades ──► GET /v1/accounts/{id}/positions
member closes ──► POST /v1/accounts/{id}/positions/{ticket}/close
member leaves ──► POST /v1/accounts/{id}/disconnect (or DELETE /v1/accounts/{id})Test with demo accounts
fx_test_ keys are a label for your configuration, not a sandbox: they reach real brokers. Build and test the whole flow with
broker demo accounts before connecting any member's real-money account.
1. Connect a member's account (once)
Your app asks the member for their MT5 account number, their trading password (not the investor password), and their broker server name exactly as MetaTrader shows it — for example VantageMarkets-Live or ICMarkets-Live02. Send them to your backend, and from there:
curl -sS -X POST "https://api.fxapis.com/v1/accounts" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Content-Type: application/json" \
-d '{
"login": "81234567",
"server": "VantageMarkets-Live",
"password": "the-members-trading-password",
"mode": "warm_on_demand",
"label": "member_4821"
}'- Store the returned
idagainst the member. That is how you address their account from now on. - Do not store the password anywhere on your side, and do not log the request body. We encrypt it on arrival and never return it.
"mode": "warm_on_demand"keeps the account offline until it is needed and takes it offline again after a quiet period.labelis yours — put your member id in it so our console and your records line up.- The same login and server can be connected once per workspace; a second attempt returns 409
ACCOUNT_EXISTSwith the existing account's id. A login that was disconnected can be connected again: the same account comes back, with 200.
Check the credentials straight away
A new account has not logged in yet, so a wrong password would only surface on the member's first trade. Bring it online once while the member is still on the connect screen, and tell them the result:
session.post(f"{API}/v1/accounts/{account['id']}/warm") # 202, returns at once
state = wait_until_ready(session, account["id"], timeout=90) # see Accounts → Polling wellUsually ready in about 10 seconds in our measurement. If it ends in a state that needs the member instead, show them what to fix:
| State | Tell the member |
|---|---|
invalid_credentials | The login, password or server is wrong. Check the trading password and the exact server name. |
trading_disabled | The broker has disabled trading for this login. (An investor password instead reaches ready and its orders are rejected.) |
needs_2fa | Their account requires two-factor sign-in. They need to turn it off for this account. |
needs_certificate | Their broker requires a certificate. Contact us. |
To fix credentials, send the corrected password to POST /v1/accounts/{id}/password (add server to correct the server name), then bring the account online again to check it — see Connect accounts. The account goes offline again by itself after 15 minutes; nothing else is needed.
Tell the member which signals their account can follow
Once the account has been online, its broker's symbols are known. GET /v1/accounts/{id}/instruments says, for every instrument, which symbol it will trade there — or that the broker does not offer it. Showing that on the connect screen ("your account can follow gold and the majors, but not BTCUSD") saves a refused order later.
Show each member their own broker's market hours
GET /v1/accounts/{id}/sessions?instrument=XAUUSD says whether the market is open now at that member's broker, when it next opens or closes, and its weekly hours in UTC — read from the broker itself. "Gold is closed at your broker — opens Sunday 22:01 UTC" beats a refused order.
Build your signal editor from the instruments
Create signals by choosing from GET /v1/instruments — XAUUSD, EURUSD, US30… — rather than typing a broker symbol. Store the instrument on the signal; it is what each member's order sends.
2. When a member opens a signal: prepare their account
Bring that member's account online as soon as they open the signal in your app — not when the signal is published to everyone. By the time they have read it and tapped approve, their account is logged in and the order goes straight through. Only members who look at the signal use capacity.
curl -sS -X POST "https://api.fxapis.com/v1/accounts/prepare" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountIds": ["0d7c1f5e-4b6a-4c1e-9f0a-2d9e8c7b6a51"] }'It answers 202 straight away, with a result per account: starting, already_running, needs_operator (the member has something to fix — see the table above), leased_elsewhere (busy with another request — retry in a moment), no_secret (no password stored — ask the member for it again), not_found or failed. Calling it again for an account that is already online is harmless. It accepts up to 200 accounts, if you ever want to prepare a group at once.
3. When the member approves: place the order
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/orders/market" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Idempotency-Key: signal_9931:member_4821" \
-H "Content-Type: application/json" \
-d '{
"instrument": "XAUUSD",
"side": "buy",
"volume": "0.05",
"stopLoss": "2338.50",
"takeProfit": "2371.00",
"clientOrderId": "signal_9931"
}'- Use
signal id + member idas the Idempotency-Key. A member double-tapping, a flaky mobile connection, or your own retry can never open a second position for the same signal — the repeat gets the first answer back. - Volumes and prices are strings (
"0.05", not0.05), and stops are absolute prices, not pips. - The call waits for the broker's answer, usually within a few hundred milliseconds of the account being online. If the member approved before a
warm_on_demandaccount came online, it waits for the login first (about 10 seconds). The exception is an order naming aninstrumenton an account that has never been online: its broker's symbols are not known yet, so it is refused withSYMBOLS_NOT_SYNCEDrather than waited for — which is why bringing the account online at connect time matters. - Name the instrument, not the broker's symbol. Your members' brokers spell the same market differently (
XAUUSD,XAUUSD.a,XAUUSDm,GOLD). SendinstrumentfromGET /v1/instrumentsand each order trades that member's own symbol for it; the response'ssymbolsays which. If a member's broker does not offer it, the order is refused withINSTRUMENT_NOT_OFFEREDand nothing is sent. To trade a specific broker symbol, sendsymbolinstead.
Handling the answer
| Response | What happened | Do this |
|---|---|---|
201 state: "filled" | The trade is open at filledPrice. | Show it to the member. |
422 ORDER_REJECTED | The broker refused it — nothing opened. retcode says why. | Show the reason. If retryable is true (the price moved), the member can try again with a new key. |
409 ACCOUNT_NOT_READY or NO_RUNTIME | The account could not come online in time, or it needs the member to fix something (see its state). Nothing was sent. | Retry in a few seconds with the same key — unless the account needs attention; then ask the member. |
409 SYMBOLS_NOT_SYNCED | The account has never been online, so the instrument cannot be translated yet. Nothing was sent. | Bring it online (/warm) and retry with the same key, or send symbol. |
422 INSTRUMENT_NOT_OFFERED / INSTRUMENT_AMBIGUOUS, 400 UNKNOWN_INSTRUMENT | The instrument could not be translated safely for this member's broker. Nothing was sent. | Show the member; for an ambiguous one, details lists the candidates — send the one meant as symbol. |
503 SEND_FAILED | Nothing reached the broker. | Retry with the same key. |
503 ORDER_UNRESOLVED | We do not know yet if it reached the broker. | Do not resend. Show "confirming…" and poll GET /v1/orders/{id} until it settles. |
409 IDEMPOTENCY_IN_FLIGHT | The same approval is still being processed. | Wait a moment and retry with the same key. |
402 QUOTA_EXCEEDED | On paid plans, orders beyond the allowance are billed per order up to twice the allowance, then new opening orders are refused. On Free and on a trial they are refused at the allowance. Closing is never refused for billing. | Change plan, or wait for next month. |
The full list is in Errors, and the reasoning behind these outcomes is in Trading.
Runnable click-to-trade examples — prepare, approve, place and close — are in fxapis-examples, in Python, Node/TypeScript and curl — the walkthrough shows how to run them.
4. Showing the member their trades
curl -sS "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/positions" -H "Authorization: Bearer $FXAPIS_KEY"Each position has its symbol, side, volume, openPrice, currentPrice, stopLoss, takeProfit, profit, and observedAt — when we last saw it. Positions refresh every 15 seconds or so while the account is online. While it is offline, what you get is the last snapshot, and observedAt says how old it is; a stop loss or take profit that fires at the broker in the meantime shows up the next time the account comes online.
For a live view when the member opens their trades screen, prepare their account; once its status is ready, positions refresh on their own within seconds. To refresh them immediately, call reconcile (it does nothing for an account that is offline):
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/reconcile" -H "Authorization: Bearer $FXAPIS_KEY"
curl -sS "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/positions" -H "Authorization: Bearer $FXAPIS_KEY"For a member's history, use GET /v1/orders?accountId=… (what they asked for) and GET /v1/accounts/{id}/deals (what the broker did, including stop-loss and take-profit closes). Results come newest first, 50 at a time, with a cursor for more. See Trading → History.
5. Closing a trade
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/positions/$TICKET/close" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Idempotency-Key: close_$TICKET" \
-H "Content-Type: application/json" -d '{}'$TICKET is the position's brokerPositionId. Omit volume to close all of it, or send "volume": "0.02" to close part. Always send an Idempotency-Key — a repeated close on a hedging account would open an opposite position. If the account is offline, the close brings it online first. Closing is never refused for billing reasons. To move a stop instead, use POST /v1/accounts/{id}/positions/{ticket}/modify.
6. When a member leaves
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/disconnect" -H "Authorization: Bearer $FXAPIS_KEY"This erases the stored password at once. Their trading history stays available to you. To remove the account and its history for good — a member who asked to be forgotten, or a record made with the wrong server name — delete it:
curl -sS -X DELETE "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID" -H "Authorization: Bearer $FXAPIS_KEY"Deleting cannot be undone, and is refused while the account has an order in flight or open positions (send ?force=true to delete with positions still open at the broker). See Accounts.
Capacity and limits
- Accounts come online from a shared pool of terminals. An account stays online for 15 minutes after its last order, then frees its place. If very many members trade at the same moment and the pool is momentarily full, the order returns 409 with nothing sent — retrying a few seconds later with the same key is safe. If you expect large simultaneous volumes, tell us and we will add capacity ahead of time.
- Requests are rate-limited per workspace and per kind of work. Trading requests (orders, closes, stop changes, cancels) scale with the accounts your plan allows: twice the account allowance a minute with a burst of the allowance, never below 60 a minute with a burst of 10 — for example 500 a minute with a burst of 250 on Production, 2,000 with a burst of 1,000 on Business. If more members than that take a signal in the same minute, the rest get 429 — its
retry-aftersays when, and retrying with the same key is safe. See Errors. - Your plan sets how many accounts you can connect and how many orders a month you can place. See the console's Billing page.
Checklist before going live
- The API key lives only on your backend, in a secret store.
- Member passwords go straight to fxapis and are never stored or logged by you.
- New accounts are checked with
warm+ status while the member is still on the connect screen. - Opening a signal calls
preparefor that member's account. - Every order and close carries an Idempotency-Key derived from the signal and member.
-
ORDER_UNRESOLVEDis never resent;ACCOUNT_NOT_READY,NO_RUNTIMEandSEND_FAILEDare retried with the same key. - Positions are shown with how fresh they are (
observedAt). - The whole flow has been run end to end on demo accounts.
SDKs and examples
Official TypeScript and Python SDKs, runnable examples, MCP client configs, integrations and a Next.js starter — all open source on GitHub.
Guides
Step-by-step guides for the fxapis MetaTrader 5 API: complete programs in Python, Node.js, PHP, C#, Go, Java and Ruby, connecting accounts, multi-account orders, trade copiers, idempotent orders, TradingView alerts, AI agents, prop-firm monitoring and migrating from MetaApi.