Connect your scripts and bots to MetaScalp to interact with exchange connections. Use HTTP REST to query data and execute trades, or WebSocket to receive real-time order, position, and balance updates.
HTTP API: Host: 127.0.0.1 Ports: 17845–17855 CORS enabled for all origins.
Socket API: Host: 127.0.0.1 Same port as HTTP Endpoint: ws://127.0.0.1:{port}/
Getting started: 1. Launch MetaScalp. 2. Discover the HTTP port with GET /ping. 3. List connections with GET /api/connections. 4. Use connection IDs for REST or WebSocket operations. Full documentation
Scan ports 17845–17855 to find the running MetaScalp instance. Returns the app name and version.
Changes the current ticker. The active window is always notified. When a named binding is provided, that binding's linked panels are also updated.
Accepts two request formats: a ticker pattern string (e.g. BINANCE:BTCUSDT.p) or explicit fields (exchange + market + ticker).
A binding is a named group of linked panels inside MetaScalp (e.g. a chart, order book, and trade feed showing the same ticker). Bindings are numbered "001"–"500" and configured by the user in the MetaScalp UI.
EXCHANGE:SYMBOL.suffix — suffix: .p (futures), .m (margin), .o (options), omitted (spot)_, /, -, $, :.BINANCE:BTCUSDT.p, BYBIT:ETHUSDT, OKX:BTC-USDT.m, HYPERLIQUID:cash:HOODUSDT0.p
Opens a combo layout for the specified ticker in the MetaScalp UI.
Two mutually exclusive payload shapes are accepted — the shape decides the behaviour, there is no request flag:
{ "ticker": "BTCUSDT" } — opens one combo layout for a single ticker (unchanged behaviour).{ "tickers": ["BTCUSDT", "ETHUSDT", "SOLUSDT"] } — opens one combo layout per ticker, in the order given.The tickers form is a write validated as all-or-nothing: the whole list is validated first, and if any ticker resolves on no eligible connection, nothing is opened and the request returns 400 naming the rejected tickers. Supplying both ticker and tickers, or an empty tickers array with no ticker, also returns 400.
Returns all currently active exchange connections. Use the id from the response to query orders, positions, balances, or to subscribe via WebSocket.
| Field | Type | Description |
|---|---|---|
id | integer | Connection ID — use for all exchange operations |
name | string | User-defined connection name |
exchange | string | Exchange name |
exchangeId | integer | Exchange identifier |
market | string | Market display name |
marketType | integer | Market type enum value |
state | integer | 0=Disconnected, 1=Connecting, 2=Connected, 3=Reconnecting, 4=Resetting |
viewMode | boolean | Read-only (no trading) |
demoMode | boolean | Paper trading |
Returns details for a single connection by ID.
Returns all available trading pairs on a connection with price/size precision, trading status, and constraints.
Optional query parameter Refresh=true forces a fresh fetch of the ticker list from the exchange instead of returning the cached set.
Returns open orders for a specific ticker on a connection. The Ticker query parameter is required.
| Field | Type | Description |
|---|---|---|
id | integer | Exchange order ID |
ticker | string | Trading pair |
clientId | string? | Client-generated order ID |
side | integer | 0 None, 1 Buy, 2 Sell |
price | decimal | Order price |
size | decimal | Order size |
filledSize | decimal | Filled amount |
filledPrice | decimal | Execution price (0 if not yet filled) |
remainingSize | decimal | Remaining amount |
status | integer | 0 New, 1 Open, 2 Closed |
type | integer | 0 Limit, 1 Stop, 2 StopLoss, 3 TakeProfit, 4 Market |
triggerPrice | decimal? | Trigger price for stop/conditional orders |
createDate | string (ISO) | Order creation timestamp |
Read-only. Reports, live from the venue, the maximum leverage the venue allows and the maximum
position size available for a ticker at the current (or an explicitly supplied) leverage. Values come
from the venue's own risk-limit tiers — nothing is locally computed, cached or defaulted.
The Ticker query parameter is required.
| Field | Type | Description |
|---|---|---|
connectionId | integer | Connection ID |
ticker | string | Trading pair |
leverage | decimal | The leverage the max position is reported for — the supplied leverage query param if given, otherwise the venue's current leverage for this ticker |
maxLeverage | decimal? | Highest leverage the venue allows for this ticker. null means the venue reports no cap (e.g. a spot market with no leverage tiers) — never 0 |
maxPosition | decimal? | Maximum position size available at the reported leverage (re-read per leverage). null means the venue reports no cap — never 0 |
| Query | Type | Description |
|---|---|---|
Ticker | string | Required. Trading pair. |
Leverage | decimal | Optional. Report the max position at this leverage instead of the venue's current leverage. Must be greater than 0. |
Read-only. Returns a fresh REST order-book snapshot for every panel currently in link group
groupId. The membership is read LIVE from the UI's own grouping — a panel
unlinked or closed a moment ago is already absent, and nothing about linking is changed by this
request. A link group is a number (the link value the UI shows), not a stored entity: an in-range
group with no live members is a legitimate empty result (200 with an empty
members list), NOT a 404. Valid group ids are 1–500.
Partial failure never fails the whole response. Each member is reported
independently: a good book carries ok: true and a book object; a member
that could not be snapshotted carries ok: false and a stable machine-readable
reason token. One failing book never turns into a 500.
| Field | Type | Description |
|---|---|---|
groupId | integer | The link group (binding) id, 1–500 |
members | array | One entry per panel currently live in the group (empty when the group has no live members) |
members[].externalId | guid | The panel's document id |
members[].kind | string? | orderBook or chart (null when the document could not be resolved) |
members[].connectionId | integer? | The panel's connection id (null when unset/unresolved) |
members[].ticker | string? | The panel's ticker (null when unset/unresolved) |
members[].ok | boolean | true when a snapshot was returned, else false |
members[].book | object | Present only when ok: true. Same shape as orderbook-snapshot: updateId, asks, bids, bestAsk, bestBid |
members[].reason | string | Present only when ok: false. A machine-readable token (see below) |
reason token | Meaning |
|---|---|
resolve_failed | The document could not be resolved from the layout |
not_an_order_book | The member is a chart (no order book to snapshot) |
no_connection | The panel has no connection set |
no_ticker | The panel has no ticker set |
connection_not_open | The panel's connection is not currently open |
market_service_unavailable | The connection has no active market service |
unsupported_exchange | The exchange has no REST snapshot endpoint (e.g. the Bybit family) |
snapshot_unavailable | The exchange returned no snapshot |
snapshot_failed | Fetching the snapshot threw |
Status codes: 200 (including an empty group); 400 for a non-numeric or
out-of-range groupId.
Returns all open positions on a connection (futures/margin markets).
Returns account balances for all assets on a connection.
Places a new order on the exchange through a connection. Returns an auto-generated clientId for order tracking and executionTimeMs indicating how long the exchange request took (in milliseconds).
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | Trading pair symbol |
side | integer | yes | 1 Buy, 2 Sell |
price | decimal | yes* | Order price (*required for non-market orders) |
size | decimal | yes | Order size (must be > 0) |
type | integer | no | 0 Limit (default), 1 Stop, 2 StopLoss, 3 TakeProfit, 4 Market |
reduceOnly | boolean | no | Close-only, default false |
Cancels an existing order on the exchange.
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | Trading pair symbol |
orderId | integer | yes | Exchange order ID |
type | integer | no | Order type (default 0 Limit) |
Cancels all open orders for a given ticker on the exchange.
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | Trading pair symbol |
Returns { status, cancelledCount } — cancelledCount: 0 if there are no open orders for that ticker.
Returns the current cluster (volume profile / footprint) data for a ticker. The snapshot contains up to 10 time columns, each holding bid/ask volumes at every price level.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
Ticker | string | yes | Trading pair symbol | |
TimeFrame | string | yes | S30, M1, M5, M10, M15, M30, H1, H4, D1 | |
ZoomIndex | int | no | 1 | Price aggregation factor (1 = raw levels) |
When ZoomIndex > 1, prices are grouped into buckets of ZoomIndex × PriceIncrement and volumes are summed.
Signal levels are price triggers attached to a (connectionId, ticker). A level fires
when the last trade crosses its price in the direction of its triggerRule.
An object created via the API is indistinguishable from one drawn by hand: same table, same
visibility in every window showing that instrument. Read the whole set with
GET /api/connections/{connectionId}/signal-levels?Ticker=...; delete one with
DELETE .../signal-levels/{signalLevelId}, or all for a ticker with
DELETE .../signal-levels?Ticker=.... GET returns each level as
id, connectionId, ticker, price,
isTriggered, triggerTime, triggerRule, plus
(MC-421) note and the seven appearance fields
lineThickness, lineStyle, lineColor, textSize,
textColor, textAlignment, textStyle (each null
when unset; the enum-backed ones are emitted as their string name, e.g. "Dashed").
Returns all signal levels for a specific ticker on a connection, each with the fields
described above (id, connectionId, ticker,
price, isTriggered, triggerTime, triggerRule,
note and the seven appearance fields, null when unset).
Read-modify-write: id, connectionId,
ticker, isTriggered and triggerTime are
accepted and ignored by the matching PUT, so the object returned here can
be edited and sent straight back. Trigger state is owned by the signal engine and is never
settable over the API.
Creates a signal level for a ticker. The ticker must have live order book data (best ask is required) — subscribe to its order book first.
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | Trading pair symbol |
price | decimal | yes | Trigger price (must be > 0) |
triggerRule | string | no | Firing direction: LessThanEqual (fires when a trade price ≤ price) or GreaterThanEqual (fires when ≥ price), case-insensitive. Omitted → derived from best ask exactly as before: price ≤ bestAsk ? LessThanEqual : GreaterThanEqual. |
note | string | no | Free-text note/label for the level. Omitted → no note (unchanged). |
lineThickness | number | no | Stroke thickness. Omitted → unset (today's theme-driven stroke). |
lineStyle | string | no | Solid / Dashed / Dotted (case-insensitive, or the enum int). Omitted → unset. |
lineColor | string | no | Line colour as a hex string. Omitted → unset. |
textSize | number | no | Label font size. Omitted → unset. |
textColor | string | no | Label colour as a hex string. Omitted → unset. |
textAlignment | string | no | Left / Center / Right (case-insensitive, or the enum int). Omitted → unset. |
textStyle | string | no | Normal / Bold / Italic (case-insensitive, or the enum int). Omitted → unset. |
Appearance is additive and optional (MC-421): every appearance field defaults to unset — a level created without them looks exactly as it does today.
Errors: missing ticker/price → 400;
price ≤ 0 → 400 Price must be greater than zero; unknown
triggerRule → 400 Invalid 'triggerRule'. Allowed values: LessThanEqual,
GreaterThanEqual.; an invalid appearance enum value (lineStyle /
textAlignment / textStyle) → 400 Invalid '{field}'.;
no order book data for the ticker → 400; unknown
connection → 404 Connection {id} not found.
Partial in-place update of an existing signal level. Only the supplied fields are applied;
the level keeps its identity (the same id row is modified —
no delete + re-create). At least one of price / triggerRule must be
present.
| Field | Type | Required | Description |
|---|---|---|---|
price | decimal | no | New trigger price (must be > 0). Leaves triggerRule untouched. |
triggerRule | string | no | LessThanEqual / GreaterThanEqual, case-insensitive. Leaves price untouched. |
note | string | no | New note. Leaves the other fields untouched. |
lineThickness | number | no | New stroke thickness. Leaves the other fields untouched. |
lineStyle | string | no | Solid / Dashed / Dotted (or the enum int). Leaves the other fields untouched. |
lineColor | string | no | New line colour (hex). Leaves the other fields untouched. |
textSize | number | no | New label size. Leaves the other fields untouched. |
textColor | string | no | New label colour (hex). Leaves the other fields untouched. |
textAlignment | string | no | Left / Center / Right (or the enum int). Leaves the other fields untouched. |
textStyle | string | no | Normal / Bold / Italic (or the enum int). Leaves the other fields untouched. |
Partial update (MC-421): the note and every appearance field follow the same
partial-apply rule as price/triggerRule — only the fields present
are written, the rest are left untouched. At least one of price /
triggerRule / note / an appearance field must be present. An invalid
appearance enum value → 400 Invalid '{field}'.
Re-arm on price move: if the level had already fired
(isTriggered = true) and its price is changed, it is re-armed
(isTriggered back to false, triggerTime cleared) so it can
fire again at the new price.
Errors: invalid connection id → 400 Invalid connection ID;
connection not open → 404 Connection {id} not found; invalid level id →
400 Invalid signal level ID; empty body / all fields null → 400 Request
body must set at least one of 'price', 'triggerRule', 'note' or an appearance field.; price ≤ 0 →
400 Price must be greater than zero; unknown triggerRule →
400 Invalid 'triggerRule'. Allowed values: LessThanEqual, GreaterThanEqual.; level
not found → 404 Signal level {id} not found.
Removes a single signal level by ID.
Removes all signal levels for a specific ticker on a connection.
Removes all triggered signal levels across all connections and tickers.
Returns all order book settings for a specific ticker on a connection. Settings include trading defaults, order book display configuration, tick thresholds, and cluster options.
Partial update — only send the fields you want to change. Omitted fields keep their current values. Changes are pushed live to the running panels. Returns the full updated settings object.
Enum fields: ShowRuler (None, Points, Percent, PercentVolume), ZoomType (Absolute, Percentage), SizeType (Coin, Usd).
Fetches a fresh order book snapshot directly from the exchange REST endpoint — no cache lookup, no WebSocket subscription side effects. Intended as a one-shot complement to orderbook_subscribe with FetchSnapshot=false: subscribe to deltas cheaply, then call this once per ticker when you actually need to seed the book.
Each call performs one REST request to the exchange. The caller is responsible for not exceeding the exchange's rate limit when invoking this endpoint for many tickers in quick succession.
Returns 501 Not Implemented for exchanges that don't expose a REST snapshot endpoint (e.g. Bybit USDT Perpetual, which only delivers snapshots over WebSocket).
User levels are the plain price levels a trader draws by hand in the order book and on the chart
(a horizontal price line with a text label). They are attached to a (connectionId, ticker).
An object created via the API is indistinguishable from one drawn by hand: same table, same label
default, same visibility in every window showing that instrument. Read the whole set with
GET /api/connections/{connectionId}/user-levels?Ticker=...; delete one with
DELETE .../user-levels/{userLevelId}, or all for a ticker with
DELETE .../user-levels?Ticker=.... GET returns each level as
id, connectionId, ticker, price, name,
date. Note on date: the value is stored as epoch seconds;
it is returned as a nullable ISO-8601 timestamp but accepted on input as the raw
epoch-seconds integer (to preserve fidelity with the hand-drawn horizontal-ray path, which stores
the raw chart time). A plain level has date = null.
Returns all user levels for a specific ticker on a connection, each with the fields
described above (id, connectionId, ticker,
price, name, note, date — the
date is returned as a nullable ISO-8601 timestamp), plus the seven appearance
fields lineThickness, lineStyle, lineColor,
textSize, textColor, textAlignment,
textStyle (each null when unset; the enum-backed ones are emitted as
their string name). (MC-421 Step 15)
note is the user level's own free-text note (null when unset), read
back exactly as sent — the same as on a signal level; it is distinct from name.
Read-modify-write: id, connectionId and
ticker are accepted and ignored by the matching PUT, so the
object returned here can be edited and sent straight back. One exception: a level whose
date is non-null is not round-trippable verbatim — GET emits
ISO-8601 while PUT takes raw epoch seconds.
⚠ Stored and returned, but not drawn. Unlike a signal-level note,
a user-level note is not rendered on the order book or on either chart engine: a user
level has a single label slot and name already occupies it. The note is an API-level
attribute only until the layout question is settled.
Errors: invalid connection id → 400 Invalid connection ID;
unknown connection → 404 Connection {id} not found; missing Ticker
query → 400 Query parameter 'Ticker' is required.
Creates a user level for a ticker. Unlike signal levels there is no market-data precondition — the hand-drawn path has none, so the API does not add one.
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | Trading pair symbol |
price | decimal | yes | Level price (must be > 0) |
name | string | no | Label. Omitted → the exact hand-drawn default the app uses when a level is drawn by hand: today's date as dd.MM.yyyy. |
note | string | no | (MC-421 Step 15) The user level's own free-text note, stored and read back exactly as on a signal level. Omitted → null (no note). Distinct from name. |
date | integer (epoch seconds) | no | Optional timestamp (the hand-drawn horizontal ray stores the chart time here). Omitted → null (a plain level). |
lineThickness | number | no | Stroke thickness. Omitted → unset (today's theme-driven stroke). |
lineStyle | string | no | Solid / Dashed / Dotted (case-insensitive, or the enum int). Omitted → unset. |
lineColor | string | no | Line colour as a hex string. Omitted → unset. |
textSize | number | no | Label font size. Omitted → unset. |
textColor | string | no | Label colour as a hex string. Omitted → unset. |
textAlignment | string | no | Left / Center / Right (case-insensitive, or the enum int). Omitted → unset. |
textStyle | string | no | Normal / Bold / Italic (case-insensitive, or the enum int). Omitted → unset. |
Appearance is additive and optional (MC-421): every appearance field defaults to unset — a level created without them looks exactly as it does today.
Errors: missing ticker/price → 400 Invalid
request body. 'ticker' and 'price' are required.; price ≤ 0 →
400 Price must be greater than zero; an invalid appearance enum value
(lineStyle / textAlignment / textStyle) →
400 Invalid '{field}'.; invalid connection id → 400 Invalid
connection ID; unknown connection → 404 Connection {id} not found.
Partial in-place update of an existing user level. Only the supplied fields are applied; the
level keeps its identity (the same id row is modified — no
delete + re-create). At least one of price / name / date
must be present.
| Field | Type | Required | Description |
|---|---|---|---|
price | decimal | no | New price (must be > 0). Leaves name / date untouched. |
name | string | no | New label. Leaves the other fields untouched. |
note | string | no | (MC-421 Step 15) New note (partial-update: supplied → overwrite, omitted → untouched). Distinct from name. |
date | integer (epoch seconds) | no | New timestamp. Leaves the other fields untouched. |
lineThickness | number | no | New stroke thickness. Leaves the other fields untouched. |
lineStyle | string | no | Solid / Dashed / Dotted (or the enum int). Leaves the other fields untouched. |
lineColor | string | no | New line colour (hex). Leaves the other fields untouched. |
textSize | number | no | New label size. Leaves the other fields untouched. |
textColor | string | no | New label colour (hex). Leaves the other fields untouched. |
textAlignment | string | no | Left / Center / Right (or the enum int). Leaves the other fields untouched. |
textStyle | string | no | Normal / Bold / Italic (or the enum int). Leaves the other fields untouched. |
Partial update (MC-421): every appearance field follows the same partial-apply
rule — only the fields present are written, the rest are left untouched. At least one of
price / name / note / date / an appearance
field must be present.
Errors: invalid connection id → 400 Invalid connection ID;
connection not open → 404 Connection {id} not found; invalid level id →
400 Invalid user level ID; empty body / all fields null → 400 Request
body must set at least one of 'price', 'name', 'date', 'note' or an appearance field.;
an invalid appearance enum value → 400 Invalid '{field}'.;
price ≤ 0 →
400 Price must be greater than zero; level not found → 404 User level {id}
not found.
Removes a single user level by ID.
Errors: invalid connection id → 400 Invalid connection ID;
unknown connection → 404 Connection {id} not found; invalid level id →
400 Invalid user level ID.
Removes all user levels for a specific ticker on a connection.
Errors: invalid connection id → 400 Invalid connection ID;
unknown connection → 404 Connection {id} not found; missing Ticker
query → 400 Query parameter 'Ticker' is required.
Chart annotations are the free-form shapes a trader draws on the chart, attached to a
(connectionId, ticker). There are three separate lists — trend
lines, horizontal-lines and horizontal-rays. An object created
via the API is indistinguishable from a hand-drawn one by construction: every write
(API and sidebar alike) goes through the same annotation service, so an API-created shape appears
identically in every chart environment showing that instrument (both the SciChart and TradingView
engines read the same rows — the store is per-(connectionId, ticker), not per-engine).
No per-item identity. Annotations carry no id — the only handle is the
zero-based index within its own list. GET stamps each item with its
index; POST appends one; DELETE .../{type}/{index} removes by
that index (out of range → 404, list unchanged). The primary write is
PUT .../{type}, a whole-list replace for one type. Because addressing is
by index and every edit replaces the list, concurrent editors are last-writer-wins
(inherent to the id-less model, not a defect).
{type} is one of lines / horizontal-lines /
horizontal-rays. Geometry by type: a line is
{ x1, x2 (ISO-8601 datetime), y1, y2 (number) }; a horizontal-line is
{ y }; a horizontal-ray is { y, x1 (ISO-8601 datetime) }. The
?Ticker= query is required on every route; an unknown connection →
404 Connection {id} not found.
Prices are venue prices — in both directions. The y /
y1 / y2 fields are the instrument's real market price
(e.g. 0.2 for ADAUSDT). POST/PUT a venue price and it lands on the chart at that price;
GET returns the on-screen venue price of every shape, including hand-drawn ones. Timestamp fields
(x1 / x2) are unchanged.
⚠ Breaking change (from build 1.0.2835-MC-400). Earlier builds passed
y / y1 / y2 through unconverted, exposing the chart's
internal axis units (the price axis is scaled per instrument — ×104
for ADAUSDT), so a line POSTed at 0.2 landed off-scale and a hand-drawn shape read back at
≈1999.79. These fields now carry venue prices, converted per
(connectionId, ticker) from that instrument's own price increment. Any client that
was compensating by multiplying/dividing by the axis factor must stop doing so and send/expect
plain venue prices.
Unknown / zero-increment instrument. The conversion needs the instrument's price
increment. If the Ticker is not known on the connection, or its price increment is
≤ 0, the request is rejected with
400 Ticker '{ticker}' not found on connection {connectionId} on GET, PUT and POST —
the API never silently falls back to raw axis units (which would reintroduce the off-scale defect for
that one instrument).
Live redraw of open charts. A POST/PUT/DELETE now redraws every already-open chart
of that (connectionId, ticker) immediately, without reopening. This is an in-app
change only: the client-facing contract is unchanged — there is no new
annotations_* WebSocket push and no new unit conversion. Requests, responses and the venue-price
units above are exactly as before.
Returns the whole set as
{ connectionId, ticker, lineAnnotations[], horizontalLineAnnotations[],
horizontalRayAnnotations[] }. Each item carries its geometry plus its zero-based
index in its own list — the only handle usable for
DELETE .../{type}/{index}.
| List | Item fields |
|---|---|
lineAnnotations | index, x1, x2 (ISO-8601 datetime), y1, y2 (venue price) |
horizontalLineAnnotations | index, y (venue price) |
horizontalRayAnnotations | index, y (venue price), x1 (ISO-8601 datetime) |
The y / y1 / y2 values are venue prices,
converted out of the chart's internal axis units per (connectionId, ticker). A
hand-drawn shape reads back at its on-screen price (e.g. 0.2, not
≈1999.79).
Since MC-400 Step 15c: this GET also returns shapes a trader draws
by hand on the TradingView chart — horizontal_line,
horizontal_ray and trend_line tools are written back into the same
shared store as they are created / moved / removed, and appear here in venue prices exactly like
API-created shapes (the TradingView bundle dist/bundle.js was rebuilt for this).
Errors: invalid connection id → 400 Invalid connection ID;
unknown connection → 404 Connection {id} not found; missing Ticker
query → 400 Query parameter 'Ticker' is required; unknown ticker or
PriceIncrement ≤ 0 → 400 Ticker '{ticker}' not found on connection
{connectionId}.
The primary write: replaces the entire list for {type} with the
posted array. The body is a JSON array of items of that type's geometry (a null / empty body
clears the list). This is how a client persists edits — read with GET, mutate
the array client-side, PUT it back.
Concurrency — last-writer-wins. Because the annotation model has no
per-item identity (the only handle is the list index) the write is a whole-list replace, not a
per-item patch. If the sidebar and the API (or two API clients) edit the same
(connectionId, ticker) list concurrently, the last PUT overwrites the
other's changes wholesale. Read immediately before you PUT to minimise the window.
{type} | Body (array of) |
|---|---|
lines | { x1, x2 (ISO-8601 datetime), y1, y2 (venue price) } |
horizontal-lines | { y (venue price) } |
horizontal-rays | { y (venue price), x1 (ISO-8601 datetime) } |
Send y / y1 / y2 as venue prices; they
are converted to the chart's axis units per (connectionId, ticker) before storage, so
the shape lands at the price you sent.
Errors: unknown {type} → 400 Unknown annotation type
'{type}'. Allowed values: lines, horizontal-lines, horizontal-rays.; invalid connection id
→ 400; unknown connection → 404; missing Ticker
→ 400; unknown ticker or PriceIncrement ≤ 0 →
400 Ticker '{ticker}' not found on connection {connectionId}.
Appends a single annotation to the {type} list. The body is one item (not an
array) of that type's geometry, with y / y1 / y2 as
venue prices (converted to axis units per (connectionId, ticker)
before storage). Implemented as read-modify-write inside the one action (read the
current list, append, replace the whole list) — the list is never cached across requests. The
appended item lands at the last index.
Errors: unknown {type} → 400; empty / wrong-shape
body → 400 Request body must be a {type} annotation.; invalid connection id
→ 400; unknown connection → 404; missing Ticker
→ 400; unknown ticker or PriceIncrement ≤ 0 →
400 Ticker '{ticker}' not found on connection {connectionId}.
Removes the annotation at the given zero-based {index} in the {type}
list (read-modify-write inside the one action). An out-of-range index is a
404 and the stored list is left unchanged (the whole-list replace is not
performed). The index is the value stamped on each item by GET.
Errors: unknown {type} → 400; non-numeric index
→ 400 Invalid annotation index; index out of range → 404
{Type} annotation index {index} not found; invalid connection id → 400;
unknown connection → 404; missing Ticker → 400.
Clears all three annotation lists (lines, horizontal-lines, horizontal-rays)
for the (connectionId, ticker) in one call — the same action the «clear
all shapes» sidebar command performs.
Errors: invalid connection id → 400; unknown connection
→ 404; missing Ticker → 400.
Manage the saved screener templates (the named sets of columns, filters and market
selections the market screener uses) without opening a window. Every open screener window listens to the
same OnTemplatesChanged broadcast the app raises internally, so a create / edit / delete made
here is reflected live in the window's template list — no reopen needed. The list
always includes the synthetic read-only Default template (id -1): it is
list-only and cannot be fetched, edited or deleted individually. Bodies are camelCase; the
settings object is the full template configuration and round-trips verbatim.
Returns { count, templates[] }. Each template is
{ id, name, settings } where settings carries the whole configuration
(columnSettings[], columnFilters[], marketFilters[],
coinTags[], numberOfRows, activeFilter, the new-coin flags,
…). The synthetic Default template (id -1) is always first.
Returns the single template { id, name, settings }.
Errors: non-numeric {templateId} → 400 Invalid template
ID; unknown id (including the synthetic Default -1) → 404 Screener
template {id} not found.
Body { name, settings? }. name is required; settings is
optional — omit it and the template is created with the default configuration. The full
settings blob (columns / filters / market selections) is persisted, so a template
created here carries its filters and columns, not just its name. Returns the created
{ id, name, settings }.
Errors: missing / blank name → 400 Request body must
carry a non-empty 'name'.; body that is not valid JSON → 400 Request body is not
valid JSON.
Body { name?, settings? }. A null / omitted name keeps
the stored name and a null / omitted settings keeps the stored blob (partial update).
Returns the updated { id, name, settings }.
Errors: non-numeric {templateId} → 400; body that
is not valid JSON → 400; unknown id → 404 Screener template {id} not
found (checked before any write).
Removes the template. Returns { status: "ok" }.
Errors: non-numeric {templateId} → 400; unknown id
(including the synthetic Default -1) → 404 Screener template {id} not
found (checked before any write).
Returns ONE headless snapshot of the full row set of the given template — every row an open
screener window on the same template would show — without opening a screener window.
A fresh background screener socket is subscribed, the first complete frame captured, and the socket
disposed; there is no streaming. The synthetic Default (id -1) is answered too (it resolves
to the default configuration, exactly like an open window's fallback).
Response { templateId, count, rows[] }. Each row:
{ ticker, wireTicker, isNewCoin, exchange, listedExchanges[], columns[] } where
exchange / listedExchanges are screener exchange keys
("binance_s", "bybit_f", "polymarket") and each
columns[] entry is { type, timeFrame, time, metric, value }.
Errors: non-numeric {templateId} → 400 Invalid template
ID; unknown id → 404 Screener template {id} not found (checked before
any subscribe). If the screener backend produces no rows within the deadline, rows is an
empty array (count = 0).
Push your own row into MetaScalp's notification feed (the «Line notifications» window).
An injected notification is indistinguishable from a native one except for its
event-type label: it shares the same feed, the same 200-row cap, the same ordering, and is delivered
live over the WebSocket as notification_update. Every field is optional
— a field you do not send renders as an empty cell. Nothing is persisted (in-memory
only). The feed's sound and the settings checkboxes do not apply to an injected row.
Builds a notification and adds it to the feed. All fields are optional. The
eventType is an arbitrary string rendered verbatim in the Event Type column
(no fixed enum). A field that is not sent leaves that column empty — price and
size stay blank (no 0 / -1 sentinel).
| Field | Type | Required | Description |
|---|---|---|---|
time | string (ISO-8601) | no | Notification timestamp. Omitted → the request time (server UTC now). |
connectionId | integer | no | When supplied, fills the exchange icon + S/F badge and the exchange/market/colour cells from that connection. Omitted → those cells stay empty. Unknown id → 404. |
ticker | string | no | Instrument label. Omitted → empty cell. |
eventType | string | no | Free-text label shown in the Event Type column, verbatim. Omitted → empty cell. |
size | decimal | no | Size cell. Omitted → empty (null, no sentinel). |
price | decimal | no | Price cell. Omitted → empty (null, no sentinel). |
tabName | string | no | Tab-name cell. Omitted → empty cell. |
Success → 200 { "status": "ok" }. The row is broadcast to subscribed WebSocket
clients as a notification_update whose type is the supplied
eventType (or, for native notifications, the notification-type name as before).
Notifications globally disabled still answers 200. This endpoint
injects directly into the in-memory feed and its WebSocket broadcast; it does not consult the
user's global «show notifications» toggle or the per-type settings checkboxes. A request
therefore succeeds (and is delivered to subscribed WS clients) even when the desktop UI would
suppress the equivalent native pop-up. Treat delivery as feed-level, not UI-visibility-level.
Errors: unknown connectionId → 404 Connection {id} not
found.
Two read-only endpoints describe the whole open interface — every window, its tabs, and the
order books and charts inside each — plus standalone chart windows. The tree is rebuilt entirely
from the saved layout in the database, never by reading the live on-screen windows
(each runs on its own UI thread and cannot be touched from the API). Every field is always
present: a value the system does not store, or that only exists at runtime, is reported as
null, never omitted. Nothing is opened, closed or changed.
Returns the whole open UI as
{ "windows": [ ... ], "standaloneCharts": [ ... ] }. Each window carries its
geometry, monitor and tabs; each tab its documents (order books and charts); each document its
identity, connection, ticker, link number and docking position, plus kind-specific fields.
| Field | Type | Nullable | Description |
|---|---|---|---|
id | integer | no | Persisted window id. |
type | string | no | Window type (Workspace / MainWorkspace / LightWorkspace). A window has no title and no persisted focus flag — both are dropped; the «title» is a tab concept (tab.name) and the «active» signal is tab.isSelected. |
geometry | object | no | { top, left, width, height, isFullScreen, pinOnTop, isTopBarHidden }. pinOnTop is nullable (tri-state in the model). |
monitor | integer | always null | Would be computed from geometry vs the screen list, but screen enumeration lives in a UI assembly the API does not reference; no dispatcher-safe helper is reachable, so this is reported as null (pre-authorized fallback). |
tabs | array | no | The window's tabs (see below). |
| Field | Type | Nullable | Description |
|---|---|---|---|
id | integer | no | Persisted tab id. |
name | string | yes | Tab name (the «title»). |
orderId | integer | no | Tab ordering index. |
isSelected | boolean | no | Whether this is the active tab in its window (the only persisted «active» signal). |
color | string | yes | Tab colour, when set. |
documents | array | no | The order books and charts on this tab. |
| Field | Type | Nullable | Description |
|---|---|---|---|
id | integer | no | Persisted document id. |
externalId | string (GUID) | no | The stable id the docking layout XML joins on (its ContentId). |
kind | string | no | orderBook or chart. |
connectionId | integer | yes | Owning connection; null when the document is not bound to a connection. |
ticker | string | yes | Instrument label; null when unset. |
linkNumber | integer | yes | The link/binding group id read from the runtime binding bus (bindingTypeId, not a column). null when the document is not a binding source. DB-backed — no UI thread is used. |
layoutPosition | object | yes | { paneGroupIndex, documentIndex, orientation } parsed from the tab's AvalonDock layout XML (matched by externalId == ContentId). null for standalone charts (no tab layout) and for documents absent from the layout XML. |
orderBook | object | yes | Order-book details (below); null when kind != orderBook. |
chart | object | yes | Chart details (below); null when kind != chart. |
document.orderBook)| Field | Type | Nullable | Description |
|---|---|---|---|
baseAsset | string | yes | Base asset. |
quoteAsset | string | yes | Quote asset. |
priceIncrement | decimal | no | Price tick size. |
sizeIncrement | decimal | no | Size step. |
minSize | decimal | no | Minimum order size. |
maxSize | decimal | yes | Maximum order size, when defined. |
stepPrice | decimal | yes | Step price, when defined. |
state | string | no | Order-book state (Open / Closing). |
isTradingAllowed | boolean | no | Whether trading is enabled on this book. |
pinMarketType | boolean | no | Whether the market type is pinned. |
zoomIndex | integer | yes | The order-book zoom (aggregation) index, resolved from the persisted order-book settings set (OrderBookSettingsSet.ZoomIndex) via a non-side-effecting read. null only when the document has no connection/ticker or no settings row exists. This is not the market-data per-WS-subscription zoomIndex. |
document.chart)| Field | Type | Nullable | Description |
|---|---|---|---|
timeFrame | string | no | Chart time frame enum name (None, M1, …). |
engine | string | no | Chart engine: Regular or TradingView. |
xVisibleRangeDiff | integer | no | Visible x-range span. |
pinMarketType | boolean | no | Whether the market type is pinned. |
geometry | object | yes | Own window geometry for standalone charts; null for charts docked inside a tab. |
standaloneCharts is a top-level array of chart documents (same document shape) for
chart windows not attached to any tab (tabId == null); each includes its own
chart.geometry and never appears under a tab.
Alongside windows and standaloneCharts, the inventory lists every OTHER
open window family the open route (POST /api/ui/windows) can create. Every key is always
present; where a value is absent it is null (never omitted), and a family that fails to
read degrades to its own empty list / null object without blanking the rest.
| Field | Type | Nullable | Description |
|---|---|---|---|
watchlists | array | no | Open watchlist windows, each { id, externalId, geometry }. A watchlist row has no visibility flag — every listed row is an open window. |
screeners | array | no | Open screener windows, each { id, externalId, name, templateId, geometry }. A screener row exists only while its window is open (created on open, removed on close), so every listed row is open. |
lineNotifications | object | yes | The line-notifications singleton: { id, isOpen, geometry }. The row is always persisted; isOpen is true only when the window is actually shown — a persisted-but-hidden singleton reads isOpen: false. null if the family could not be read. |
finres | object | yes | The finres singleton, same shape/semantics as lineNotifications. |
userTrades | object | yes | The user-trades singleton, same shape/semantics. |
listing | object | yes | The listing singleton, same shape/semantics. |
Success → 200 with the object above.
Returns exactly the same single-window object as that window's entry in
/api/ui/state — same geometry, tabs and documents.
Optional ?windowType= query. The window ids returned by
POST /api/ui/windows are per-surface: a chart / watchlist / screener id is unique
only within its own surface, not against the workspace Windows table this route reads by
default. Supply ?windowType= with one of chart, tradingViewChart,
watchlist or screener (case-insensitive) to resolve the id within that
surface instead — letting a client read a window back at the very address the create route
returned. The response is then { id, type, document | watchlist | screener } (exactly one
of the three type blocks is populated). Omit the query for byte-for-byte today's Windows-table
lookup.
Errors: a non-numeric windowId →
400 Invalid window ID; an unknown window (in the default table, or within the requested
windowType surface, or an unrecognised windowType) →
404 Window {id} not found.
Sets the link (binding-group) number of one addressed panel — the same
linkNumber reported by GET /api/ui/state
(document.linkNumber). {externalId} is the document's GUID.
Request body:
{ "linkNumber": 3 }
| Field | Type | Required | Description |
|---|---|---|---|
linkNumber | integer | yes | Binding-group id: 1 = active window, 2–500 = a user link, 9999 = cancel link. Must be positive. |
Persisted immediately via the binding bus (a DB-backed write — no UI thread is used). The stored link changes at once; a panel already on screen may not fully re-wire its live link until it is reloaded. Affects only the addressed document.
Asynchronous w.r.t. the UI thread. Success → 200 { "status": "ok" }.
Errors: a non-GUID externalId → 400 Invalid document ID;
a missing/omitted linkNumber → 400; a non-positive value →
400; an unknown document → 404 Document {externalId} not found
(nothing is written).
Opens ONE terminal window of the requested type at an explicit position and size, on an explicit
monitor. Geometry is entirely optional: omit top/left/
height/width and the window lands exactly where it lands today for that type
— including MetaScalp's open-at-the-cursor placement where the app does that today (workspace,
watchlist, screener, line-notifications, finres, user-trades). A supplied rect is clamped to fit the
target monitor. Position is in virtual-desktop coordinates; monitor (0-based) selects the
screen.
Request body:
{ "windowType": "watchlist", "top": 100, "left": 200, "height": 450, "width": 300, "monitor": 0 }
| Field | Type | Required | Description |
|---|---|---|---|
windowType | string | yes | One of: workspace, lightWorkspace, chart, tradingViewChart, watchlist, screener, lineNotifications, finres, userTrades, listing. |
top | number | no | Virtual-desktop Y. Omit for today's default placement. |
left | number | no | Virtual-desktop X. Omit for today's default placement. |
height | number | no | Window height. Omit for the type's default. |
width | number | no | Window width. Omit for the type's default. |
monitor | integer | no | 0-based target screen. Omit to place by virtual-desktop coordinate. |
templateId | integer | no | Screener only. Open the screener already showing this saved template's filters and rows — the same state the window's own template picker produces. Omit for today's Default view. The synthetic Default (id -1) is accepted and resolves to Default. Ignored for any non-screener windowType. |
Response { windowId, windowType, top, left, height, width } — where the window
actually landed (post-clamp). For the four singleton types (lineNotifications,
finres, userTrades, listing) windowId is
null: each has a single fixed row (id 1, which would collide with the main
workspace) and its close/activate routes ignore the id entirely, so there is no addressable per-surface
id to return. Every other type returns its real per-surface id.
Opening the screener on a template. For windowType: "screener" a
supplied templateId is threaded onto the screener window so it loads that template exactly as
picking it by hand would; because the choice is persisted with the window, it also becomes the window's
template on the next launch. Omitting templateId is bit-for-bit today's behaviour (the app's
Default view).
Errors: a malformed body → 400 Malformed request body.; a missing
windowType → 400 with the supported list; an unknown or
unsupported-standalone type (e.g. settings, which is modal, or the full-screen chart,
which needs an existing chart) → 400 naming the supported list; an unknown screener
templateId → 404 Screener template {templateId} not found — all
validated before any window is opened.
Closes ONE terminal window through the app's OWN close path (the same path a manual X uses, so it
inherits today's close-confirmation-popup behaviour). {windowId} is the id WITHIN the surface
named by windowType — ids are not unique across surfaces (a workspace,
a chart, a screener and a watchlist can all be id 1), so the body's windowType is what routes
the id to the correct window. The singleton surfaces (lineNotifications, finres,
userTrades, listing) ignore the path id. finres is HIDDEN rather
than destroyed, matching its manual toggle.
Request body:
{ "windowType": "workspace" }
| Field | Type | Required | Description |
|---|---|---|---|
windowType | string | yes | One of: workspace, lightWorkspace, chart, tradingViewChart, watchlist, screener, lineNotifications, finres, userTrades, listing. |
Response { windowType, windowId, closed, outcome }.
The main workspace can never be closed through the API — it would quit the
terminal. A windowId that resolves to the main workspace → 400 The main workspace
cannot be closed through the API. (+ supported list).
Errors: a non-numeric {windowId} → 400 Invalid window ID;
a malformed body → 400 Malformed request body.; a missing / unknown / unsupported
windowType → 400 naming the supported list; an unknown window id →
404 — all validated before any window is closed.
Brings ONE terminal window to the front on its own window thread. Same id-addressing as the close
route: {windowId} is the id within the surface named by windowType; singletons
ignore the id.
Request body:
{ "windowType": "chart" }
| Field | Type | Required | Description |
|---|---|---|---|
windowType | string | yes | One of the ten window types (see close). |
Response { windowType, windowId, activated, outcome }.
Errors: a non-numeric {windowId} → 400 Invalid window ID;
a malformed body → 400; a missing / unknown / unsupported windowType →
400 naming the supported list; the main workspace → 400; an unknown window
id → 404.
Re-points one addressed panel (order book or chart) to a different market.
This targets only that panel — unlike POST /api/change-ticker,
which drives the active window and a whole named link group. {externalId} is the
document's GUID.
Request body:
{ "ticker": "ETHUSDT", "connectionId": 77 }
| Field | Type | Required | Description |
|---|---|---|---|
ticker | string | yes | The instrument to switch the panel to. |
connectionId | integer | no | Target connection. Omit to keep the panel's current connection (a plain re-ticker on the same venue). |
Asynchronous w.r.t. the UI thread. The API resolves the document and dispatches
a change-ticker action to that panel's own live listener, which re-resolves the ticker and
re-tickers itself on its own window dispatcher; the visible change and its persistence happen
inside the panel. Success → 200 { "status": "ok" }.
Errors: a non-GUID externalId → 400 Invalid document ID;
a missing ticker → 400; an unknown document →
404 Document {externalId} not found; a document with no current connection and no
supplied connectionId → 400.
Opening/closing a panel and switching the active window or tab are not yet available. They require calling MetaScalp's own window-thread methods, and the local API layer has no reachable window‑id → dispatcher lookup to marshal onto (it does not reference the WPF UI project). Faking them by writing the database directly would diverge the on-screen layout from the saved model, so they are deliberately deferred to a dedicated follow-up rather than shipped fragile. The following routes therefore do not exist yet:
| Route | Intent |
|---|---|
POST /api/ui/windows/{windowId}/order-books | Open an order book in a window's active tab |
POST /api/ui/windows/{windowId}/charts | Open a chart in a window's active tab |
DELETE /api/ui/documents/{externalId} | Close one addressed panel (with its DB delete) |
POST /api/ui/tabs/{tabId}/activate | Activate a tab |
Connect via WebSocket to receive real-time updates for your exchange connections. Subscribe by connection ID for order, position, balance, and finres events. Subscribe by connection ID + ticker for real-time trade and order book market data.
The WebSocket server shares the same port as the HTTP API (17845–17855). Connect to ws://127.0.0.1:{port}/.
All messages (inbound and outbound) are JSON with this envelope:
{ "Type": "message_type", "Data": { ... } }
Connection-level — subscribe by connection ID for order, position, balance, and finres updates:
| Type | Data | Description |
|---|---|---|
subscribe | { "connectionId": 123 } | Subscribe to updates for a connection. Connection must be active. Idempotent. |
unsubscribe | { "connectionId": 123 } | Stop receiving updates for a connection. Idempotent. |
Market data — subscribe by connection ID + ticker for trade or order book updates:
| Type | Data | Description |
|---|---|---|
trade_subscribe | { "connectionId": 123, "ticker": "BTCUSDT", "zoomIndex": 1 } | Subscribe to real-time trade updates for a specific ticker. Optional zoomIndex — when > 1, trades are aggregated by zoomed price level. Idempotent — re-subscribing updates zoomIndex. |
trade_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving trade updates for that ticker. Idempotent. |
orderbook_subscribe | { "connectionId": 123, "ticker": "BTCUSDT", "zoomIndex": 0, "depthLevels": 50, "depthPercent": 0.5, "fetchSnapshot": true } | Subscribe to order book updates (snapshot + incremental). Idempotent — re-subscribing replaces depth params. Optional zoomIndex (aggregation), depthLevels (top-N per side, snapshot only), depthPercent (per-side band on best ask / best bid, snapshot + updates). Optional fetchSnapshot (default true) — when false AND this is the first subscriber for the ticker, the exchange REST snapshot fetch is skipped, so you receive only delta updates (useful for mass-subscribing to many tickers without hitting exchange REST rate limits; seed state separately via GET /api/connections/{id}/orderbook-snapshot). |
orderbook_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving order book updates for that ticker. Idempotent. |
mark_price_subscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Subscribe to mark price updates. No initial snapshot — only live updates. Idempotent. |
mark_price_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving mark price updates for that ticker. Idempotent. |
index_price_subscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Subscribe to index price updates (futures only). No initial snapshot — only live updates. Idempotent. |
index_price_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving index price updates for that ticker. Idempotent. |
funding_subscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Subscribe to funding rate updates (perpetual futures only). No initial snapshot. Idempotent. |
funding_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving funding updates for that ticker. Idempotent. |
Acknowledgements:
| Type | When | Data fields |
|---|---|---|
subscribed | After successful connection subscribe | connectionId |
unsubscribed | After successful connection unsubscribe | connectionId |
trade_subscribed | After successful trade subscribe | connectionId, ticker, zoomIndex |
trade_unsubscribed | After successful trade unsubscribe | connectionId, ticker |
orderbook_subscribed | After successful order book subscribe | connectionId, ticker, zoomIndex; depthLevels / depthPercent echoed when non-null, fetchSnapshot only when false |
orderbook_unsubscribed | After successful order book unsubscribe | connectionId, ticker |
mark_price_subscribed | After successful mark price subscribe | connectionId, ticker |
mark_price_unsubscribed | After successful mark price unsubscribe | connectionId, ticker |
index_price_subscribed | After successful index price subscribe | connectionId, ticker |
index_price_unsubscribed | After successful index price unsubscribe | connectionId, ticker |
funding_subscribed | After successful funding subscribe | connectionId, ticker |
funding_unsubscribed | After successful funding unsubscribe | connectionId, ticker |
error | Invalid message, bad connection ID, or missing ticker | error (string) |
Connection-level updates:
| Type | When | Data fields |
|---|---|---|
order_update | Order created/changed/filled/cancelled | connectionId, orderId, ticker, side, type, price, filledPrice, size, filledSize, fee, feeCurrency, status, time |
position_update | Position opened/changed/closed | connectionId, positionId, ticker, side, size, avgPrice, avgPriceFix, avgPriceDyn, status |
balance_update | Account balance changed | connectionId, balances[] with coin, total, free, locked |
finres_update | Financial results updated | connectionId, finreses[] with currency, result, fee, funds, available, blocked |
Market data updates:
| Type | When | Data fields |
|---|---|---|
trade_update | New trades for subscribed ticker (server-side aggregated — see note below) | connectionId, ticker, trades[] with price, size, side, time, highPrice, lowPrice |
orderbook_snapshot | Full order book on subscribe | connectionId, ticker, asks[], bids[], bestAsk, bestBid — each with price, size, type |
orderbook_update | Incremental order book changes | connectionId, ticker, updates[] with price, size, type |
mark_price_update | Mark price changed (futures only) | connectionId, ticker, markPrice |
index_price_update | Index price changed (futures only) | connectionId, ticker, indexPrice |
funding_update | Funding rate / time changed (perpetual futures only) | connectionId, ticker, fundingRate, fundingTime (ISO 8601) |
Trade aggregation: trade_update entries are aggregated server-side using the order book's AddingTicksForAPeriod setting (same value that drives the UI ticks section, default 200 ms; per-(connection, ticker)). Consecutive same-side trades within the window are merged into one entry — size is summed, price and time track the latest merged trade. A new entry is emitted when the side changes or the window expires. Each entry also carries highPrice and lowPrice — the highest and lowest price among the trades merged into that entry (the range the price travelled inside the tick), while price stays the latest merged trade. With AddingTicksForAPeriod = 0 aggregation is disabled and every entry has highPrice == lowPrice == price. Changes are picked up live by active subscriptions.
Signal levels — subscribe (globally, no data payload) to receive signal-level lifecycle events:
| Type | Direction | When | Data fields |
|---|---|---|---|
signal_level_subscribe | client → server | Start receiving signal-level events | — |
signal_level_unsubscribe | client → server | Stop receiving them | — |
signal_level_subscribed | server → client | Subscribe acknowledged (a signal_levels_snapshot follows immediately) | — |
signal_level_unsubscribed | server → client | Unsubscribe acknowledged | — |
signal_levels_snapshot | server → client | Current levels on subscribe | signalLevels[] with id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule, and (MC-421) note, lineThickness, lineStyle, lineColor, textSize, textColor, textAlignment, textStyle (null when unset) |
signal_level_placed | server → client | A level was created | id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule, and the same MC-421 note + appearance fields |
signal_level_updated | server → client | A level was modified in place (price and/or rule changed, identity preserved) | Same shape as signal_level_placed |
signal_level_triggered | server → client | A level fired | id, triggerTime |
signal_level_removed | server → client | A level was deleted | id |
signal_levels_removed_all | server → client | All levels (for a ticker or globally) were deleted | — |
signal_levels_removed_triggered | server → client | All triggered levels were cleared | — |
User levels — subscribe (globally, no data payload) to receive user-level lifecycle events. date is a nullable ISO-8601 timestamp:
| Type | Direction | When | Data fields |
|---|---|---|---|
user_level_subscribe | client → server | Start receiving user-level events | — |
user_level_unsubscribe | client → server | Stop receiving them | — |
user_level_subscribed | server → client | Subscribe acknowledged | — |
user_level_unsubscribed | server → client | Unsubscribe acknowledged | — |
user_levels_snapshot | server → client | Current levels on subscribe | userLevels[] with id, connectionId, ticker, price, name, note, date, and (MC-421) lineThickness, lineStyle, lineColor, textSize, textColor, textAlignment, textStyle (null when unset). note is a real stored field, distinct from name |
user_level_placed | server → client | A level was created | id, connectionId, ticker, price, name, note, date, and the same MC-421 appearance fields |
user_level_updated | server → client | A level was modified in place (identity preserved) | Same shape as user_level_placed |
user_level_removed | server → client | One or more levels were deleted (covers DELETE {id} and DELETE ?Ticker=) — removal is a batch, so ids are carried as an array | ids[] |
user_levels_removed_all | server → client | All levels (globally) were cleared | — |
Notifications — subscribe (globally, no data payload) to receive the notification feed (the «Line notifications» window). On subscribe the server acknowledges with notification_subscribed and pushes a one-shot notification_snapshot of the recent feed (inside the 200-row cap); after that every new row — native or injected via POST /api/notifications — is delivered live as notification_update. Without this subscribe a client never receives the notification_update the Notifications section documents:
| Type | Direction | When | Data fields |
|---|---|---|---|
notification_subscribe | client → server | Start receiving notification-feed events | — |
notification_unsubscribe | client → server | Stop receiving them | — |
notification_subscribed | server → client | Subscribe acknowledged (a notification_snapshot follows immediately) | — |
notification_unsubscribed | server → client | Unsubscribe acknowledged | — |
notification_snapshot | server → client | The recent feed on subscribe (one-shot, within the 200-row cap) | notifications[] — each with connectionId, type, ticker, size, price, time and the feed cells |
notification_update | server → client | A new notification was added to the feed (covers API-injected rows) | notifications[] — same shape as the snapshot rows |
Chart annotations — subscribe per (connectionId, ticker) to receive the current shapes. This family delivers a subscribe/unsubscribe ack and a one-shot snapshot only — there is no annotations_updated push. The annotation service exposes no change-notification / listener mechanism, so live deltas cannot be delivered without inventing one; a client that needs fresh state after an edit re-subscribes or re-issues GET .../annotations. Datetime fields are ISO-8601.
⚠ Snapshot prices are venue prices (from the Step 14 build). The y / y1 / y2 in annotations_snapshot are the instrument's real market price (e.g. 0.2 for ADAUSDT) — identical to what GET .../annotations returns — converted per (connectionId, ticker) from that instrument's own price increment. Earlier builds passed these fields through unconverted, exposing the chart's internal axis units (×104 for ADAUSDT), so a hand-drawn ray read back at ≈1999.79 on the socket while REST already reported 0.2; the two channels now agree. The REST-side conversion shipped earlier in 1.0.2835-MC-400; the socket snapshot converts as of the Step 14 build. Timestamp fields (x1 / x2) are unchanged. If the Ticker is unknown on the connection or its price increment is ≤ 0, the server emits an error frame instead of the snapshot (a socket cannot answer 400) — it never silently returns raw axis units.
| Type | Direction | When | Data fields |
|---|---|---|---|
annotation_subscribe | client → server | Start receiving annotations for one (connectionId, ticker) | connectionId, ticker |
annotation_unsubscribe | client → server | Stop receiving them for that (connectionId, ticker) | connectionId, ticker |
annotation_subscribed | server → client | Subscribe acknowledged (an annotations_snapshot follows immediately) | connectionId, ticker |
annotation_unsubscribed | server → client | Unsubscribe acknowledged | connectionId, ticker |
annotations_snapshot | server → client | The current shapes on subscribe (one-shot; not re-pushed on change) | connectionId, ticker, lineAnnotations[] (index, x1, x2, y1 (venue price), y2 (venue price)), horizontalLineAnnotations[] (index, y (venue price)), horizontalRayAnnotations[] (index, y (venue price), x1) |
UI change events — subscribe (globally, no data payload) to observe the open UI. On subscribe the server acknowledges with ui_subscribed and immediately pushes a ui_snapshot whose Data is identical to GET /api/ui/state (same camelCase shape). After that, a ui_update is pushed per observed change. Read-only — the server never opens, closes or mutates any UI element here:
| Type | Direction | When | Data fields |
|---|---|---|---|
ui_subscribe | client → server | Start receiving UI change events (send "data": {}) | — |
ui_unsubscribe | client → server | Stop receiving them | — |
ui_subscribed | server → client | Subscribe acknowledged (a ui_snapshot follows immediately) | — |
ui_unsubscribed | server → client | Unsubscribe acknowledged | — |
ui_snapshot | server → client | The whole open UI on subscribe | Same shape as GET /api/ui/state: windows[], standaloneCharts[] |
ui_update | server → client | One observed UI change | kind plus a kind-specific payload — see below |
ui_update payloads by kind:
kind | When | Data fields |
|---|---|---|
tabAdded | A tab was added to a window | kind, windowId, window (the full window subtree, same shape as one entry of state.windows[]) |
documentOpened | A chart document was opened | kind, documentId, externalId, connectionId (null when unbound), ticker, document (same shape as a state document, kind: "chart") |
Coverage — MetaScalp.Api observes only Application-layer events (it does not reference the WPF UI project), so this rung delivers exactly the two ui_update kinds above. The following changes are not emitted yet (no reachable Application-layer event publishes them today, and no polling is used): order-book document opened, ticker changed, window opened/closed, tab removed/activated, document closed/moved, and link-number changes. Clients that need the current state after such a change can re-request GET /api/ui/state.
| Event | Behavior |
|---|---|
| Client connects | Session created. No updates until subscribe. |
| Subscribe (valid ID) | Server responds with subscribed. Order, position, balance, finres updates start flowing. |
| Subscribe (invalid ID) | Server responds with error. No subscription created. |
| Subscribe (already subscribed) | Idempotent — responds with confirmation, no duplicate events. |
| Trade/orderbook subscribe (valid) | Server responds with trade_subscribed / orderbook_subscribed. Market data starts flowing for that ticker. |
| Trade/orderbook subscribe (invalid) | Server responds with error (bad connection ID or missing ticker). |
| Mark price / funding subscribe (valid) | Server responds with mark_price_subscribed / funding_subscribed. Updates flow as the exchange publishes them (no initial snapshot). |
| Mark price / funding subscribe (invalid) | Server responds with error (bad connection ID or missing ticker). |
| Unsubscribe | Server responds with confirmation. Updates stop for that subscription. |
| Client disconnects | All subscriptions (connection-level and market data) are cleaned up automatically. |
| Multiple connections | A single client can subscribe to multiple connection IDs and multiple tickers simultaneously. |
// Subscribe
{ "Type": "subscribe", "Data": { "connectionId": 1 } }
// Server confirms
{ "Type": "subscribed", "Data": { "connectionId": 1 } }
// Order update pushed by server
{
"Type": "order_update",
"Data": {
"connectionId": 1,
"orderId": 98765,
"ticker": "BTCUSDT",
"side": "Buy",
"type": "Limit",
"price": 65000.0,
"filledPrice": 0.0,
"size": 0.01,
"filledSize": 0.0,
"fee": 0.0,
"feeCurrency": "USDT",
"status": "New",
"time": "2026-03-13T10:30:00+00:00"
}
}
// Position update pushed by server
{
"Type": "position_update",
"Data": {
"connectionId": 1,
"positionId": 4321,
"ticker": "ETHUSDT",
"side": "Buy",
"size": 1.5,
"avgPrice": 3200.00,
"avgPriceFix": 3200.00,
"avgPriceDyn": 3200.00,
"status": "Open"
}
}
// Balance update pushed by server
{
"Type": "balance_update",
"Data": {
"connectionId": 1,
"balances": [
{ "coin": "USDT", "total": 10000.0, "free": 8500.0, "locked": 1500.0 },
{ "coin": "BTC", "total": 0.5, "free": 0.5, "locked": 0.0 }
]
}
}
// FinRes update pushed by server
{
"Type": "finres_update",
"Data": {
"connectionId": 1,
"finreses": [
{ "currency": "USDT", "result": 250.50, "fee": 12.30, "funds": 10000.0, "available": 8500.0, "blocked": 1500.0 },
{ "currency": "BTC", "result": 0.005, "fee": 0.0001, "funds": 0.5, "available": 0.5, "blocked": 0.0 }
]
}
}
// Subscribe to trades for a specific ticker (zoomIndex is optional)
{ "Type": "trade_subscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "zoomIndex": 1 } }
// Server confirms trade subscription
{ "Type": "trade_subscribed", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "zoomIndex": 1 } }
// Trade update pushed by server
{
"Type": "trade_update",
"Data": {
"connectionId": 1,
"ticker": "BTCUSDT",
"trades": [
{ "price": 65123.50, "size": 0.15, "side": "Buy", "time": "2026-03-16T12:00:01.234+00:00", "highPrice": 65124.00, "lowPrice": 65123.00 },
{ "price": 65123.00, "size": 0.03, "side": "Sell", "time": "2026-03-16T12:00:01.235+00:00", "highPrice": 65123.00, "lowPrice": 65123.00 }
]
}
}
// Subscribe to order book for a specific ticker (zoomIndex / depthLevels / depthPercent / fetchSnapshot are optional; fetchSnapshot defaults to true)
{ "Type": "orderbook_subscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "zoomIndex": 0, "depthLevels": 50, "depthPercent": 0.5, "fetchSnapshot": true } }
// Server confirms order book subscription (echoes any non-null depth params; fetchSnapshot only when false)
{ "Type": "orderbook_subscribed", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "zoomIndex": 0, "depthLevels": 50, "depthPercent": 0.5 } }
// Order book snapshot (full state, sent once on subscribe)
{
"Type": "orderbook_snapshot",
"Data": {
"connectionId": 1,
"ticker": "BTCUSDT",
"asks": [
{ "price": 65124.00, "size": 1.20, "type": "Ask" },
{ "price": 65125.00, "size": 0.85, "type": "Ask" }
],
"bids": [
{ "price": 65123.00, "size": 2.50, "type": "Bid" },
{ "price": 65122.00, "size": 1.10, "type": "Bid" }
],
"bestAsk": { "price": 65124.00, "size": 1.20, "type": "BestAsk" },
"bestBid": { "price": 65123.00, "size": 2.50, "type": "BestBid" }
}
}
// Order book incremental update
{
"Type": "orderbook_update",
"Data": {
"connectionId": 1,
"ticker": "BTCUSDT",
"updates": [
{ "price": 65124.00, "size": 0.90, "type": "Ask" },
{ "price": 65126.00, "size": 0.50, "type": "Ask" },
{ "price": 65123.00, "size": 2.80, "type": "Bid" }
]
}
}
// Subscribe to mark price for a specific ticker
{ "Type": "mark_price_subscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
// Server confirms mark price subscription
{ "Type": "mark_price_subscribed", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
// Mark price update pushed by server
{ "Type": "mark_price_update", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "markPrice": 65123.5 } }
// Subscribe to funding for a specific ticker (perpetual futures only)
{ "Type": "funding_subscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
// Server confirms funding subscription
{ "Type": "funding_subscribed", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
// Funding update pushed by server
{ "Type": "funding_update", "Data": { "connectionId": 1, "ticker": "BTCUSDT", "fundingRate": 0.0001, "fundingTime": "2026-03-16T16:00:00+00:00" } }
// Unsubscribe from market data
{ "Type": "trade_unsubscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
{ "Type": "orderbook_unsubscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
{ "Type": "mark_price_unsubscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
{ "Type": "funding_unsubscribe", "Data": { "connectionId": 1, "ticker": "BTCUSDT" } }
// Unsubscribe from connection
{ "Type": "unsubscribe", "Data": { "connectionId": 1 } }
// Error (invalid connection)
{ "Type": "error", "Data": { "error": "Connection 999 not found or not active" } }
| Value | Exchange |
|---|---|
| 2 | Binance |
| 3 | GateIo |
| 5 | KuCoin |
| 6 | Bybit |
| 7 | Bitget |
| 8 | Mexc |
| 10 | Okx |
| 11 | BingX |
| 12 | HTX |
| 13 | BitMart |
| 14 | LBank |
| 15 | HyperLiquid |
| 16 | UpBit |
| 17 | AsterDex |
| 18 | Moex |
| 19 | Lighter |
| Value | Type | Description |
|---|---|---|
| 0 | Spot | Spot trading |
| 1 | Futures | Generic futures |
| 2 | UsdtFutures | USDT-margined futures |
| 3 | CoinFutures | Coin-margined futures |
| 4 | InverseFutures | Inverse futures |
| 5 | UsdtPerpetual | USDT perpetual swaps |
| 6 | UsdcPerpetual | USDC perpetual swaps |
| 7 | Margin | Margin (cross/isolated) |
| 8 | Options | Options contracts |
| 9 | Stock | Stock / equity markets |
.p suffix auto-resolves to the right futures type. For explicit fields, 2 (UsdtFutures) is the most common choice for perpetual futures.Errors return HTTP 400 or 404 with a JSON body.
Request body parsing (MC-421 Step 15, corrected in Step 16) — applies to every endpoint with a JSON body:
a body that is not valid JSON returns 400 immediately. A body carrying an
unknown property is rejected with 400 naming the offending
field (previously the unknown property was silently discarded and the request answered 200).
Two endpoint families answer with their own fixed wording instead of the shared text and are unchanged:
/api/ui/windows/{windowId}/close and /activate answer
Malformed request body., and the screener-template routes answer
Request body is not valid JSON..
⚠ Breaking change for clients that send extra fields.
Before this change an unrecognised property was accepted and dropped. It is now a 400, on
every body endpoint including POST /api/connections/{id}/orders and the cancel
routes — so an SDK or script that sends a field this reference does not list will stop working and
no order will be placed. Send only the documented fields.
Read-modify-write is supported. The identity and server-owned fields that a
GET emits are accepted and ignored on the matching PUT/POST,
so a client can send back the object it just read: id, connectionId,
ticker on user levels; those plus isTriggered and triggerTime on
signal levels; id on screener templates; index on all three annotation types.
A value supplied in one of these fields never re-keys, moves or re-triggers anything — the route
parameters remain authoritative. Two known exceptions that are not round-trippable: a user
level whose date is non-null (GET emits ISO-8601, PUT takes raw
epoch seconds — a deliberate asymmetry that predates this change), and the order-book settings
PUT, which takes the bare settings object rather than the {connectionId, ticker,
settings} envelope that GET returns.
| Endpoint | Condition | HTTP | Error message |
|---|---|---|---|
(any endpoint with a JSON body) | Body is not valid JSON | 400 | Malformed request body: not valid JSON. |
(any endpoint with a JSON body) | Unknown property in body | 400 | Unknown field '{field}' in request body. |
/api/change-ticker | Missing fields | 400 | Invalid request body. Provide 'tickerPattern' or 'exchange'+'market'+'ticker'. |
/api/change-ticker | Invalid pattern | 400 | Invalid ticker pattern: '{pattern}' |
/api/change-ticker | Binding not found | 400 | Binding '{name}' not found. Available: {list} |
/api/change-ticker | No connection | 400 | No connection found for exchange ... and market ... |
/api/change-ticker | Ticker not on connection | 400 | Ticker '{ticker}' not found on connection {id} |
/api/combo | Missing ticker | 400 | Invalid request body. 'ticker' is required. |
/api/connections/{id} | Connection not found | 404 | Connection {id} not found |
/api/connections/{id}/... | Invalid connection ID | 400 | Invalid connection ID |
/api/connections/{id}/... | Connection not found | 404 | Connection {id} not found |
/api/connections/{id}/... | Connection not active | 400 | Connection {id} is not active |
/api/connections/{id}/orders | Missing ticker query param | 400 | Query parameter 'Ticker' is required |
/api/connections/{id}/orders | Invalid order fields | 400 | Invalid request body. 'ticker', 'side', 'price', and 'size' are required. |
/api/connections/{id}/orders | Size <= 0 | 400 | Size must be greater than zero |
/api/connections/{id}/orders | Price <= 0 (non-market) | 400 | Price must be greater than zero for non-market orders |
/api/connections/{id}/orders/cancel | Missing fields | 400 | Invalid request body. 'ticker' and 'orderId' are required. |
POST /api/connections/{id}/signal-levels | Missing ticker/price | 400 | Invalid request body. 'ticker' and 'price' are required. |
POST/PUT .../signal-levels | Price <= 0 | 400 | Price must be greater than zero |
POST/PUT .../signal-levels | Unknown triggerRule | 400 | Invalid 'triggerRule'. Allowed values: LessThanEqual, GreaterThanEqual. |
POST /api/connections/{id}/signal-levels | No order book data | 400 | No market data for '{ticker}'. Subscribe to order book data for this ticker first. |
PUT .../signal-levels/{signalLevelId} | Invalid signal level ID | 400 | Invalid signal level ID |
PUT .../signal-levels/{signalLevelId} | Empty body / all fields null | 400 | Request body must set at least one of 'price' or 'triggerRule'. |
PUT .../signal-levels/{signalLevelId} | Level not found | 404 | Signal level {id} not found |
POST /api/connections/{id}/user-levels | Missing ticker/price | 400 | Invalid request body. 'ticker' and 'price' are required. |
POST/PUT .../user-levels | Price <= 0 | 400 | Price must be greater than zero |
PUT/DELETE .../user-levels/{userLevelId} | Invalid user level ID | 400 | Invalid user level ID |
PUT .../user-levels/{userLevelId} | Empty body / all fields null | 400 | Request body must set at least one of 'price', 'name', 'date', 'note' or an appearance field. |
PUT .../user-levels/{userLevelId} | Level not found | 404 | User level {id} not found |
GET/DELETE .../user-levels | Missing Ticker query | 400 | Query parameter 'Ticker' is required |
POST /api/notifications | Unknown connectionId | 404 | Connection {id} not found |
GET /api/ui/windows/{windowId} | Non-numeric window id | 400 | Invalid window ID |
GET /api/ui/windows/{windowId} | Unknown window | 404 | Window {id} not found |
POST /api/ui/windows | Malformed body | 400 | Malformed request body. |
POST /api/ui/windows | Missing windowType | 400 | POST /api/ui/windows requires 'windowType' in the body to know which window to open. (+ supported list) |
POST /api/ui/windows/{windowId}/close|activate | Missing windowType | 400 | POST /api/ui/windows/{windowId}/close|activate requires 'windowType' in the body: window ids are per-surface, so the type is what routes the id to the right window. (+ supported list) |
POST /api/ui/windows | Unknown / unsupported-standalone type | 400 | Unsupported windowType '…'. (+ supported list) |
POST /api/ui/windows | Unknown screener templateId | 404 | Screener template {templateId} not found |
PUT /api/ui/documents/{externalId}/link-number | Non-GUID document id | 400 | Invalid document ID |
PUT /api/ui/documents/{externalId}/link-number | Missing linkNumber | 400 | Request body must set 'linkNumber'. |
PUT /api/ui/documents/{externalId}/link-number | Non-positive linkNumber | 400 | linkNumber must be a positive binding-group id (1-500, or 9999 to cancel). |
PUT /api/ui/documents/{externalId}/link-number | Unknown document | 404 | Document {externalId} not found |
PUT /api/ui/documents/{externalId}/ticker | Non-GUID document id | 400 | Invalid document ID |
PUT /api/ui/documents/{externalId}/ticker | Missing ticker | 400 | Request body must set 'ticker'. |
PUT /api/ui/documents/{externalId}/ticker | No connection to inherit | 400 | connectionId is required: the document has no current connection to inherit. |
PUT /api/ui/documents/{externalId}/ticker | Unknown document | 404 | Document {externalId} not found |
| any | Unknown path | 400 | Not found |
| any | Wrong HTTP method | 400 | Method not allowed |
Socket errors are sent as { "Type": "error", "Data": { "error": "..." } }
| Condition | Error message |
|---|---|
| Subscribe to non-existent connection | Connection {id} not found or not active |
| Trade/orderbook subscribe with missing ticker | Ticker is required for trade subscription / Ticker is required for order book subscription |
| Trade/orderbook subscribe with invalid connection | Connection {id} not found or not active |
| Mark price / funding subscribe with missing ticker | Ticker is required for mark price subscription / Ticker is required for funding subscription |
| Mark price / funding subscribe with invalid connection | Connection {id} not found or not active |
| Unknown message type | Unknown message type: {type} |
| Invalid JSON | Invalid message format |
MetaScalp exposes a local API that lets your scripts and bots interact with connected exchanges. Use HTTP REST endpoints for request/response operations and WebSocket for real-time streaming of orders, positions, balances, trades, and order book data.
127.0.0.1 (localhost only)17845–17855 (first available)application/jsonAccess-Control-Allow-Origin: *.Use HTTP to discover connections, query data, and execute trades:
| Endpoint | Purpose |
|---|---|
GET /ping | Find the running MetaScalp instance and check its version |
POST /api/change-ticker | Switch the active ticker in the MetaScalp UI |
POST /api/combo | Open a combo layout for a ticker |
GET /api/connections | List all active exchange connections |
GET /api/connections/{id}/... | Query tickers, orders, positions, balances for a connection |
POST /api/connections/{id}/orders | Place or cancel orders on a connection |
POST /api/connections/{connectionId}/orders/cancel-all | Cancel all open orders for a ticker on a connection (body: { ticker }) → { status, cancelledCount } |
GET /api/connections/{connectionId}/orderbook-snapshot | REST order-book snapshot for a ticker (optional DepthLevels / DepthPercent; 501 when the exchange has no REST snapshot) |
GET /api/link-groups/{groupId}/orderbook-snapshots | Read-only order books of every panel currently in link group N (live membership; per-entry reason on partial failure) |
GET/PUT /api/connections/{connectionId}/orderbook-settings | Read, and partially update (every field optional), the order-book settings set for a ticker — changes are pushed live to the running panels. Working volumes are stored as paired USD/coin mirrors (tradeAmountUsd1..5 ↔ tradeAmount1..5): writing one side recomputes the other at the live best-ask (truncated to the ticker's size increment, floored at its min size), so the terminal's own volume buttons show USD and coin figures that agree with the current price. Writing both sides of a pair keeps the values you sent. If no order book is open for the connection+ticker there is no live price → 409 (open the order book so the conversion has a live price); unknown ticker → 404; nothing is written in either case. |
GET /api/connections/{connectionId}/cluster-snapshot | Remote cluster (footprint) snapshot for a ticker at a timeframe (optional ZoomIndex) |
GET/POST/PUT/DELETE /api/connections/{id}/signal-levels | Read, create, modify (in place) and delete signal levels for a ticker |
DELETE /api/signal-levels/triggered | Remove all triggered signal levels (across every connection) |
GET/POST/PUT/DELETE /api/connections/{id}/user-levels | Read, create, modify (in place) and delete user (plain) levels for a ticker |
GET/PUT/POST/DELETE /api/connections/{id}/annotations | Read all three chart-annotation lists, replace a list (PUT), append one (POST) or delete by index / clear all — addressed by zero-based list index |
POST /api/notifications | Inject a custom row into the notification feed (all fields optional, arbitrary event type) |
GET /api/ui/state | Full read-only inventory of the open UI: windows, tabs, order-book/chart documents and standalone charts (rebuilt from the saved layout) |
GET /api/ui/windows/{windowId} | The same inventory object for one window; unknown id → 404 |
POST /api/ui/windows | Open a terminal window at a requested place/size (and monitor); omitted geometry → today's default placement |
PUT /api/ui/documents/{externalId}/link-number | Set one panel's link (binding-group) number; unknown document → 404 |
PUT /api/ui/documents/{externalId}/ticker | Re-point one addressed panel to a different market (that panel only) |
Connect via WebSocket to receive real-time updates for your exchange connections.
ws://127.0.0.1:{port}/ (same port as HTTP — scan 17845–17855)subscribe with a connection ID to receive order, position, balance, and finres updatestrade_subscribe, orderbook_subscribe, mark_price_subscribe, or funding_subscribe with connection ID + ticker to receive trade, order book, mark price, or funding updates1. Launch MetaScalp — both HTTP and WebSocket servers start automatically.
2. Discover the port — scan 17845–17855 with GET /ping.
3. List connections — call GET /api/connections to see available exchange connections.
4. Execute operations — use a connection ID for REST queries or WebSocket subscriptions.
GET /ping → find MetaScalp
GET /api/connections → list active connections
GET /api/connections/{id}/tickers → get available tickers
GET /api/connections/{id}/balance → check balances
GET /api/connections/{id}/orders?Ticker=BTCUSDT → view open orders
POST /api/connections/{id}/orders → place an order
POST /api/connections/{id}/orders/cancel → cancel an order
1. Connect
ws = new WebSocket("ws://127.0.0.1:17845/")
2. Subscribe to a connection (orders, positions, balances, finres)
ws.send('{"Type":"subscribe","Data":{"connectionId":1}}')
← {"Type":"subscribed","Data":{"connectionId":1}}
3. Subscribe to market data for a specific ticker
ws.send('{"Type":"trade_subscribe","Data":{"connectionId":1,"ticker":"BTCUSDT"}}')
← {"Type":"trade_subscribed","Data":{"connectionId":1,"ticker":"BTCUSDT"}}
ws.send('{"Type":"orderbook_subscribe","Data":{"connectionId":1,"ticker":"BTCUSDT"}}')
← {"Type":"orderbook_subscribed","Data":{"connectionId":1,"ticker":"BTCUSDT"}}
4. Receive real-time updates
← {"Type":"order_update","Data":{"connectionId":1,"orderId":123,...}}
← {"Type":"position_update","Data":{"connectionId":1,...}}
← {"Type":"balance_update","Data":{"connectionId":1,"balances":[...]}}
← {"Type":"finres_update","Data":{"connectionId":1,"finreses":[...]}}
← {"Type":"trade_update","Data":{"connectionId":1,"ticker":"BTCUSDT","trades":[...]}}
← {"Type":"orderbook_snapshot","Data":{"connectionId":1,"ticker":"BTCUSDT","asks":[...],"bids":[...],...}}
← {"Type":"orderbook_update","Data":{"connectionId":1,"ticker":"BTCUSDT","updates":[...]}}
5. Unsubscribe when done
ws.send('{"Type":"trade_unsubscribe","Data":{"connectionId":1,"ticker":"BTCUSDT"}}')
ws.send('{"Type":"orderbook_unsubscribe","Data":{"connectionId":1,"ticker":"BTCUSDT"}}')
ws.send('{"Type":"unsubscribe","Data":{"connectionId":1}}')
← {"Type":"unsubscribed","Data":{"connectionId":1}}
| Type | Data | Description |
|---|---|---|
subscribe | { "connectionId": 123 } | Start receiving order, position, balance, finres updates for a connection. |
unsubscribe | { "connectionId": 123 } | Stop receiving connection-level updates. |
trade_subscribe | { "connectionId": 123, "ticker": "BTCUSDT", "zoomIndex": 1 } | Start receiving trade updates for a specific ticker. Optional zoomIndex > 1 aggregates by zoomed price. |
trade_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving trade updates for that ticker. |
orderbook_subscribe | { "connectionId": 123, "ticker": "BTCUSDT", "fetchSnapshot": true } | Start receiving order book snapshot + incremental updates for a specific ticker. Optional zoomIndex, depthLevels, depthPercent; optional fetchSnapshot (default true) — set to false to skip the exchange REST snapshot fetch on cold subscribe, useful for mass-subscribing many tickers without hitting REST rate limits. |
orderbook_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving order book updates for that ticker. |
mark_price_subscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Start receiving mark price updates for a specific ticker (no initial snapshot). |
mark_price_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving mark price updates for that ticker. |
funding_subscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Start receiving funding rate updates (perpetual futures only). |
funding_unsubscribe | { "connectionId": 123, "ticker": "BTCUSDT" } | Stop receiving funding updates for that ticker. |
| Type | Data fields |
|---|---|
subscribed | connectionId |
unsubscribed | connectionId |
trade_subscribed | connectionId, ticker, zoomIndex |
trade_unsubscribed | connectionId, ticker |
orderbook_subscribed | connectionId, ticker, zoomIndex (+ depthLevels / depthPercent when non-null, fetchSnapshot when false) |
orderbook_unsubscribed | connectionId, ticker |
order_update | connectionId, orderId, ticker, side, type, price, filledPrice, size, filledSize, fee, feeCurrency, status, time |
position_update | connectionId, positionId, ticker, side, size, avgPrice, avgPriceFix, avgPriceDyn, status |
balance_update | connectionId, balances[]: coin, total, free, locked |
finres_update | connectionId, finreses[]: currency, result, fee, funds, available, blocked |
trade_update | connectionId, ticker, trades[]: price, size, side, time, highPrice, lowPrice |
orderbook_snapshot | connectionId, ticker, asks[], bids[], bestAsk, bestBid — each: price, size, type |
orderbook_update | connectionId, ticker, updates[]: price, size, type |
mark_price_subscribed | connectionId, ticker |
mark_price_unsubscribed | connectionId, ticker |
mark_price_update | connectionId, ticker, markPrice |
funding_subscribed | connectionId, ticker |
funding_unsubscribed | connectionId, ticker |
funding_update | connectionId, ticker, fundingRate, fundingTime (ISO 8601) |
signal_levels_snapshot | signalLevels[]: id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule |
signal_level_placed | id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule |
signal_level_updated | Same shape as signal_level_placed (level modified in place) |
signal_level_triggered | id, triggerTime |
signal_level_removed | id |
user_levels_snapshot | userLevels[]: id, connectionId, ticker, price, name, note, date |
user_level_placed | id, connectionId, ticker, price, name, note, date |
user_level_updated | Same shape as user_level_placed (level modified in place) |
user_level_removed | ids[] (batch removal) |
user_levels_removed_all | — |
annotation_subscribed | connectionId, ticker |
annotation_unsubscribed | connectionId, ticker |
annotations_snapshot | connectionId, ticker, lineAnnotations[]: index, x1, x2, y1, y2; horizontalLineAnnotations[]: index, y; horizontalRayAnnotations[]: index, y, x1 — y/y1/y2 are venue prices (same as GET; unresolvable ticker → error frame) (no annotations_updated — snapshot is one-shot) |
error | error (string message) |
For complete endpoint documentation with request/response details, field descriptions, and integration examples, see the interactive panels above or download the Markdown file.