Trade MetaTrader 5 from Node.js with the fxapis TypeScript SDK
Install the fxapis TypeScript SDK, connect an MT5 account, place an idempotent market order and handle every failure — from Node.js, with no MetaTrader terminal.
In short
- npm install fxapis gives you a typed client for the hosted MT5 API: one method per endpoint, generated from the OpenAPI spec, with no runtime dependencies.
- Four calls take you from an MT5 login to a filled order: connect the account, bring it online, wait for ready, place the order with an idempotency key.
- Every failure is an FxapisError with a stable code. ORDER_UNRESOLVED is never resent; anything retryable is resent with the same idempotency key.
- Keep the API key on your server. The SDK is for backends, never for a browser or a mobile app.
On this page
The official TypeScript SDK for fxapis wraps the hosted MetaTrader 5 REST API in a small client: one method per endpoint, generated from the API's OpenAPI spec, with no runtime dependencies. It runs on Node.js 18 or newer, ships ESM with full type declarations, and is open source at github.com/FXapis/fxapis-typescript.
There is no MetaTrader terminal on your side. The terminals run in fxapis's cloud and your code makes HTTPS requests — so the same program runs on a laptop, a Linux server or a serverless function. This post covers installing it, the calls that take you from an MT5 login to a closed trade, and the error handling that matters when an order is real money.
Install it
npm install fxapis
Create an API key in the console and keep it in an environment variable on your server. The key must never reach a browser or a mobile app: anyone holding it can trade the accounts it can see. While you build, connect a broker demo account — every fxapis key, test keys included, reaches a real broker.
From an MT5 login to a closed trade
import { Fxapis, newIdempotencyKey } from "fxapis";
const fx = new Fxapis(process.env.FXAPIS_API_KEY!);
// 1. Connect the account once. The trading password, not the investor password.
const account = await fx.postAccounts({
login: "26177561",
server: "VantageMarkets-Demo",
password: process.env.MT5_PASSWORD,
mode: "warm_on_demand",
});
// 2. Bring it online, and wait for `ready` (about ten seconds for a typical broker).
await fx.postAccountsByIdWarm(account.id);
for (;;) {
const { state, detail } = await fx.getAccountsByIdStatus(account.id);
if (state === "ready") break;
if (["invalid_credentials", "needs_2fa", "needs_certificate", "trading_disabled"].includes(state)) {
throw new Error(`the account needs attention: ${state} — ${detail}`);
}
await new Promise((r) => setTimeout(r, 2000));
}
// 3. A market order, with a key you keep until the order has an answer.
const order = await fx.postAccountsByIdOrdersMarket(
account.id,
{ symbol: "EURUSD", side: "buy", volume: "0.01" },
{ idempotencyKey: newIdempotencyKey() },
);
console.log(order.state, order.filledPrice);
// 4. Close the position it opened — that position carries the order's own ticket.
await fx.postAccountsByIdReconcile(account.id); // refresh positions now rather than in ~15 s
const positions = await fx.getAccountsByIdPositions(account.id);
const opened = positions.find((p: { brokerPositionId: string }) => p.brokerPositionId === order.brokerOrderId);
if (opened) {
await fx.postAccountsByIdPositionsByPositionIdClose(account.id, opened.brokerPositionId, {}, {
idempotencyKey: newIdempotencyKey(),
});
}
Three details in there are deliberate:
- Volumes and prices are strings —
"0.01", not0.01. A JSON number has already been through floating point, and the API refuses one rather than trade a size you did not ask for. - Connecting does not log in.
postAccountsstores the account and encrypts its password;postAccountsByIdWarmanswers at once while the login happens, and you poll the status. A wrong password shows up there, not on the first trade. - Every order and close carries an idempotency key. If the request times out you resend with the same key, and you get the first answer back instead of a second position. Idempotent orders explains why this matters more than it looks.
Method names, query parameters and pages
Each method is named after its route — getAccounts, postAccountsByIdWarm, getAccountsByIdPositions, postAccountsPrepare for bringing many accounts online at once. Path parameters come first, then the body, then options. Query parameters go in options.query, and a paginated list comes back as an array with its page attached:
const unknown = await fx.getOrders({ query: { state: "unknown", limit: 50 } });
const next = unknown.page?.nextCursor; // pass it back as `cursor` for the next page
Responses are the API's data, untyped on purpose: your code names the fields it reads, and the API reference documents each one.
When an order does not simply fill
Every failure is an FxapisError with a stable code, a human message, the requestId to quote to support, and retryable:
import { FxapisError } from "fxapis";
try {
await fx.postAccountsByIdOrdersMarket(accountId, body, { idempotencyKey: key });
} catch (err) {
if (!(err instanceof FxapisError)) throw err;
if (err.code === "ORDER_UNRESOLVED") {
// It may be live at the broker. Never resend: poll getOrdersById until it settles.
} else if (err.code === "ORDER_REJECTED") {
// The broker refused it and nothing opened. Show err.message. A new attempt needs a NEW key.
} else if (err.retryable) {
// SEND_FAILED, ACCOUNT_NOT_READY, RATE_LIMITED, GATEWAY_ERROR …: nothing was done.
// Wait (err.retryAfter seconds when set) and resend with the SAME key.
}
}
The rule that protects you is the first branch: ORDER_UNRESOLVED means nobody knows yet whether the order reached the broker, and retryable is false for it whatever else is true. fxapis confirms an opening order with the broker's own deal history and moves it out of unknown. A proxy's error page between you and fxapis — a 502 or 504 that is not an fxapis answer — arrives as GATEWAY_ERROR, retryable with the same key, never as a JSON parse error.
The SDK never retries on its own. That decision stays with your code, which knows what the order was for.
Where to go next
- fxapis-examples has this quickstart, click-to-trade for signal providers and a trade copier, runnable against a demo account — see the examples post.
- The Next.js starter is a small working app on this SDK: connect an account, watch its positions, place a confirmed trade.
- SDKs and examples in the docs, and the Node.js guide if you would rather see every request with plain
fetch. - Trading covers every outcome an order can have.