Quickstart

From sign-up to a filled demo trade, closed again, in one page
Nothing here touches real money: a test key cannot reach a live account

The curl blocks below run in order against a demo account; test/quickstart.mjs in the site repository extracts them from this page and runs them against the mock gateway on every change, so they cannot drift from what the gateway does
The Node and Python blocks are the same calls in a program
From step 8 on, the same integration with the typed JavaScript client (download v0.7.1): its read and trade examples are run against the same mock by the same test, and its account-state stream

1Sign up

Sign up in the console with an email address and a password
The console opens at once; confirming the address is a nudge, not a door

2Create a test key

In the console, API access → Generate API access token
Give it the read, trade and provision scopes and the test environment
The secret is shown once; keep it as MIRROAPI_KEY

Test keys and demo accounts A key is mk_test_… or mk_live_…
A test key trades and records consent on demo accounts only: on a live account it is refused with 403 TEST_KEY_LIVE_ACCOUNT, and it can only create other test keys (403 TEST_KEY_LIVE_KEY)
Everything else — reads, the audit log, usage — is the same, so the code you write here runs unchanged with a live key
A demo account is a broker demo (practice money) on a hosted MetaTrader terminal; it costs the same as any other account: 6 USD per 30 days on a plan, or by the hour without one

export BASE=https://api.mirroapi.cloud/v1
export MIRROAPI_KEY=mk_test_...   # the secret the console showed once

3Add a demo MetaTrader account

The login, password and server of a broker demo account, MetaTrader 5 or 4 (platform: mt4 works the same)
MirroAPI starts a terminal for it; state goes DEPLOYING → DEPLOYED and connection becomes CONNECTED, usually within a minute
GET /platforms says which platforms can be added this way today (selfOnboard.ok) and why not when they cannot

curl -s -X POST $BASE/accounts \
  -H "Authorization: Bearer $MIRROAPI_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"platform":"mt5","name":"Quickstart demo","login":"51234567","password":"<DEMO_PASSWORD>","server":"ICMarketsSC-Demo","environment":"demo","role":"follow"}'
# → 201 { "id": "<ACCOUNT_ID>", "state": "DEPLOYING", "connection": "DISCONNECTED", ... }
# Poll until the terminal is up and signed in to the broker
curl -s $BASE/accounts/<ACCOUNT_ID> -H "Authorization: Bearer $MIRROAPI_KEY"
# → { "state": "DEPLOYED", "connection": "CONNECTED", ... }

4Send a market order

Trades are asynchronous
POST /trades answers 202 with a Command: the order is on the ledger and handed to the terminal; the fill comes back on the command, not in this answer
The Idempotency-Key is what makes a retry safe: the same key never places twice, and a replay answers the command as it is now with Idempotent-Replayed: true
Volume is a decimal string, never a float

curl -s -X POST $BASE/accounts/<ACCOUNT_ID>/trades \
  -H "Authorization: Bearer $MIRROAPI_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"action":"market","symbol":"EURUSD","side":"buy","volume":"0.01"}'
# → 202 { "id": "<COMMAND_ID>", "action": "market", "state": "claimed", "request": { ... }, "result": {}, "expiresAt": "..." }

A refusal is a 422 with a stable code, the numbers in details, and docs pointing at the error catalogue
Branch on the code, never on the message
Nothing was queued when you get a 422

5Poll the command until it resolves

GET /trades/{commandId} until state leaves queued / claimed / sent
The endings:

stateresult.codeMeaning
filled—result.ticket, result.price, result.filledLots, result.latencyMs (gateway receipt to broker acknowledgement)
rejectedBROKER_REJECTEDThe terminal's final refusal; result.broker is its word
A transient answer (requote, off quotes) is re-driven for you until the command expires
rejectedEXECUTION_REFUSEDMIRRO's own engine refused it before any terminal saw it; result.reason is the word and there is no result.broker
refusedRISK_*, NO_CONSENT, …The gateway's refuse-to-send gate, also answered as the 422 above
expiredEXECUTION_NOT_CLAIMEDNothing claimed it before it expired
Nothing was sent; send again
expiredEXECUTION_UNKNOWNRead, never resend A terminal took the command and no answer came back
The outcome is not known and it is never called rejected: read GET /positions and the deal history before sending anything again
A late answer can still correct the row (result.correctedFrom)
curl -s $BASE/accounts/<ACCOUNT_ID>/trades/<COMMAND_ID> -H "Authorization: Bearer $MIRROAPI_KEY"
# → { "state": "filled", "result": { "ticket": "<TICKET>", "price": "1.16912", "filledLots": "0.01", "latencyMs": 6 } }

6Read the book

GET /positions is the account's open positions as its terminal last reported them
An empty list means the account is flat
A 503 BOOK_UNAVAILABLE is not an empty book: the book could not be read and nothing about it is known from that answer — retry, and never act as if the account were flat

MT4: positionId versus ticket On MetaTrader 4 a partial close ends the ticket and gives the remainder a new one
ticket is what a close must name (the live order ticket now); positionId is the stable identity of the position (the origin ticket on MT4, the position id on MT5)
Key your own records on positionId; name ticket when you act
A close-partial that names a superseded ticket is refused 422 TICKET_SUPERSEDED with the live ticket in details.liveTicket, so a retry never cuts the remainder twice

curl -s $BASE/accounts/<ACCOUNT_ID>/positions -H "Authorization: Bearer $MIRROAPI_KEY"
# → [ { "ticket": "<TICKET>", "positionId": "<TICKET>", "symbol": "EURUSD", "side": "buy", "volume": "0.01", "openPrice": "1.16912", "timeBasis": "utc", ... } ]

7Close it

A close names the ticket
Exits are never refused by risk rules
Poll the command the same way; then the book is empty again

curl -s -X POST $BASE/accounts/<ACCOUNT_ID>/trades \
  -H "Authorization: Bearer $MIRROAPI_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"action":"close","ticket":"<TICKET>"}'
curl -s $BASE/accounts/<ACCOUNT_ID>/trades/<CLOSE_ID> -H "Authorization: Bearer $MIRROAPI_KEY"
# → { "state": "closed", "result": { "ticket": "<TICKET>", "filledLots": "0.01", "price": "...", "latencyMs": 5 } }
curl -s $BASE/accounts/<ACCOUNT_ID>/positions -H "Authorization: Bearer $MIRROAPI_KEY"
# → []

8Use the typed JavaScript client

The same calls, with a clear recovery path: read your account, submit one identifiable operation, and follow its actual outcome
The same REST trade actions serve MT4 and MT5
Download client v0.7.1
Node.js 20 or later
No runtime dependencies
Install this versioned client directly from the download URL below

npm install https://mirroapi.com/downloads/mirroapi-client-0.7.1.tgz

Connect an account in the console (or use the demo account from step 3)
Create an API key limited to that account and the scopes your application needs (API keys)
Keep the key in your server’s secret store; never embed it in a public browser bundle
The client examples read MIRRO_API_KEY and MIRRO_ACCOUNT_ID: the same key as MIRROAPI_KEY above, and the account's id

export MIRRO_API_KEY='your-key-from-the-console'
export MIRRO_ACCOUNT_ID='your-account-UUID'

curl --fail-with-body \
  -H "Authorization: Bearer $MIRRO_API_KEY" \
  https://api.mirroapi.cloud/v1/capabilities

The capabilities response describes the running release, including unavailable features and this key’s limits
Account permissions, broker connectivity and EA compatibility still apply

import {MirroClient} from '@mirroapi/client';

const api = new MirroClient({apiKey: process.env.MIRRO_API_KEY});
try {
  const accountId = process.env.MIRRO_ACCOUNT_ID;
  const account = await api.account(accountId);
  const symbols = await api.symbols(accountId);
  const positions = await api.positions(accountId);
  console.log({account, symbols, positions});
} finally {
  api.close();
}

Preserve the exact symbol returned by the account, such as BTCUSDm
Volume and price inputs are decimal strings
A read failure is an error, never a fabricated empty account

Try the complete demo flow

The runnable demo script includes durable local receipts and restart recovery
Use a dedicated, empty, broker-confirmed demo account with copying disabled and an account-limited read/trade key
Node.js 20+ on Linux or macOS is required for its durable filesystem writes
It refuses live accounts

curl --fail --proto '=https' --tlsv1.2 \
  https://mirroapi.com/downloads/mirro-demo-1.mjs -o mirro-demo.mjs
node mirro-demo.mjs inspect
# Use the exact symbol returned above; this example is broker-specific.
node mirro-demo.mjs inspect --symbol BTCUSDm
node mirro-demo.mjs open --symbol BTCUSDm --volume 0.01
node mirro-demo.mjs resume
node mirro-demo.mjs close

Install client v0.7.1 first, as shown above
The script checks the account’s symbol, volume rules and fresh quote
Each receipt is saved before sending
resume recovers the same operation after a lost reply; once the command ID is known it only queries that command
Concurrent starts in the same directory share one identity
After preparing a close, resume follows that close

Keep mirro-demo-receipts private and intact, with the original key and gateway
A known command can still be queried later; an unknown operation older than 23 hours requires manual reconciliation
Never delete receipts to retry an uncertain order
A new receipt directory means a deliberate new trading cycle
This small tutorial caps volume at 0.01 lots; it is not a portfolio manager or a promise of broker acceptance

9Give each trade a durable identity

Save the prepared operation in your application’s durable storage before submitting
In this example, saveOperation is your own storage function, and api and accountId are the client and the account from step 8
Review the account, symbol and amount before enabling execution

const operation = api.prepareTrade(accountId, {
  action: 'market', symbol: 'BTCUSDm', side: 'buy', volume: '0.01'
});
await saveOperation(operation);
const accepted = await api.submitTrade(operation);
const result = await api.waitForCommand(accountId, accepted.id);
console.log(result.state, result.result);

A 202 response means accepted
Only the command’s final state confirms a fill, close or rejection
To close a position, prepare a new operation with {action:'close', ticket:'the-ticket'}

After a lost response, reload and submit the same saved operation
Never create a fresh identity merely because a request timed out
Server idempotency retention is 24 hours; beyond that window, reconcile the original command/account before resubmitting
A timeout while waiting carries the command ID so you can resume checking it
EXECUTION_UNKNOWN requires reconciliation; it does not mean nothing happened

10Keep load bounded

  • The client defaults to six active reads and two trade submissions, with 64 queued operations per lane and up to 32 command waiters
  • GETs and durable trade submissions use bounded retries, jitter and Retry-After
    Other writes are not automatically retried
  • CLIENT_BUSY means the client queue is full
    503 API_BUSY means gateway concurrency is full
    Slow the producer and reuse any existing trade identity
  • 429 RATE_LIMITED carries a retry delay
    Read and trade quotas are separate
    X-RateLimit-Reset is Unix time in seconds
  • Cancellation stops client work; it does not undo an accepted broker order

Use onResponse(meta) for request IDs, response status, retry attempt and gateway timing
No credentials or request bodies are included
Gateway timing, MIRRO routing and broker-confirmed completion measure different intervals

11Subscribe to account state

Connect a standard WebSocket client to wss://api.mirroapi.cloud/v1/stream
Supply a bearer header, or wait for auth-required and send the first message below
Never put a key in the URL
Client v0.7.1 manages reconnects, replay, resets and bounded buffering
Use Node.js 22+ for the built-in WebSocket, or pass a compatible constructor on Node.js 20

const stream = api.stream({
  subscriptions: [{accountId, channels: ['positions', 'orders']}]
});
try {
  for await (const event of stream) {
    if (event.type === 'snapshot') {
      await saveSnapshot(event); // your storage: replace the whole channel
    } else if (event.type === 'status') {
      setStreamState(event.state); // your UI or monitoring
    } else {
      markCacheUnknown(event); // a reset/error is not an empty account
    }
  }
} finally {
  stream.close();
}

The three application functions above are yours to implement
The client resumes from delivered snapshots and labels replayed data
synchronized means every subscribed channel has a fresh stored-state snapshot in this recovery cycle; check terminalFresh and brokerConnected separately
A slow consumer receives CLIENT_STREAM_BACKPRESSURE; revoked access terminates recovery
Defaults are 32 queued events, 1 MiB of buffered data and eight failed reconnect attempts
Breaking the loop, aborting the signal or closing the client releases the stream

The gateway allows 30 WebSocket upgrade attempts per minute per IP, with four simultaneous sockets per IP and two per key
Repeated connection failures can exhaust the client’s recovery budget
Catch the stream error, mark state unknown, and delay a new stream instead of reconnecting in a tight loop

For integrations using their own WebSocket client, the wire protocol is:

{"op":"auth","token":"your-api-key"}

// After ready:
{"op":"subscribe","accountId":"your-account-UUID",
 "channels":["account","positions","orders","commands"]}

Snapshots carry sessionId, seq, ch, source, terminalFresh and full data
They reflect stored terminal state, polled by default every second; unchanged snapshots are suppressed
They do not contain every market tick
Snapshot reads consume the key’s read quota

// Reconnect, authenticate, wait for ready, then resume before subscribing:
{"op":"resume","sessionId":"previous-session-UUID","after":1042}

On RESUME_UNAVAILABLE, discard cached state and subscribe again
On REPLAY_GAP, discard cached state and accept fresh snapshots for retained subscriptions
Replay is process-local and bounded to at most five minutes, 256 frames and 1 MiB; restart or host changes can require a reset
Handle STREAM_BUSY by retrying the control message after a short delay
Use bounded reconnect backoff and jitter
A ready frame with resumeRefresh: true guarantees fresh snapshots after replay; sequences above the resumed.seq value are newly read snapshots

Connect TradingView without writing code

Open TradingView signals, choose New connection, select your demo account and review the trade size
The setup page writes your messages for you
First send its harmless test from TradingView; then choose what a trade alert should do
Copy the message into TradingView’s Message box and the URL into Notifications → Webhook URL
A connected account, a received test and a confirmed broker trade are shown separately

Developers can use the same connection through client v0.7.1
The following example uses api and the verified demo accountId from step 8
It creates a connection and previews a message; it sends no trading alert
Keep connection details in your private server storage and show them only to their owner

const connection = await api.createHook({
  name: 'TradingView demo', accountIds: [accountId],
  defaults: {lots: 0.01, sl: 20, tp: 40},
  policy: {scope: 'own', onSame: 'skip', onOpposite: 'refuse'}
});
const setup = await api.hookTemplates(connection.id);
const message = setup.commands.find(item => item.command === 'buy').message;
// Replace this with an exact symbol from this account's catalogue.
const preview = await api.previewHook(connection.id, message.replace('{{ticker}}', 'BTCUSDm'));
console.log({ready: preview.ok, message: preview.message});
// setup.url goes in TradingView. Never log it or the message secret.

A preview uses current prices and positions; it cannot guarantee a future fill
The gateway records and queues a real alert before returning HTTP 202
Follow api.hookAlerts(connection.id) and inspect each command’s broker result
Each intended event needs a unique nonce; retries retain the same value
An unknown outcome needs reconciliation, not a new event
The console separately builds Pine alert() and strategy alert_message expressions
Existing PineConnector messages need their credentials and units reviewed before migration; complete syntax compatibility is not offered

What this release supports

Account management, positions, working orders, observed deal history, exact symbol catalogues, asynchronous trading, copy links, risk settings and signed webhooks are available through documented routes
Compatible MT4/MT5 terminals also provide guarded quote, server-time and margin reads
MT4 margin checks return free margin before/after; MT5 returns an order margin calculation

Historical-order queries, candle or tick history, depth of market, computed statistics, automatic cross-host failover and high reliability are not offered by this release
Stored-state streaming is distinct from a live tick feed
Check the reference and /capabilities before integrating

12Where to next

  • The API reference: every route the gateway serves, as it serves it
    Paths that are planned but not served are hidden by default
  • The error catalogue: every code, its status and what to do
    The docs field on every error links straight to its row
  • PUT /accounts/{id}/risk: max lots per order, open lots, open positions, daily loss and trading hours, enforced before an order leaves
    A rule the gateway cannot enforce yet is refused (RISK_RULE_UNSUPPORTED), not stored
  • POST /webhooks with command.resolved: every ending of every command, signed, retried for about 28 hours
  • The client's own README is inside the download; GET /capabilities says what the running release serves and this key's limits
  • Going live: a live key, a live account, and the trader's recorded consent (POST /accounts/{id}/consent) before the first order

API v1 · Client v0.7.1 · Contract reviewed 19 September 2026
Availability and broker execution depend on the deployed infrastructure and broker; these examples are not an uptime or latency guarantee