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.createdA 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.connectedThe 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_credentialsLogin failed — wrong login, password, or server. status is error.
{
"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.errorThe 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.deletedThe 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.openedA new position was opened on the account. data is the full position.
{
"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.modifiedSL, 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.
{
"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.closedA 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.
{
"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.placedA pending order (buy_limit / sell_limit / buy_stop / sell_stop) was placed. data is the full pending order.
{
"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.filledA pending order was triggered and became a position. data is the resulting position.
{
"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.cancelledA pending order was cancelled before it filled. data is the pending order as last seen.
{
"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.expiredA 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.
{
"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_copiedA trade was successfully replicated to a follower (open, close, modify, or pending). action says which.
{
"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_skippedA 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.orphanThe 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.errorReplication 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.
{
"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.triggeredAn 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.
{
"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%)"
}