MetaKit
beta
API Reference

Base URL: https://api.metakit.cloud

Webhook Events

MetaKit delivers events to every webhook you register (Settings → Webhooks, or via the API). This page documents every event with its complete request body — 5 account, 7 trade, 4 copier, 1 monitor events.

Request format

Every delivery is an HTTP POST to your webhook URL with a JSON body. The event name is in both the body (event) and the X-MetaKit-Event header. Respond with any 2xx status to acknowledge.

POST /your/webhook/endpoint HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-MetaKit-Event: trade.position.opened
X-MetaKit-Signature: t=1769594460,v1=5f2c...9ab

{ ...event body (see below)... }

Note trade events carry the trade. prefix in the header (trade.position.opened) but not in the body's event field (position.opened). Account and copier events are identical in both.

Retries: failed deliveries (non-2xx, timeout, or connection error) are retried with exponential backoff — up to 5 attempts spaced 0.5s, 1s, 2s, 4s. Fan-out: if you have multiple webhooks, the same event is delivered to each independently. Timeout: each attempt times out after 5s.

Verifying signatures

Every delivery is signed with HMAC-SHA256. Each endpoint has its own secret (whsec_…), shown under Settings → Webhooks. The X-MetaKit-Signature header carries the delivery timestamp t (Unix seconds) and v1, the hex HMAC of "<t>.<raw body>".

Verify against the raw request body, before JSON parsing — re-serializing the parsed object changes the bytes and the signature will not match.

import crypto from 'node:crypto';

app.post('/hooks/metakit',
  express.raw({ type: 'application/json' }),   // raw Buffer, not express.json()
  (req, res) => {
    if (!verify(req.body, req.get('X-MetaKit-Signature'), process.env.METAKIT_WEBHOOK_SECRET)) {
      return res.sendStatus(400);
    }
    res.sendStatus(200);                       // ack fast; process asynchronously
    void handle(JSON.parse(req.body.toString('utf8')));
  });

function verify(rawBody, header, secret, toleranceSec = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=').map((s) => s.trim())),
  );
  const { t, v1 } = parts;
  if (!t || !v1) return false;

  // Reject stale deliveries so a captured payload cannot be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.${rawBody}`).digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(v1, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);  // constant-time
}

Rotation: rotate a secret from Settings → Webhooks or via POST /v1/webhooks/{id}/rotate. It takes effect immediately, so accept both the old and new secret briefly while you redeploy. Retries reuse the original t, so keep the tolerance window at ~5 minutes rather than a few seconds.

Envelopes

Account events wrap the full account object:

{
  "event": "account.connected",
  "timestamp": "2026-07-17T12:00:20.000Z",
  "account": { /* the full account object (see Accounts) */ }
}

Trade events carry the position/order in data, plus account_id:

{
  "event": "position.opened",
  "timestamp": "2026-07-17T12:30:00.000Z",
  "account_id": 3,
  "data": { /* the full position or order */ }
}

Copier events carry the copier id, both account ids, the action, a human-readable message, and an ok flag (false for skips, mismatches, and errors):

{
  "event": "copier.error",
  "timestamp": "2026-07-17T12:30:02.000Z",
  "copier_id": 1,
  "source_account_id": 3,
  "follower_account_id": 5,
  "action": "open",
  "message": "open failed: AutoTrading disabled by client",
  "ok": false
}
Account events
account.created

A new account has been added and is starting to connect.

X-MetaKit-Event: account.created
{
  "event": "account.created",
  "timestamp": "2026-07-17T12:00:00.000Z",
  "account": {
    "id": 3,
    "created_at": "2026-07-17T12:00:00.000Z",
    "updated_at": "2026-07-17T12:00:20.000Z",
    "platform": "mt5",
    "type": "full",
    "account_name": "My Account",
    "account_number": 40317,
    "broker_id": 210,
    "server": "ICMarketsSC-Demo",
    "status": "connected",
    "last_ping": "2026-07-17T12:00:20.000Z",
    "client_name": "John Doe",
    "mode": "demo",
    "leverage": 100,
    "currency": "USD",
    "balance": 100000,
    "credit": 0,
    "equity": 99850,
    "free_margin": 98500,
    "used_margin": 1350,
    "open_trades": 2,
    "pending_orders": 1,
    "daily_profit": -150,
    "weekly_profit": 320,
    "monthly_profit": 1240,
    "total_profit": 8420
  }
}
account.connected

The account logged in successfully and is now connected. The account object carries live balance/equity/profit figures.

X-MetaKit-Event: account.connected
{
  "event": "account.connected",
  "timestamp": "2026-07-17T12:00:20.000Z",
  "account": {
    "id": 3,
    "created_at": "2026-07-17T12:00:00.000Z",
    "updated_at": "2026-07-17T12:00:20.000Z",
    "platform": "mt5",
    "type": "full",
    "account_name": "My Account",
    "account_number": 40317,
    "broker_id": 210,
    "server": "ICMarketsSC-Demo",
    "status": "connected",
    "last_ping": "2026-07-17T12:00:20.000Z",
    "client_name": "John Doe",
    "mode": "demo",
    "leverage": 100,
    "currency": "USD",
    "balance": 100000,
    "credit": 0,
    "equity": 99850,
    "free_margin": 98500,
    "used_margin": 1350,
    "open_trades": 2,
    "pending_orders": 1,
    "daily_profit": -150,
    "weekly_profit": 320,
    "monthly_profit": 1240,
    "total_profit": 8420
  }
}
account.invalid_credentials

Login failed — wrong login, password, or server. status is error.

X-MetaKit-Event: account.invalid_credentials
{
  "event": "account.invalid_credentials",
  "timestamp": "2026-07-17T12:00:15.000Z",
  "account": {
    "id": 3,
    "created_at": "2026-07-17T12:00:00.000Z",
    "updated_at": "2026-07-17T12:00:20.000Z",
    "platform": "mt5",
    "type": "full",
    "account_name": "My Account",
    "account_number": 40317,
    "broker_id": 210,
    "server": "ICMarketsSC-Demo",
    "status": "connected",
    "last_ping": "2026-07-17T12:00:20.000Z",
    "client_name": "John Doe",
    "mode": "demo",
    "leverage": 100,
    "currency": "USD",
    "balance": 100000,
    "credit": 0,
    "equity": 99850,
    "free_margin": 98500,
    "used_margin": 1350,
    "open_trades": 2,
    "pending_orders": 1,
    "daily_profit": -150,
    "weekly_profit": 320,
    "monthly_profit": 1240,
    "total_profit": 8420
  }
}
account.error

The account failed to come online, or the connection to the broker was lost.

X-MetaKit-Event: account.error
{
  "event": "account.error",
  "timestamp": "2026-07-17T12:05:00.000Z",
  "account": {
    "id": 3,
    "created_at": "2026-07-17T12:00:00.000Z",
    "updated_at": "2026-07-17T12:00:20.000Z",
    "platform": "mt5",
    "type": "full",
    "account_name": "My Account",
    "account_number": 40317,
    "broker_id": 210,
    "server": "ICMarketsSC-Demo",
    "status": "connected",
    "last_ping": "2026-07-17T12:00:20.000Z",
    "client_name": "John Doe",
    "mode": "demo",
    "leverage": 100,
    "currency": "USD",
    "balance": 100000,
    "credit": 0,
    "equity": 99850,
    "free_margin": 98500,
    "used_margin": 1350,
    "open_trades": 2,
    "pending_orders": 1,
    "daily_profit": -150,
    "weekly_profit": 320,
    "monthly_profit": 1240,
    "total_profit": 8420
  }
}
account.deleted

The account was removed and disconnected. Fired BEFORE deletion so the full account is still present in the payload.

X-MetaKit-Event: account.deleted
{
  "event": "account.deleted",
  "timestamp": "2026-07-17T13:00:00.000Z",
  "account": {
    "id": 3,
    "created_at": "2026-07-17T12:00:00.000Z",
    "updated_at": "2026-07-17T12:00:20.000Z",
    "platform": "mt5",
    "type": "full",
    "account_name": "My Account",
    "account_number": 40317,
    "broker_id": 210,
    "server": "ICMarketsSC-Demo",
    "status": "connected",
    "last_ping": "2026-07-17T12:00:20.000Z",
    "client_name": "John Doe",
    "mode": "demo",
    "leverage": 100,
    "currency": "USD",
    "balance": 100000,
    "credit": 0,
    "equity": 99850,
    "free_margin": 98500,
    "used_margin": 1350,
    "open_trades": 2,
    "pending_orders": 1,
    "daily_profit": -150,
    "weekly_profit": 320,
    "monthly_profit": 1240,
    "total_profit": 8420
  }
}
Trade events
trade.position.opened

A new position was opened on the account. data is the full position.

X-MetaKit-Event: trade.position.opened
{
  "event": "position.opened",
  "timestamp": "2026-07-17T12:30:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 123456,
    "time": "2026-07-17T12:30:00.000Z",
    "symbol": "EURUSD",
    "type": "buy",
    "volume": 0.5,
    "priceOpen": 1.11920,
    "priceCurrent": 1.11925,
    "sl": 1.11500,
    "tp": 1.12500,
    "swap": 0,
    "profit": 2.5,
    "magic": 0,
    "comment": ""
  }
}
trade.position.modified

SL, TP, or volume changed on an open position. data is the full current position; changes shows only the fields that changed with their prior values.

X-MetaKit-Event: trade.position.modified
{
  "event": "position.modified",
  "timestamp": "2026-07-17T12:31:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 123456,
    "time": "2026-07-17T12:30:00.000Z",
    "symbol": "EURUSD",
    "type": "buy",
    "volume": 0.5,
    "priceOpen": 1.11920,
    "priceCurrent": 1.11960,
    "sl": 1.11800,
    "tp": 1.12500,
    "swap": 0,
    "profit": 20.0,
    "magic": 0,
    "comment": ""
  },
  "changes": {
    "sl": { "from": 1.11500, "to": 1.11800 }
  }
}
trade.position.closed

A position was closed — manually, via the API, by SL/TP, by a stop-out, or by a copier. data is the position as last seen plus the realised outcome from the closing deal(s): profit (excluding commission and swap, given separately), close_price, close_time, deal_ticket, and reason — sl, tp, stopout, manual (any client, including this API), or other.

X-MetaKit-Event: trade.position.closed
{
  "event": "position.closed",
  "timestamp": "2026-07-17T12:45:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 123456,
    "time": "2026-07-17T12:30:00.000Z",
    "symbol": "EURUSD",
    "type": "buy",
    "volume": 0.5,
    "priceOpen": 1.11920,
    "priceCurrent": 1.12170,
    "sl": 1.11800,
    "tp": 1.12500,
    "swap": 0,
    "commission": -3.5,
    "profit": 125.0,
    "close_price": 1.12170,
    "close_time": "2026-07-17T12:44:59.000Z",
    "reason": "tp",
    "deal_ticket": 91004402,
    "magic": 0,
    "comment": ""
  }
}
trade.order.placed

A pending order (buy_limit / sell_limit / buy_stop / sell_stop) was placed. data is the full pending order.

X-MetaKit-Event: trade.order.placed
{
  "event": "order.placed",
  "timestamp": "2026-07-17T12:20:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 987654,
    "time": "2026-07-17T12:20:00.000Z",
    "symbol": "XAUUSD",
    "type": "buy_limit",
    "volumeInitial": 0.1,
    "volumeCurrent": 0.1,
    "priceOpen": 2350.0,
    "sl": 2340.0,
    "tp": 2400.0,
    "magic": 0,
    "comment": ""
  }
}
trade.order.filled

A pending order was triggered and became a position. data is the resulting position.

X-MetaKit-Event: trade.order.filled
{
  "event": "order.filled",
  "timestamp": "2026-07-17T12:22:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 123999,
    "time": "2026-07-17T12:22:00.000Z",
    "symbol": "XAUUSD",
    "type": "buy",
    "volume": 0.1,
    "priceOpen": 2350.0,
    "priceCurrent": 2351.2,
    "sl": 2340.0,
    "tp": 2400.0,
    "swap": 0,
    "profit": 12.0,
    "magic": 0,
    "comment": ""
  }
}
trade.order.cancelled

A pending order was cancelled before it filled. data is the pending order as last seen.

X-MetaKit-Event: trade.order.cancelled
{
  "event": "order.cancelled",
  "timestamp": "2026-07-17T12:25:00.000Z",
  "account_id": 3,
  "data": {
    "ticket": 987654,
    "time": "2026-07-17T12:20:00.000Z",
    "symbol": "XAUUSD",
    "type": "buy_limit",
    "volumeInitial": 0.1,
    "volumeCurrent": 0.1,
    "priceOpen": 2350.0,
    "sl": 2340.0,
    "tp": 2400.0,
    "magic": 0,
    "comment": ""
  }
}
trade.order.expired

A time-limited pending order lapsed without filling (its expiration passed). Sent instead of order.cancelled. data is the pending order as last seen; expiration is a unix timestamp on the broker’s clock.

X-MetaKit-Event: trade.order.expired
{
  "event": "order.expired",
  "timestamp": "2026-07-17T16:00:01.000Z",
  "account_id": 3,
  "data": {
    "ticket": 987654,
    "time": "2026-07-17T12:20:00.000Z",
    "symbol": "XAUUSD",
    "type": "buy_limit",
    "volumeInitial": 0.1,
    "volumeCurrent": 0.1,
    "priceOpen": 2350.0,
    "sl": 2340.0,
    "tp": 2400.0,
    "magic": 0,
    "comment": "",
    "expiration": 1752768000
  }
}
Copier events
copier.trade_copied

A trade was successfully replicated to a follower (open, close, modify, or pending). action says which.

X-MetaKit-Event: copier.trade_copied
{
  "event": "copier.trade_copied",
  "timestamp": "2026-07-17T12:30:01.000Z",
  "copier_id": 1,
  "source_account_id": 3,
  "follower_account_id": 5,
  "action": "open",
  "message": "buy 0.5 EURUSD (src 123456 → 998877)",
  "ok": true
}
copier.trade_skipped

A source trade was intentionally NOT copied — blocked by a filter (symbol allowlist, min lot, max open positions) or the copier’s state.

X-MetaKit-Event: copier.trade_skipped
{
  "event": "copier.trade_skipped",
  "timestamp": "2026-07-17T12:30:01.000Z",
  "copier_id": 1,
  "source_account_id": 3,
  "follower_account_id": 5,
  "action": "skip",
  "message": "open XAUUSD: not in symbol filter",
  "ok": false
}
copier.orphan

The source closed a position that has NO matching follower position (a mismatch — e.g. the follower trade was closed manually or never opened). Nothing was closed; logged for you to reconcile.

X-MetaKit-Event: copier.orphan
{
  "event": "copier.orphan",
  "timestamp": "2026-07-17T12:45:00.000Z",
  "copier_id": 1,
  "source_account_id": 3,
  "follower_account_id": 5,
  "action": "close",
  "message": "close src 123456: no follower match (orphan)",
  "ok": false
}
copier.error

Replication failed — the follower order was rejected (e.g. AutoTrading disabled, insufficient margin), the follower is not running, or an exception occurred. message has the reason.

X-MetaKit-Event: copier.error
{
  "event": "copier.error",
  "timestamp": "2026-07-17T12:30:02.000Z",
  "copier_id": 1,
  "source_account_id": 3,
  "follower_account_id": 5,
  "action": "open",
  "message": "open failed: AutoTrading disabled by client",
  "ok": false
}
Monitor events
monitor.triggered

An equity monitor crossed its threshold. Delivered only to monitors whose channel is webhook — Slack, Discord and Telegram channels receive platform-native formatting instead. NOTE: unlike the events above, this delivery is NOT HMAC-signed; keep the monitor URL itself secret.

X-MetaKit-Event: monitor.triggered
{
  "event": "monitor.triggered",
  "timestamp": "2026-07-29T10:14:55.301Z",
  "monitor": {
    "id": 4,
    "name": "Drawdown guard",
    "metric": "drawdown",
    "comparator": "above",
    "threshold": 10
  },
  "account": {
    "id": 2,
    "name": "My Account",
    "currency": "USD",
    "equity": 10800,
    "balance": 12000
  },
  "value": 12.4,
  "message": "Drawdown reached 12.40% (limit 10.00%)"
}