Open Portal

REST API

Read your TradeSgnl signals, deliveries, accounts and performance, or send a signal, from any tool that can make an HTTP request.

Introduction

Every tool the MCP server offers is also a plain JSON endpoint under https://portal.tradesgnl.com/api/v1/. Use it from a spreadsheet, a WordPress table plugin, a cron job or your own code.

The REST API is part of TradeSgnl Cloud, included on the Advanced and Professional plans.

The full list of endpoints, parameters and response shapes is published as an OpenAPI document at https://portal.tradesgnl.com/openapi/v1.json. Postman, Insomnia or any OpenAPI client can be pointed at that URL directly. This page is the shorter path: what the data looks like, how to send a signal, and a few ready-made recipes.

Authentication

Create a key in the portal under API & MCP Server > API Keys (it begins with tsk_ and is shown once), then send it as a Bearer token on every request:

curl "https://portal.tradesgnl.com/api/v1/signals?limit=20" \
  -H "Authorization: Bearer tsk_YOUR_KEY"

A key is read-only unless you enabled trading when you created it. You can also cap the lot size and restrict the symbols a key may trade; those limits are enforced before anything reaches your accounts, and you can change or revoke them at any time.

Endpoints

Method and pathWhat it does
GET /api/v1/signalsSignal log, most recent first (symbol, command, limit, offset, date_from, date_to, include_deliveries)
GET /api/v1/deliveriesPer-account delivery status (signal_id, status, limit, offset, date_from, date_to)
GET /api/v1/clientsYour accounts, connected first (status)
GET /api/v1/clients/settingsEA input values of one account (client_id)
GET /api/v1/analyticsBalance and equity per account, as of the EA's last report (client_id)
POST /api/v1/analytics/refreshMake the EA upload a fresh report and wait for it (client_id)
GET /api/v1/live/accountLive balance, equity, margin, open positions, pending orders and trading permissions from a Cloud Terminal (client_id)
GET /api/v1/live/historyClosed trades, realized P&L and unfilled orders over the last hours, from a Cloud Terminal (client_id, hours, limit)
GET /api/v1/live/calcMargin and profit of a hypothetical order, computed by the broker's terminal (client_id, symbol, side, volume, price_open, price_close)
GET /api/v1/live/logsMT5 journal or Experts log lines from a Cloud Terminal (client_id, source, lines, contains)
GET /api/v1/tradesPositions for one account, from its last uploaded report (client_id, limit, offset, date_from, date_to)
GET /api/v1/performancePerformance metrics (client_id)
GET /api/v1/performance/breakdownsBreakdowns by symbol, direction, strategy, day (client_id, breakdown_type, date_from, date_to)
GET /api/v1/backtestsBacktest results (symbol, limit)
GET /api/v1/alertsAlert history (alert_type, limit, offset, date_from, date_to)
GET /api/v1/alerts/preferencesAlert preferences
GET /api/v1/subscriptionYour plan and connection counts
GET /api/v1/copiersYour Trade Copiers and whether each is copying
GET /api/v1/copiers/activityCopied trades of one Trade Copier (copier_id, status, limit, offset, date_from, date_to)
GET /api/v1/licensesYour licences, each with the accounts that run under it
GET /api/v1/presetsSaved signal presets
GET /api/v1/docs/searchSearch this documentation (query)
GET /api/v1/market/symbolsBroker symbol catalog (client_id, query)
GET /api/v1/market/symbolContract specification of one symbol: contract size, tick value, lot step, swaps (client_id, symbol)
GET /api/v1/market/candlesLive candles from 1 minute to monthly, with paging (client_id, symbol, interval, count, before)
GET /api/v1/market/quoteLive bid, ask, last and tick time (client_id, symbol)
GET /api/v1/market/quotesLive quotes for up to 50 symbols in one call (client_id, symbols)
GET /api/v1/market/ticksTick history for one symbol, with paging (client_id, symbol, from, count)
GET /api/v1/analyst/reportRun the Market Analyst (client_id, symbol, style). Spends AI credits
POST /api/v1/signalsSend a trade signal. Needs a key with the trade permission

Query parameters map one to one onto the tool inputs; numbers and booleans are read from the query string. The live/* and market/* endpoints and analyst/report read from the account's Cloud Terminal, so client_id must be an account that has one running; it can be left out when exactly one of your accounts qualifies.

Reading the data

Account figures come from the EA's periodic report, not a live read: analytics, trades and performance carry a reported_at (and trades a report_age_minutes). For numbers right now there are two paths. The live/* endpoints read straight from the account's Cloud Terminal (see Live account data). POST /api/v1/analytics/refresh makes the EA upload a fresh report, waits up to 20 seconds for it to land, and returns the new reported_at; one refresh per account per minute.

List endpoints take limit (up to 50) and offset for paging and date_from / date_to as YYYY-MM-DD, both inclusive, and return the newest rows first. Signals and deliveries are kept for 30 days. Trade history in trades is not pruned. Every response is a JSON object; lists sit under a named key (signals, clients, trades, ...) alongside counts such as returned and offset.

Numbers on a signal (volume, sl, tp) come with a units object that says how to read them, because the same field can hold lots or a percent of balance, pips or a price, depending on how the signal was written. params holds every parameter exactly as it was sent. Error codes on signals, deliveries and alerts come with an error_message that spells out what the code means.

A signal with its per-account deliveries, from GET /api/v1/signals?limit=1&include_deliveries=true:

{
  "signals": [
    {
      "id": "BEIJ9",
      "license_code": "1234567890",
      "symbol": "EURUSD",
      "command": "buy",
      "volume": 0.1,
      "sl": 30,
      "tp": 60,
      "comment": "Strategy1",
      "error": null,
      "error_message": null,
      "timestamp": "2026-09-09T00:40:59.8+00:00",
      "source": "mcp",
      "params": { "vol_lots": 0.1, "sl_pips": 30, "tp_pips": 60, "comment": "Strategy1" },
      "units": { "volume": "lots", "sl": "pips", "tp": "pips" },
      "deliveries": [
        {
          "client_id": "ICM12345678",
          "status": "executed",
          "error": null,
          "error_message": null,
          "created_at": "2026-09-09T00:41:00.2+00:00",
          "ticket": 987654321
        }
      ]
    }
  ],
  "returned": 1,
  "offset": 0
}

A signal produces one delivery per account. The status says what that account did:

  • executed: the account carried the signal out. An entry's row carries its ticket: the position ticket, or the order ticket for a pending order. A close, cancel or modify carries a note such as "Closed 2 positions" or "Nothing to close", and no ticket.
  • failed: nothing was placed, or a close or modify only partly succeeded. In the partial case the note gives how many were done out of how many, and error is the broker's code.
  • rejected: the account refused the signal, and error_message says why. A modify that matches nothing is rejected with code 6019. An entry held by the EA's news filter and then dropped is rejected with code 6023.
  • received: the account got the signal but reported no outcome. An EA older than 0.74 reports nothing more for closes, cancels and modifies, so treat it as delivered, not as proof a trade opened. An entry queued by the EA's news filter also reads received while it waits. A received delivery that carries a ticket did open that order.
  • unknown: the account reported a status the server did not recognise. Treat it as unconfirmed.

ticket is the broker's ticket for the position an entry opened; it is what links a signal to a row in GET /api/v1/trades and to the closing deals in GET /api/v1/live/history, and it is the number to pass as ticket when you close or modify that one trade (see Actions). It is recorded by EA 0.66 or newer on signals from 2026-09-09 onward, and only when the signal opened a position or placed a pending order, so it is null on older deliveries and on closes. Check ea_version in GET /api/v1/clients.

A delivery is the account's own report of what it did. For conclusive proof of a close, read GET /api/v1/live/account (the position is gone) and GET /api/v1/live/history (the closing deal is there).

Your accounts, from GET /api/v1/clients (trimmed):

{
  "clients": [
    {
      "client_id": "ICM12345678",
      "license_code": "1234567890",
      "broker_name": "IC Markets",
      "account_number": "12345678",
      "account_type": "demo",
      "status": "connected",
      "connected_at": "2026-09-01T08:12:44+00:00",
      "disconnected_at": null,
      "last_seen": "2026-09-09T06:58:03+00:00",
      "cloud_terminal": { "status": "active", "is_trial": false, "trial_expires_at": null }
    }
  ],
  "total": 3,
  "connected": 1,
  "disconnected": 2
}

Every account that ever connected is listed; pass status=connected for the live ones only. client_id is the id every other endpoint takes. license_code is the licence whose signals the account takes. Accounts connected from the portal (cTrader, TradeLocker, crypto exchanges) are listed separately under exchange_connections, with their own license_code. cloud_terminal is null for an account running the EA on your own machine or VPS.

Licences and their accounts

GET /api/v1/licenses returns your licences, oldest first, each with every account that has run under it (trimmed):

{
  "licenses": [
    {
      "code": "1234567890",
      "created_at": "2026-05-02T09:14:00+00:00",
      "is_preassigned": false,
      "accounts": [
        {
          "client_id": "ICM12345678",
          "platform": "mt5",
          "broker_name": "IC Markets",
          "account_number": "12345678",
          "account_type": "demo",
          "ea_version": "0.81",
          "online": true,
          "last_seen": "2026-09-09T06:58:03+00:00"
        },
        {
          "connection_id": "0d6f2c1e-4b0a-4f57-9a7e-2f1c9b8d7e11",
          "platform": "ctrader",
          "display_name": "Spotware 5896112",
          "account_type": "real",
          "status": "active",
          "enabled": true
        }
      ]
    }
  ],
  "total": 1,
  "note": "The list shows every account that ever ran under each licence, as last heard. ..."
}

An MT5 terminal carries client_id. A cTrader, TradeLocker or crypto connection carries connection_id, the same id clients lists under exchange_connections, plus its status and whether it is switched on (enabled). online is the last state TradeSgnl heard: it stays true until the terminal reports a disconnect. The list also holds accounts that ran under the licence in the past, so use it to see which licence an account belongs to. To see who gets the next signal right now, send a dry run.

Trade history

GET /api/v1/trades?client_id=ICM12345678 returns the account's last uploaded report, not a live read, with one row per position. Rows are newest first. client_id is required. limit defaults to 20 and goes up to 50, offset pages, and date_from / date_to (YYYY-MM-DD) are compared with the close time. An open position has no close time: date_from keeps it and date_to leaves it out. To get a current report first, call POST /api/v1/analytics/refresh.

The response carries reported_at (when the account built the report, in UTC), report_age_minutes, collection_time, the trades list and a summary:

{
  "reported_at": "2026-09-09T06:55:00+00:00",
  "report_age_minutes": 4,
  "collection_time": "2026-09-09T06:55:00+00:00",
  "trades": [
    {
      "ticket": 987654321,
      "symbol": "EURUSD",
      "type": "Buy",
      "volume": 0.1,
      "price_open": 1.1002,
      "price_close": 1.1062,
      "sl": 1.0972,
      "tp": 1.1062,
      "profit": 60,
      "swap": 0,
      "commission": -0.7,
      "fee": 0,
      "comment": "Strategy1",
      "open_time": "2026.09.09 00:41:00",
      "close_time": "2026.09.09 04:12:37",
      "is_open": false
    }
  ],
  "summary": {
    "total_trades": 214,
    "returned": 1,
    "offset": 0,
    "trades_order": "newest first",
    "closed_trades": 213,
    "balance_operations_excluded": 2,
    "total_profit": 1840.5,
    "win_rate": "61.5%",
    "currency": "USD",
    "balance": 11840.5,
    "initial_balance": 10000
  }
}
  • ticket is the MT5 position id, the same number a delivery's ticket carries.
  • type is Buy or Sell. volume is a closed position's opening size and an open position's current size.
  • price_close is set on closed rows and null on open rows. current_price is set on open rows only, as of reported_at.
  • sl and tp are 0 when none is set.
  • commission is summed over the position's deals on closed rows. fee is always 0 on MT5.
  • open_time and close_time are in the broker's server time, formatted YYYY.MM.DD HH:MM:SS. close_time is null while the position is open.
  • History covers the whole account history as of the report.
  • The rows do not list individual closing deals. Use GET /api/v1/live/history for those, including each partial close.
  • win_rate is a string such as "61.5%", or "N/A" when no trade has closed.

Live account data

These endpoints read the MT5 terminal on the account's Cloud Terminal at the moment of the call.

Account, positions and pending orders

GET /api/v1/live/account returns as_of (when it was read), balance, equity, margin and open P&L, the open positions with their current price, commission and fee, and the pending orders. Each open position carries ticket, volume, stop_loss, take_profit, profit, swap, commission, magic, comment and opened_at; stop_loss and take_profit are null when none is set. The ticket is what you pass to close or modify that one trade. Two groups say what stands between an order and the broker:

  • trading: trade_allowed is false on an investor (read-only) login, algo_trading_enabled is the terminal's Algo Trading button, broker_connected is whether the terminal is connected to the broker, margin_mode is hedging or netting, and ping_ms is the latency to the broker. On a hedging account each entry opens its own position. On a netting account MT5 keeps one position per symbol, with one stop loss and one take profit, so two strategies cannot be managed separately on the same symbol (see Multiple Strategies).
  • stop_out: margin_call_level and stop_out_level, as a margin level percent, or as account money when mode is money.

On every position and order, opened_by_ea (or placed_by_ea) is true when it carries the EA's magic number and false when it does not, so manual trades stand out. It is null until the EA has reported its settings. opened_via says where it was placed: expert, desktop, mobile or web.

Closed trades

GET /api/v1/live/history?hours=24 is read from the terminal at the moment of the call. hours runs from 1 to 168 (default 24) and limit from 1 to 100 (default 20). It returns:

  • as_of and time_basis. Times are UTC when time_basis is utc. server means the broker's clock offset has not been measured yet, and the times are on the broker's clock.
  • summary of realized profit, swap, commission, fee and net over the whole window, not only the rows listed.
  • closed: one row per closing deal, newest first, with deal_ticket, position_id, symbol, direction, volume, price_open, price_close, profit, swap, commission, fee, reason (sl, tp, stop_out, expert, ...), reversal, magic, comment and closed_at. A partial close is its own row.
  • closed_truncated: true when more closes exist than limit.
  • ended_orders: pending orders that were canceled, rejected or expired.

position_id equals the delivery's ticket and the ticket in GET /api/v1/trades, so it joins a signal to its fills. For history older than 168 hours use GET /api/v1/trades.

Margin and profit calculator

GET /api/v1/live/calc?symbol=XAUUSD&side=buy&volume=0.5&price_close=2390 asks the account's terminal what the order needs and makes, using the broker's own rules for leverage, margin currency and hedged margin:

{
  "symbol": "XAUUSD",
  "side": "buy",
  "volume": 0.5,
  "currency": "USD",
  "price_open": 2401.37,
  "price_close": 2390,
  "margin": 1200.69,
  "profit": -568.5,
  "margin_free": 9850
}

price_open defaults to the price the order would fill at now. Pass your stop loss as price_close to get the money at risk. Nothing is sent.

Terminal logs

GET /api/v1/live/logs returns the newest lines of the MT5 journal (source=journal: broker connection, authorization, trade requests and their results, the EA loading) or of the Experts log (source=experts: what the EA printed). contains filters lines and lines goes up to 500. Logs read even while the terminal is disconnected from the broker, which is when they help most.

Market data

The market/* endpoints read from the MT5 terminal the account runs on, so prices, times and contract specifications are your broker's own. The symbol, candle and quote endpoints were first published under analyst/, and those paths still answer.

Quotes

From GET /api/v1/market/quote?symbol=XAUUSD:

{
  "client_id": "ICM12345678",
  "broker_label": "IC Markets 12345678",
  "symbol": "XAUUSD",
  "bid": 2401.12,
  "ask": 2401.37,
  "last": null,
  "spread_points": 25,
  "price": 2401.12,
  "price_basis": "bid",
  "change_percent": 0.42,
  "day_open": 2391.08,
  "tick_time": "2026-09-14T08:15:02.418Z",
  "tick_age_seconds": 0.6,
  "time_basis": "utc",
  "trade_mode": "full"
}

tick_time is when the broker's latest tick arrived and tick_age_seconds how long ago that was, so a closed market shows up as a large age. price is the price the symbol's candles are built from (see price_basis below) and change_percent runs from today's daily open. last is the last trade price, set only for exchange-traded instruments. trade_mode is full when the symbol can be traded; long_only, short_only, close_only and disabled mean the broker restricts new trades.

Candles

interval is one of 1min, 5min, 15min, 30min, 1h, 2h, 4h, 1day, 1week or 1month, and count goes up to 5,000 bars, newest last. For older history, pass the time of the oldest bar you hold as before and repeat; every page holds only bars strictly older than it.

{ "time": "2026-09-14T08:14:00Z", "open": 2400.95, "high": 2401.40, "low": 2400.81, "close": 2401.12, "volume": 212, "real_volume": 0, "spread": 25 }
  • time is the bar's open time.
  • Prices follow the response's price_basis: bid for forex, metals and CFDs, which is how MT5 builds their charts, and last for exchange-traded instruments.
  • volume is tick volume, the number of price updates in the bar. real_volume is traded volume where the broker reports one and 0 otherwise. spread is the bar's spread in points.

Times on quotes and candles are UTC when time_basis is utc, which is the normal case. It reads server only when the terminal could not measure the broker's clock offset yet (a terminal started over a weekend), and the times are then the broker's clock written as UTC.

Contract specification

GET /api/v1/market/symbol?symbol=XAUUSD returns what position sizing needs: contract_size, tick_size and tick_value, volume_min, volume_max and volume_step, stops_level_points, the filling_modes and order_types the broker accepts, swap_long, swap_short and triple_swap_day, and margin figures. tick_value is the money one tick_size move is worth on one lot, in tick_value_currency (the account currency), so a price move is worth move / tick_size * tick_value * lots. The specification is read live, so tick_value follows the exchange rate for symbols quoted in another currency.

Several quotes at once

GET /api/v1/market/quotes?symbols=EURUSD,XAUUSD,US30 returns up to 50 quotes in one call, each with the fields above. A symbol the account does not offer, or that has no price, appears under errors (unknown_symbol, no_data) instead of failing the whole call.

Ticks

GET /api/v1/market/ticks?symbol=XAUUSD returns the newest ticks of the last hour (count, up to 10,000). Each tick has time to the millisecond, bid, ask, last (exchange-traded instruments), volume, and flags naming what changed. For a range, pass from as an ISO UTC time: ticks come oldest first, and when next_from is set, pass it as from to get the next page with nothing skipped or repeated.

Sending a signal

curl -X POST https://portal.tradesgnl.com/api/v1/signals \
  -H "Authorization: Bearer tsk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "license_code": "1234567890",
    "symbol": "EURUSD",
    "action": "buy",
    "volume": { "lots": 0.1 },
    "stop_loss": { "pips": 30 },
    "take_profit": { "pips": 60 },
    "comment": "Strategy1",
    "idempotency_key": "order-2026-09-09-001"
  }'

The response is a receipt: the exact signal syntax that was sent, the accounts it went to (recipients), the accounts under the licence it did not go to (skipped), and the signal_id to check afterwards. Both lists come from the signal server's answer to this very send, not from a lookup afterwards.

{
  "sent": true,
  "syntax": "1234567890,EURUSD,buy,vol_lots=0.1,sl_pips=30,tp_pips=60,comment=Strategy1",
  "license_code": "1234567890",
  "signal_id": "BEIJ9",
  "idempotency_key": "order-2026-09-09-001",
  "recipients": [
    {
      "platform": "mt5",
      "client_id": "ICM12345678",
      "broker_name": "IC Markets",
      "account_number": "12345678",
      "account_type": "demo",
      "ea_version": "0.81"
    },
    {
      "platform": "ctrader",
      "connection_id": "0d6f2c1e-4b0a-4f57-9a7e-2f1c9b8d7e11",
      "display_name": "Spotware 5896112",
      "account_type": "real"
    }
  ],
  "skipped": [],
  "connected_accounts": 2,
  "server_response": { "signalId": "BEIJ9" },
  "next": "The server accepted the signal; the accounts in recipients execute it next. ..."
}

recipients are the accounts the signal was handed to: each MT5 terminal whose connection was open, and each cTrader, TradeLocker or crypto connection that is switched on, signed in and on a plan that is active. It is a hand-off, not a result: the deliveries say what each account did. accounts carries the same list as recipients, and connected_accounts is its length; both are kept for integrations written before recipients existed.

skipped lists accounts the signal server knows under the licence that did not get this signal, each with a code and a reason in words:

codeWhy the account was skipped
7021Its EA is too old for this action (modify actions need 0.68, a ticket needs 0.71, cancel_at on a pending order needs 0.80). Update the EA.
7601A cTrader account whose sign-in expired. Reconnect it from its card in the portal.
7701A TradeLocker account whose sign-in was refused. Reconnect it from its card in the portal.
7020The signal is an exit and carries noexit, so the exit is dropped and positions close only by stop loss or take profit.
7016The account's connection to the signal server was closing, so nothing was sent to it.

An account that is offline, switched off or on a lapsed plan is not in either list: the signal server only names accounts it holds right now.

Then read GET /api/v1/deliveries?signal_id=BEIJ9 to see whether each account executed it, or GET /api/v1/signals?include_deliveries=true to get signals and their per-account status in one call. Give the EA a moment before checking.

A 200 means the signal was accepted and passed to the accounts in recipients, the ones connected under that licence at that moment. It is not proof of execution: the deliveries say what each account did. A signal is not held for an account that is offline when it is sent. A signal goes to every account connected under the licence. To send to one account only, put that account on its own licence.

Two optional fields make sending safer:

  • "dry_run": true validates the order and asks the signal server who would receive it, without sending anything. The server reads the signal exactly as it would a real one (so a syntax or secret error comes back the same way) and picks the accounts with the very same rules, so recipients and skipped are the exact live answer for this signal, including the accounts it would leave out and why. Nothing is stored, no alert is raised, and an idempotency_key is not used up. The receipt has sent: false, and a dry run does not count toward the signal limits. A dry run is limited to 60 a minute per licence (reason rate_limited).

    {
      "dry_run": true,
      "sent": false,
      "syntax": "1234567890,EURUSD,modifylong,sl_price=1.0950",
      "license_code": "1234567890",
      "recipients": [
        {
          "platform": "mt5",
          "client_id": "ICM12345678",
          "broker_name": "IC Markets",
          "account_number": "12345678",
          "account_type": "demo",
          "ea_version": "0.81"
        },
        {
          "platform": "ctrader",
          "connection_id": "0d6f2c1e-4b0a-4f57-9a7e-2f1c9b8d7e11",
          "display_name": "Spotware 5896112",
          "account_type": "real"
        }
      ],
      "skipped": [
        {
          "platform": "mt5",
          "client_id": "EXN87654321",
          "broker_name": "Exness",
          "account_number": "87654321",
          "account_type": "real",
          "ea_version": "0.67",
          "code": 7021,
          "reason": "EA update required for this command: this command needs EA 0.68 or newer, and this account runs 0.67. Update the EA."
        }
      ],
      "connected_accounts": 2,
      "next": "Nothing was sent. recipients is exactly where the signal server would send it right now, and skipped lists the accounts it would not, with why. ..."
    }

    If nothing is connected under the licence and its plan is not active, a real send would be dropped. The dry run says so with blocked (code 7022) and empty lists.

    For an entry sized in lots the dry run also carries risk, priced on each recipient with live access (an MT5 account on a Cloud Terminal, or a cTrader account) from that account's own prices and rules, the way the order will be placed (symbol mapping, prefix and suffix, pip size included). A skipped account is not priced. TradeLocker and crypto accounts get no risk figures:

    "risk": [
      {
        "client_id": "ICM12345678",
        "symbol": "EURUSD",
        "side": "buy",
        "volume": 0.1,
        "entry_price": 1.1002,
        "entry_is_estimate": true,
        "stop_loss_price": 1.0972,
        "take_profit_price": 1.1062,
        "currency": "USD",
        "margin_required": 220.04,
        "margin_free": 9850,
        "loss_at_stop_loss": -30,
        "profit_at_take_profit": 60
      }
    ]

    A market order fills at whatever the price is when it arrives, so entry_is_estimate is true. An account the preview cannot price, such as one whose broker does not offer the symbol, gets a note instead of figures. Volumes given as a percent of balance or equity are not priced: the EA, or TradeSgnl on an account without one, turns them into lots when the signal arrives.

  • "idempotency_key" (any unique tag, 8 to 128 characters of letters, digits, _ and -) makes a retry safe. If your request times out after the order was already placed and you send it again with the same key, the server returns the original receipt with duplicate: true instead of placing a second order. Keys are kept for 24 hours; use a fresh key for each new order.

    The same idempotency_key for the same licence within 24 hours returns the original receipt with duplicate: true and sends nothing. A key is remembered only when the signal reached an account. A send that reached nothing (no account connected) or was rejected is not remembered, so the same key can be retried. A stored signal carries its key in params.idempotency_key on GET /api/v1/signals, so you can find a signal by its key.

Actions

ActionWhat it doesFields
buy, sellOpen at marketvolume; optional stop_loss, take_profit
buystop, buylimit, sellstop, selllimitPlace a pending ordervolume, entry; optional stop_loss, take_profit
closebuy, closesellClose positions on one side of the symboloptional partial, optional ticket
closeallClose every position on the symbol
cancellong, cancelshortCancel pending orders on one side
modifylong, modifyshortChange the stop loss and take profit of open positions on one sideat least one of stop_loss, take_profit; stop_loss also takes { "entry": true } to move it to each position's open price; optional ticket
modifybuystop, modifybuylimit, modifysellstop, modifyselllimitChange the entry, stop loss and take profit of unfilled orders of that typeat least one of entry, stop_loss, take_profit; optional ticket
closelongsell, closeshortbuy, closelongbuy, closeshortsellClose one side, then open the othervolume; optional stop_loss, take_profit

Each field is an object with exactly one key: volume is { "lots": n }, { "percent_balance": n } or { "percent_equity": n }; entry is { "pips": n } from the current price or { "price": n }; stop_loss and take_profit are { "pips": n }, { "points": n } (a price distance) or { "price": n }; partial is { "lots": n } or { "percent": n } from 1 to 100, and only goes with closebuy and closesell. The size to close is rounded up to the broker's lot step, and if what would remain is below the broker's minimum lot, the whole position closes. comment (1 to 31 characters: letters, digits, space, _ and -) is optional on every action and groups trades: a close or modify with a comment only touches positions that carry exactly that comment (exact and case-sensitive), and one without a comment touches every position the EA opened on that symbol and side.

ticket acts on one trade only. Give it as a string of digits or an integer. It is allowed with closebuy, closesell (with or without partial) and the six modify actions; any other action with a ticket is refused with 400 and reason invalid_input. For a pending-order modify it is the order's ticket. The ticket narrows the signal, it never widens it: symbol and side (and comment, if sent) must still match the position. A ticket that is no longer open changes nothing. The delivery of the signal that opened the trade carries the ticket, and GET /api/v1/live/account lists it on each open position. It needs EA 0.71 or later and an MT5 account. The MCP send_signal tool takes the same field.

curl -X POST https://portal.tradesgnl.com/api/v1/signals \
  -H "Authorization: Bearer tsk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"license_code":"1234567890123","symbol":"EURUSD","action":"closebuy","ticket":"10753414922"}'

Taking a level off a position is not available in JSON: send sl=none or tp=none in the comma-separated signal format.

Recipes

Google Sheets

IMPORTDATA cannot send a header, so use a short Apps Script instead. In your sheet open Extensions > Apps Script, paste this, run it once to grant access, then add a time-driven trigger to refresh it on a schedule.

function refreshSignals() {
  const res = UrlFetchApp.fetch(
    'https://portal.tradesgnl.com/api/v1/signals?limit=50&include_deliveries=true',
    { headers: { Authorization: 'Bearer tsk_YOUR_KEY' } },
  );
  const { signals } = JSON.parse(res.getContentText());
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.clear();
  sheet.appendRow(['Time', 'Symbol', 'Action', 'Volume', 'Executed on', 'Error']);
  signals.forEach((s) => sheet.appendRow([
    s.timestamp, s.symbol, s.command, s.volume,
    s.deliveries.filter((d) => d.status === 'executed' || d.status === 'received').length,
    s.error_message || '',
  ]));
}

WordPress

Any table plugin that can read a JSON URL with a custom request header works. Point it at the endpoint you want (for example https://portal.tradesgnl.com/api/v1/signals?limit=50), add Authorization: Bearer tsk_YOUR_KEY as a header, and set the JSON root to the list key (signals, clients, trades). Use a read-only key for anything that lives on a public site.

A daily P&L line from cron

GET /api/v1/trades returns a summary alongside the positions. Filtering by date_from limits the closed trades to that day, so this prints today's closed P&L every weekday evening:

0 22 * * 1-5 curl -s "https://portal.tradesgnl.com/api/v1/trades?client_id=ICM12345678&date_from=$(date +\%F)&limit=50" \
  -H "Authorization: Bearer tsk_YOUR_KEY" | jq '.summary | {total_profit, closed_trades, win_rate, currency}'

Errors and limits

Errors are JSON with an error message that says what to do next and a stable reason code, and use the usual statuses:

StatusWhen
400Invalid input; error names the field
401Missing, revoked or expired key
402AI credits exhausted (Market Analyst)
403The plan does not include the API, the key lacks the trade permission, or a per-key limit blocked the call. no_cloud_connection means no account has live access at all
404Unknown account (account_not_found) or symbol
409The account cannot be read: no_cloud_terminal (it has no Cloud Terminal), terminal_offline (its Cloud Terminal is stopped), not_connected or account_paused
422The terminal could not price the request: no tick for the symbol (no_data), or MT5 could not run the calculation (calc_failed)
429Rate limit reached (rate_limited)
502The terminal could not be read (terminal_unavailable), or the signal server rejected the signal (webhook_rejected) or could not be reached (webhook_unreachable); for a signal, nothing was sent
503The account could not be read (account_unavailable)
504A refresh did not land in time (refresh_timeout)

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp in seconds). Limits: 60 calls per minute across all endpoints and tools. Within that, POST /api/v1/signals is limited to 10 per minute and 200 per day. The REST API is part of TradeSgnl Cloud, included on the Advanced and Professional plans.

On this page