API reference
Trade a hosted MT5 account over REST and Server-Sent Events. JSON in, JSON out. Contract version 1.
Quickstart
- Sign in at the app with Telegram and open the Desk.
- Connect an MT5 account as a trader account: broker server, login, and the trading password (not investor). It must be a hedging account. The password is encrypted on your device.
- In AGENT ACCESS create a key. Choose scopes, accounts and limits. The secret
kp_live_...is shown once; copy it. - Call the API.
export KOPIARD_API_KEY=kp_live_YOUR_KEY
curl -s {{BASE}}/v1/accounts \
-H "Authorization: Bearer $KOPIARD_API_KEY"{ "accounts": [ { "id": "acc_123", "status": "connected", "login": 12345678,
"server": "Broker-Demo", "currency": "USD", "balance": 10000, "equity": 10012.5,
"margin": 220.4, "free_margin": 9792.1, "margin_level": 4543.1, "leverage": 500,
"trade_allowed": true, "halted": false, "updated_at": 1760000000, "last_error": null } ] }Authentication
Send the key as a bearer token: Authorization: Bearer kp_live_.... Keys are stored as hashes and shown once. A revoked or expired key gets 401 unauthorized.
| Scope | Allows |
|---|---|
| read | All GET calls and the SSE stream. |
| trade | Place, modify, close, cancel, close-all, halt. |
Scopes are independent: a trade key does not imply read, and a key without the needed scope gets 403 forbidden_scope. A key is bound to specific account ids, or to all of your trader accounts.
Base URL
Use the API Worker URL above for everything, including the stream. Streams do not go through the web host.
Conventions
- JSON bodies with
Content-Type: application/json. Times are unix seconds. Numbers are JSON numbers. - Tickets (position and order ids) are strings.
- Write calls take
?wait=<ms>, 0 to 10000, default 5000.201(or200for close, modify, cancel) means done.202with{"command_id","status":"pending"}means accepted but not finished (wait=0, or the terminal was slow): poll the command. - Latency: order commands typically complete in under a second; state is fresh to within about two seconds. This is not for high-frequency trading.
- Hedging accounts only. Netting accounts return
409 netting_unsupported.
Accounts
GET/v1/accountsread
Trader accounts the key can see. Response {"accounts":[Account]}.
GET/v1/accounts/:idread
One account snapshot.
| Field | Type | Meaning |
|---|---|---|
| id | string | Account id. |
| status | string | starting, connected, error, queued or suspended. |
| login, server, currency, leverage | As reported by the terminal. | |
| balance, equity, margin, free_margin, margin_level | number | Account currency. margin_level is a percent. |
| trade_allowed | boolean | Broker permits trading. |
| halted | boolean | Kill switch is on. |
| updated_at | integer | Age of the state. If the node is offline you get the last state with its time. |
| last_error | string or null | Last connection error. |
| default_comment | string or null | Owner-set default trade comment. Read only. |
Quotes, symbols, history
GET/v1/accounts/:id/quotes?symbols=EURUSD,XAUUSDread
Latest bid and ask. Asking for a symbol adds it to the account's watch list for 10 minutes.
{ "quotes": [ { "symbol": "EURUSD", "bid": 1.0851, "ask": 1.0853, "time": 1760000001 } ],
"updated_at": 1760000001 }GET/v1/accounts/:id/symbols?symbols=read
Specs for watched and active symbols only: symbol, digits, point, volume_min, volume_max, volume_step, contract_size, tick_size, tick_value, trade_mode. Use it to round volumes and prices.
GET/v1/accounts/:id/history?from=&to=&limit=read
Closed trades, cached, at most your tier's history rows. from and to are unix seconds.
{ "trades": [ { "ticket": "987654", "symbol": "EURUSD", "side": "buy", "volume": 0.1,
"open_price": 1.0852, "close_price": 1.0861, "profit": 9, "swap": 0,
"open_time": 1760000100, "close_time": 1760003700, "comment": "Kopiard" } ] }Candles
GET/v1/accounts/:id/candles?symbol=EURUSD&timeframe=H1&count=500read
OHLC bars from the account's own broker feed, so prices and spreads match what that broker would fill. Bars come back oldest first. Nothing is stored on our side: each call reads the hosted terminal (identical calls within a few seconds share one answer).
| Param | Meaning |
|---|---|
| symbol | Required. Must exist on the account's broker, else 404 not_found. |
| timeframe | Required. One of M1 M5 M15 M30 H1 H4 D1 W1 (upper case). |
| count | Bars wanted, default 200. The maximum depends on your tier (table below); above it is 422 tier_limit. |
| to | Optional unix seconds (UTC): the open time of the newest bar you want. Default now. The result ends at the last bar whose open time is at or before to, so to plus count pages back through history. |
{ "symbol": "EURUSD", "timeframe": "H1",
"bars": [ { "time": 1760000400, "open": 1.0851, "high": 1.0862, "low": 1.0847, "close": 1.0859,
"tick_volume": 1204, "spread": 8, "real_volume": 0 } ],
"partial": false, "forming_last": true, "updated_at": 1760003712 }| Field | Meaning |
|---|---|
| time | Bar open time, unix seconds UTC (broker server time already converted). |
| spread | Points. real_volume is 0 when the broker gives none. |
| forming_last | true when the final bar is still forming (its open time plus one timeframe is in the future). Do not treat it as closed. It will change on the next call. |
| partial | true when the terminal could not load all the requested history in time. The bars are valid but fewer than asked; retry shortly to get the rest. |
| updated_at | When the bars were read from the terminal. |
Other outcomes: 502 history_unavailable (history not loaded yet, Retry-After: 2; the SDKs retry it for you), 504 node_timeout after 8 seconds, 409 account_not_connected. Each call counts as one request toward your rate limit. A halted account can still read candles.
Per-request cap by tier
| Turn on JavaScript to see the caps. |
No look-ahead: use closed bars only
If you compute a signal on the forming bar, the signal can flip before the bar closes and a backtest will never match live. Drop the last bar when forming_last is true.
r = k.candles(acct, "EURUSD", "H1", count=250)
bars = r["bars"][:-1] if r["forming_last"] else r["bars"]
closes = [b["close"] for b in bars]
fast = sum(closes[-20:]) / 20
slow = sum(closes[-100:]) / 100
if fast > slow: # decided on CLOSED bars only
k.buy(acct, "EURUSD", 0.01, sl=closes[-1] - 0.0030)const r = await k.candles(acct, { symbol: "EURUSD", timeframe: "H1", count: 250 });
const bars = r.forming_last ? r.bars.slice(0, -1) : r.bars; // closedBars(r)
const closes = bars.map((b) => b.close);
const avg = (xs: number[]) => xs.reduce((a, b) => a + b, 0) / xs.length;
if (avg(closes.slice(-20)) > avg(closes.slice(-100))) // CLOSED bars only
await k.buy(acct, "EURUSD", 0.01, { sl: closes.at(-1)! - 0.0030 });The SDKs include closedBars / closed_bars and candlesToArrays / candles_to_arrays (column arrays for numpy or pandas). In MCP the get_candles tool returns compact [t,o,h,l,c] rows (at most 500) and a warning when the last bar is still forming.
Positions and orders
GET/v1/accounts/:id/positionsread
All open positions, including ones opened by hand in the Desk or terminal. sl and tp are null when unset.
{ "positions": [ { "ticket": "987654", "symbol": "EURUSD", "side": "buy", "volume": 0.1,
"open_price": 1.0852, "price": 1.0858, "sl": 1.08, "tp": 1.09, "profit": 6, "swap": 0,
"open_time": 1760000100, "comment": "Kopiard", "magic": 5674273 } ],
"updated_at": 1760000200 }GET/v1/accounts/:id/ordersread
Pending orders: ticket, symbol, type (limit|stop|stop_limit), side, volume, price, stop_limit_price, sl, tp, expiration, time, comment. expiration null or 0 means good till cancelled.
Place an order
POST/v1/accounts/:id/orders?wait=5000trade
| Field | Required | Notes |
|---|---|---|
| symbol | yes | As named by your broker. |
| side | yes | buy or sell. |
| type | yes | market, limit, stop or stop_limit. |
| volume | yes | Lots, above zero, on the symbol's step. |
| price | pending | Required for limit, stop and stop_limit. |
| stop_limit_price | stop_limit | Limit price after the trigger. |
| sl, tp | no | Absolute prices. |
| deviation_pts | no | Market only, default 20 points. |
| expiration | no | Unix seconds, pending only; omit for GTC. |
| comment | no | 31 characters at most. See comments. |
| client_id | no | 40 characters at most. Strongly recommended. See idempotency. |
curl -s -X POST "{{BASE}}/v1/accounts/acc_123/orders?wait=5000" \
-H "Authorization: Bearer $KOPIARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"symbol":"EURUSD","side":"buy",
"type":"market","volume":0.1,
"sl":1.0800,"tp":1.0900,
"client_id":"my-agent-0001"}'{
"command_id": "cmd_1",
"status": "filled",
"ticket": "987654",
"price": 1.0852,
"volume": 0.1
}Status is filled (market), placed (pending order accepted) or pending. A broker refusal is 422 order_rejected with retcode and broker_message. A breached key limit is 422 key_limit. A halted account is 423 halted.
Close, modify, cancel
POST/v1/accounts/:id/positions/:ticket/closetrade
Body {"volume"?, "client_id"?}. Omit volume for a full close; give a smaller one for a partial close. Returns status: closed or partially_closed with price and volume. Allowed while halted.
PATCH/v1/accounts/:id/positions/:tickettrade
Body {"sl"?, "tp"?, "client_id"?}. A number sets it, null removes it, omitted leaves it. Returns {"command_id","status":"modified"}.
{ "sl": 1.0810, "tp": null }PATCH/v1/accounts/:id/orders/:tickettrade
Modify a pending order: {"price"?, "sl"?, "tp"?, "expiration"?}.
DELETE/v1/accounts/:id/orders/:tickettrade
Cancel a pending order. Returns {"status":"cancelled"}. No body, so send idempotency as an Idempotency-Key header if you need it. Allowed while halted.
POST/v1/accounts/:id/close-alltrade
Body {"symbol"?} limits it to one symbol. Returns {"command_id","status":"closed","closed":4}. Allowed while halted.
Halt and resume
POST/v1/accounts/:id/halttrade
{ "halted": true }423 halted. There is no API call to resume. Only a signed-in person can lift the halt from the Desk. An agent can stop itself and can never restart itself. This is intentional.Commands
GET/v1/commands/:command_idread
Poll a command after a 202. Returns {command_id, account_id, op, status, result, ts}. Status is pending until the terminal answers, then the result status or a failure state such as rejected, error or failed. A 504 node_timeout means the outcome is unknown; the terminal may still have run it. Look at positions, or retry with the same client_id.
SSE stream
GET/v1/accounts/:id/stream?symbols=EURUSDread
Content-Type: text/event-stream. The first event is snapshot (account, positions, orders, quotes). Then account, quote, position ({event: open|update|close, position}), order and command. A comment line : hb arrives every 15 seconds. Each open stream counts toward your tier's streams limit per account; over it you get 422 tier_limit. On reconnect, rely on the new snapshot.
curl -N "{{BASE}}/v1/accounts/acc_123/stream?symbols=EURUSD" \
-H "Authorization: Bearer $KOPIARD_API_KEY"
event: snapshot
data: {"account":{...},"positions":[],"orders":[],"quotes":[]}
event: quote
data: {"symbol":"EURUSD","bid":1.0851,"ask":1.0853,"time":1760000001}
: hbIdempotency
Send client_id (40 characters at most) on writes. It is unique per account. Repeat the same client_id and you get the original result back with 200 and the header Idempotent-Replay: true, or the original 422 or 504 body. No second trade is made. client_id is also read from close, modify and close-all bodies, and an Idempotency-Key header works on every write including cancel. Retry network errors, timeouts, 5xx and 429 with the same id. Do not retry 422.
Key limits
Set when the key is created, in the Desk. Enforced by the API on every command.
| Limit | Effect |
|---|---|
| max_volume | Largest volume of one order. |
| max_open_positions | Cap on open positions. |
| symbols_allow | Only these symbols. |
| max_daily_loss_pct | Daily loss cap in percent. |
| expires_at | Key stops working at this time. |
A breach returns 422 key_limit. Separately, you can set account-level caps (max lot, max positions, allowed symbols) in the Desk; the terminal's EA enforces those independently of the API, so they hold even for a signed-in session. The SDKs never try to get around any limit.
Errors
Shape: {"error":{"code","message","request_id"}}. Quote the request_id when you ask for help.
| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, invalid, revoked or expired key. |
| forbidden_scope | 403 | Key lacks the scope or the account. |
| not_found | 404 | Account, ticket or command unknown. |
| account_not_connected | 409 | Terminal offline; writes refused. Reads return the last state. |
| netting_unsupported | 409 | Account is a netting account. |
| halted | 423 | Kill switch is on. |
| rate_limited | 429 | Slow down; honour Retry-After. |
| validation_error | 422 | Bad request body or query. |
| key_limit | 422 | Blocked by the key's limits. |
| tier_limit | 422 | Plan limit: accounts, keys, streams or candles per request. |
| order_rejected | 422 | Broker refused. Adds retcode and broker_message. |
| history_unavailable | 502 | Candles only: terminal history not loaded yet. Retry after Retry-After seconds. |
| node_timeout | 504 | Terminal did not answer. Outcome unknown; retry with the same client_id. |
| internal | 500 | Server error. |
{ "error": { "code": "order_rejected", "message": "Not enough money",
"request_id": "req_abc", "retcode": 10019, "broker_message": "No money" } }Rate limits and tiers
Requests are counted per trading account over a sliding 60 seconds, across all keys and the Desk. Over the limit: 429 rate_limited with Retry-After in seconds. Limits come from your tier:
| Turn on JavaScript to see the tier table. |
Paid checkout is not switched on yet; contact us on Telegram to change tier.
SDKs
sdk/ folder. The names below are what the published packages will be called.import { Kopiard } from "@kopiard/sdk";
const k = new Kopiard({
apiKey: process.env.KOPIARD_API_KEY!,
baseUrl: "{{BASE}}",
});
const [a] = await k.accounts.list();
await k.buy(a.id, "EURUSD", 0.01,
{ sl: 1.0800, tp: 1.0900 });from kopiard import Kopiard
k = Kopiard(api_key=KEY,
base_url="{{BASE}}")
acct = k.list_accounts()[0]["id"]
k.buy(acct, "EURUSD", 0.01,
sl=1.0800, tp=1.0900)Both generate a client_id for you and retry network errors, 5xx and 429 (honouring Retry-After) with the same id. They never retry a 422. The TypeScript client has no runtime dependencies; Python offers sync and asyncio clients. The machine-readable spec is OpenAPI 3.1 in sdk/openapi.yaml.
MCP server
@kopiard/mcp is a stdio MCP server so Claude and other agents can trade without glue code. Tools: list_accounts, get_account, get_quotes, get_positions, get_orders, get_history, get_candles, place_order, close_position, modify_position, cancel_order, close_all, halt_trading. Each tool description states the risk limits. There is no resume tool.
{
"mcpServers": {
"kopiard": {
"command": "npx",
"args": ["-y", "@kopiard/mcp"],
"env": {
"KOPIARD_API_KEY": "kp_live_YOUR_KEY",
"KOPIARD_BASE_URL": "{{BASE}}"
}
}
}
}Like the SDKs, the package is built but not published yet; run it from the repository's sdk/mcp folder for now. Use a dedicated key with a small max_volume for any agent.
Not built yet
- Webhooks.
- Published npm and PyPI packages.
Nothing else on this page is a promise: every call above exists today.
Trade comments
An order with no
commentgets, in order: the key's comment tag, the account's default comment, then the literalKopiard. An explicit comment always wins. Allowed characters areA-Z a-z 0-9 space _ . - # @ / :; others are stripped from defaults. A comment containing|,;or a line break is rejected with 422. The comment cannot be changed after the trade opens.