# 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.

Published 2026-10-08 by fxapis. Canonical: https://fxapis.com/blog/mt5-api-typescript-sdk

## 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.

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](https://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

```bash
npm install fxapis
```

Create an API key in the [console](https://fxapis.com/dashboard/keys) 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

```ts
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](https://docs.fxapis.com/guides/idempotency) 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:

```ts
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](https://docs.fxapis.com/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`:

```ts
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](https://github.com/FXapis/fxapis-examples) has this quickstart, click-to-trade for signal providers and a trade copier, runnable against a demo account — see [the examples post](https://fxapis.com/blog/mt5-api-examples-python-node-curl).
- The [Next.js starter](https://github.com/FXapis/fxapis-nextjs-starter) is a small working app on this SDK: connect an account, watch its positions, place a confirmed trade.
- [SDKs and examples](https://docs.fxapis.com/sdks) in the docs, and the [Node.js guide](https://docs.fxapis.com/guides/nodejs) if you would rather see every request with plain `fetch`.
- [Trading](https://docs.fxapis.com/trading) covers every outcome an order can have.

## Questions

### 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.
