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.
1 · Create a read-only token
- In the dashboard, open Profile → API & webhooks.
- Name the token after what will use it, for example Trade journal sheet.
- 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.
- Choose when it expires: 30 days, 90 days, a year, or never.
- 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.
| Path | Needs | What it returns |
|---|---|---|
/api/ext/v1/me | any | The token's name, scopes, accounts and expiry. |
/desk | desk:read | Whether the copier is connected and copying; each account's role, day P&L and open positions. |
/trades | trades:read | Closed trades, newest first. since, until, account, limit (1–500), and before from the previous page's next. |
/trades/<id> | trades:read | One closed trade. |
/pnl | pnl:read | P&L per session and account. from and to as yyyy-mm-dd, up to 92 days. |
/journal | journal:read | The copier's journal lines. limit, type, account, q (text search). |
/copy-problems | copy:read | Recent copies that failed, were held by a rule or slipped. ticks sets the slippage threshold. |
/alerts | alerts:read | The alerts Meridian sent you, newest first. |
/events | events:read | Your 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.
| Event | Sent when |
|---|---|
trade.closed | A round trip closes — instrument, side, quantity, entry, exit, P&L, fees. |
signal.delivered | A TradingView signal reached the copier — the order it asked for and the copier's answer. |
signal.refused | A signal was refused (a gate, the copier offline, the copier said no). |
order.rejected | Your broker rejected an order. |
copy.failed · copy.held · copy.slipped | A follower's copy was not placed, was held by a rule, or filled worse than its leader. |
guard.tripped | A risk guard or the news guard acted on an account. |
copier.offline · copier.online | Your 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, a429or a5xx, 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
4xxmarks that one event failed and moves on. - An endpoint is paused, and you are emailed, when it answers
401,403,404or410, 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 lastnextcursor 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
httpson 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
400 | A parameter is wrong (a limit out of range, a malformed date). |
401 | The token is missing, mistyped, revoked or expired. |
402 | Your plan does not include the API (Pro does). |
403 | The token lacks the scope named in the message, or may not read that account. |
404 | No such endpoint, or no such trade on your account. |
405 | Anything but GET: the API is read-only. |
429 | Over the limit: 120 calls a minute per token, 600 per account. Wait for Retry-After. |
503 | Live data while the copier PC is off (copier_offline). |
Up to 10 tokens and 3 webhook endpoints per account.