Skip to content
MERIDIAN

Reference · Pro plan

API & event webhooks

Connect your own systems to your desk. A read-only API lets a spreadsheet, journal, dashboard or script read your accounts, positions, closed trades, P&L and copier journal. Event webhooks send a signed message to your server the moment a trade closes, a copy fails, a guard trips or the copier PC drops.

Read-only by designAn API token can never place an order, flatten, pause or change a setting. A leaked token can show your data — it can never move your money.
Signed, retried, replayableEvery webhook carries an HMAC signature. A receiver that is down is retried, and anything it missed can be read back for 7 days.
Yours aloneTokens and webhooks only ever see your own account. Changing your password revokes every token and pauses every webhook.

1 · Create a read-only token

  1. In the dashboard, open Profile → API & webhooks.
  2. Name the token after what will use it, for example Trade journal sheet.
  3. Choose what it may read — desk, trades, P&L, journal, copy problems, alerts, the event feed — and, if you like, limit it to some accounts.
  4. Choose when it expires: 30 days, 90 days, a year, or never.
  5. Confirm with your password (and your two-factor code if it is on). The token is shown once and starts with msk_. Copy it now — Meridian keeps only a fingerprint of it, so it cannot be shown again.

You get an email whenever a token is created. If you did not create it, change your password: that revokes every token at once.

2 · Read your desk

Send the token in the Authorization header — never in the address, where it would end up in logs and browser history (a token in the address is ignored).

curl -H "Authorization: Bearer msk_…" \
  https://www.meridiansignaldesk.com/api/ext/v1/trades?limit=50

Every endpoint answers GET with JSON. Times are UTC unless a field says it is the copier PC's clock.

PathNeedsWhat it returns
/api/ext/v1/meanyThe token's name, scopes, accounts and expiry.
/deskdesk:readWhether the copier is connected and copying; each account's role, day P&L and open positions.
/tradestrades:readClosed trades, newest first. since, until, account, limit (1–500), and before from the previous page's next.
/trades/<id>trades:readOne closed trade.
/pnlpnl:readP&L per session and account. from and to as yyyy-mm-dd, up to 92 days.
/journaljournal:readThe copier's journal lines. limit, type, account, q (text search).
/copy-problemscopy:readRecent copies that failed, were held by a rule or slipped. ticks sets the slippage threshold.
/alertsalerts:readThe alerts Meridian sent you, newest first.
/eventsevents:readYour event feed, oldest first — the same events webhooks send. after (the previous page's next), since, type.

/desk, /journal and /copy-problems read live from NinjaTrader on your PC and answer 503 with "code":"copier_offline" while it is off. Trades, P&L, alerts and events are kept by Meridian and answer either way — a nightly export does not fail because NinjaTrader is closed.

3 · Event webhooks

Add up to three endpoints under Profile → API & webhooks: a name, an https:// address on the public internet, and the events it should receive. Its signing secret (whsec_…) is shown once. Send test delivers a signed ping so you can check your receiver.

EventSent when
trade.closedA round trip closes — instrument, side, quantity, entry, exit, P&L, fees.
signal.deliveredA TradingView signal reached the copier — the order it asked for and the copier's answer.
signal.refusedA signal was refused (a gate, the copier offline, the copier said no).
order.rejectedYour broker rejected an order.
copy.failed · copy.held · copy.slippedA follower's copy was not placed, was held by a rule, or filled worse than its leader.
guard.trippedA risk guard or the news guard acted on an account.
copier.offline · copier.onlineYour copier PC disconnected or came back.

There is no “all events” subscription: Select all ticks today's list, so an event type added later never reaches your server unannounced.

Each event is a POST with a JSON body:

{
  "id": "evt_5f0c…",            // dedupe on this — delivery is at-least-once
  "type": "trade.closed",
  "version": 1,
  "account": "SIM-1",
  "occurredAt": "2026-11-08T14:32:10.123Z",
  "sequence": 1842,             // per endpoint, only rises: a gap means a missed event
  "data": { "instrument": "MNQ 12-26", "side": "Long", "quantity": 2,
            "entry": 21000, "exit": 21010, "pnl": 40, "fees": 1.5, … }
}

with the headers X-Meridian-Event, X-Meridian-Event-Id, X-Meridian-Delivery and X-Meridian-Signature. Answer with any 2xx within 10 seconds.

Verifying a signature

X-Meridian-Signature: t=1762612330,v1=5f0c… is an HMAC-SHA256, in lowercase hex, of the time t, a full stop, and the raw request body, keyed with your endpoint's secret. Check it over the exact bytes you received (not a re-serialised object), refuse a t more than 5 minutes old, and compare in constant time. After you replace a secret, each event carries two v1= values for 24 hours — accept either.

// Node (Express)
const crypto = require('crypto')
app.post('/meridian', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = req.get('X-Meridian-Signature').split(',')
  const t = parts[0].slice(2), sigs = parts.slice(1).map((p) => p.slice(3))
  const want = crypto.createHmac('sha256', process.env.MERIDIAN_SECRET)
    .update(t + '.' + req.body).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
  if (!fresh || !sigs.some((s) => s.length === want.length &&
      crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)))) return res.sendStatus(400)
  const event = JSON.parse(req.body)
  res.sendStatus(200)
})
# Python (Flask)
import hmac, hashlib, time, os
@app.post("/meridian")
def meridian():
    raw = request.get_data()
    parts = request.headers["X-Meridian-Signature"].split(",")
    t, sigs = parts[0][2:], [p[3:] for p in parts[1:]]
    want = hmac.new(os.environ["MERIDIAN_SECRET"].encode(), t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if abs(time.time() - int(t)) > 300 or not any(hmac.compare_digest(s, want) for s in sigs):
        return "", 400
    event = request.get_json()
    return "", 200

Retries and replay

  • One at a time, in order. Each endpoint receives one event at a time, oldest first.
  • A receiver that is down is retried — after a timeout, a 408, a 429 or a 5xx, Meridian waits (30 seconds, 2 minutes, 10 minutes, 30 minutes, then every 2 hours) and carries on from the same event once your server answers. Nothing is skipped while it waits.
  • A refused event is not retried. Any other 4xx marks that one event failed and moves on.
  • An endpoint is paused, and you are emailed, when it answers 401, 403, 404 or 410, when it redirects (redirects are never followed), or when nothing has been delivered for 24 hours. Fix it and press Resume.
  • Nothing is lost for 7 days. Every event is kept in your event feed, GET /api/ext/v1/events, whether or not it was delivered — read from your last next cursor after downtime.

Recent deliveries on each endpoint shows every attempt: the event, its state, how many tries and what your server answered.

Keeping it safe

  • Read-only, always. No token can trade, flatten, pause or change a setting — that is not a permission you can grant, it does not exist. To let software act on your desk, use an AI assistant, where every order waits for your tap.
  • Treat a token like a password. Keep it in a secret store or an environment variable, never in a shared sheet, chat or repository. Revoke it in the dashboard if it leaks; it stops working on the next request.
  • Your password protects them all. Making a token or an endpoint, moving one or turning it back on asks for your password (and two-factor code), and you are emailed each time. Changing your password — or pressing That wasn't me on a sign-in alert — revokes every token and AI-assistant link and pauses every endpoint.
  • Public addresses only. Endpoints must be https on port 443. Private, local and reserved network addresses are refused when you save and again on every delivery.
  • No secrets in events. Events never contain your signal address, passwords, keys or another account's data.

Errors and limits

400A parameter is wrong (a limit out of range, a malformed date).
401The token is missing, mistyped, revoked or expired.
402Your plan does not include the API (Pro does).
403The token lacks the scope named in the message, or may not read that account.
404No such endpoint, or no such trade on your account.
405Anything but GET: the API is read-only.
429Over the limit: 120 calls a minute per token, 600 per account. Wait for Retry-After.
503Live data while the copier PC is off (copier_offline).

Up to 10 tokens and 3 webhook endpoints per account.