Building with fxapis

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.

The fxapis team3 min read

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
  1. Install it
  2. From an MT5 login to a closed trade
  3. Method names, query parameters and pages
  4. When an order does not simply fill
  5. Where to go next

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

cURL
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

TypeScript
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", not 0.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. postAccounts stores the account and encrypts its password; postAccountsByIdWarm answers 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:

TypeScript
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:

TypeScript
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.
FAQ

Questions people also ask

Which Node.js versions does the fxapis SDK support?

Node.js 18 or newer. It is an ESM package with full TypeScript declarations and no runtime dependencies — it uses the built-in fetch.

Do I need MetaTrader 5 installed to use it?

No. The MT5 terminals run in fxapis's cloud. Your code only makes HTTPS requests, so it runs on Linux, macOS, Windows or a serverless function.

Does the SDK retry failed orders for me?

No, deliberately. Whether to resend an order is your decision: the SDK tells you whether it is safe (err.retryable) and you resend with the same idempotency key.

Why are the responses untyped?

Methods return the API's data as-is, so your code names only the fields it uses. The API reference documents every field, and the generated method docs link each call to it.

All articles
> Connect. Execute. Scale.

Trade MT5 from your own code.

Connect accounts by login, password and server, and place orders over HTTPS. We run MetaTrader 5; you install nothing.