Early access. Use demo trading accounts only while we test. Your broker password is encrypted on your device, but the security audit is not finished.
kopiard
Contents

API reference

Trade a hosted MT5 account over REST and Server-Sent Events. JSON in, JSON out. Contract version 1.

Quickstart

  1. Sign in at the app with Telegram and open the Desk.
  2. 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.
  3. In AGENT ACCESS create a key. Choose scopes, accounts and limits. The secret kp_live_... is shown once; copy it.
  4. Call the API.
First call: list accounts
export KOPIARD_API_KEY=kp_live_YOUR_KEY
curl -s {{BASE}}/v1/accounts \
  -H "Authorization: Bearer $KOPIARD_API_KEY"
Response 200
{ "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 } ] }
Start on a demo account. An order placed through the API is a real order on the connected account. Always send a stop loss and set tight key limits.

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.

ScopeAllows
readAll GET calls and the SSE stream.
tradePlace, 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

Base{{BASE}}

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 (or 200 for close, modify, cancel) means done. 202 with {"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.

FieldTypeMeaning
idstringAccount id.
statusstringstarting, connected, error, queued or suspended.
login, server, currency, leverageAs reported by the terminal.
balance, equity, margin, free_margin, margin_levelnumberAccount currency. margin_level is a percent.
trade_allowedbooleanBroker permits trading.
haltedbooleanKill switch is on.
updated_atintegerAge of the state. If the node is offline you get the last state with its time.
last_errorstring or nullLast connection error.
default_commentstring or nullOwner-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.

Response 200
{ "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.

Response 200
{ "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).

ParamMeaning
symbolRequired. Must exist on the account's broker, else 404 not_found.
timeframeRequired. One of M1 M5 M15 M30 H1 H4 D1 W1 (upper case).
countBars wanted, default 200. The maximum depends on your tier (table below); above it is 422 tier_limit.
toOptional 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.
Response 200
{ "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 }
FieldMeaning
timeBar open time, unix seconds UTC (broker server time already converted).
spreadPoints. real_volume is 0 when the broker gives none.
forming_lasttrue 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.
partialtrue 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_atWhen 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

Candles per request 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.

Python · kopiard
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)
TypeScript · @kopiard/sdk
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.

Response 200
{ "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

FieldRequiredNotes
symbolyesAs named by your broker.
sideyesbuy or sell.
typeyesmarket, limit, stop or stop_limit.
volumeyesLots, above zero, on the symbol's step.
pricependingRequired for limit, stop and stop_limit.
stop_limit_pricestop_limitLimit price after the trigger.
sl, tpnoAbsolute prices.
deviation_ptsnoMarket only, default 20 points.
expirationnoUnix seconds, pending only; omit for GTC.
commentno31 characters at most. See comments.
client_idno40 characters at most. Strongly recommended. See idempotency.
Request
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"}'
Response 201
{
  "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"}.

Request body
{ "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

Response 200
{ "halted": true }
Resume is human-only. A halted account rejects every command except close, close-all, cancel and halt, with 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
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}

: hb

Idempotency

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.

Trade comments

An order with no comment gets, in order: the key's comment tag, the account's default comment, then the literal Kopiard. An explicit comment always wins. Allowed characters are A-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.

Key limits

Set when the key is created, in the Desk. Enforced by the API on every command.

LimitEffect
max_volumeLargest volume of one order.
max_open_positionsCap on open positions.
symbols_allowOnly these symbols.
max_daily_loss_pctDaily loss cap in percent.
expires_atKey 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.

CodeHTTPMeaning
unauthorized401Missing, invalid, revoked or expired key.
forbidden_scope403Key lacks the scope or the account.
not_found404Account, ticket or command unknown.
account_not_connected409Terminal offline; writes refused. Reads return the last state.
netting_unsupported409Account is a netting account.
halted423Kill switch is on.
rate_limited429Slow down; honour Retry-After.
validation_error422Bad request body or query.
key_limit422Blocked by the key's limits.
tier_limit422Plan limit: accounts, keys, streams or candles per request.
order_rejected422Broker refused. Adds retcode and broker_message.
history_unavailable502Candles only: terminal history not loaded yet. Retry after Retry-After seconds.
node_timeout504Terminal did not answer. Outcome unknown; retry with the same client_id.
internal500Server error.
Example: order_rejected
{ "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:

Tiers
Turn on JavaScript to see the tier table.

Paid checkout is not switched on yet; contact us on Telegram to change tier.

SDKs

Status. The TypeScript and Python packages are built and tested against a local fake server, but not yet published to npm or PyPI. Until then use the REST API directly, or install from the repository's sdk/ folder. The names below are what the published packages will be called.
TypeScript · @kopiard/sdk
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 });
Python · kopiard
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.

MCP config
{
  "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.