MetaScalp API

SDK & Source metascalp.io
Searching...

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: 1784517855   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

Discovery
GET /ping Discover running instance

Scan ports 1784517855 to find the running MetaScalp instance. Returns the app name and version.

Ticker
POST /api/change-ticker Switch ticker in MetaScalp UI

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.

Format: EXCHANGE:SYMBOL.suffix — suffix: .p (futures), .m (margin), .o (options), omitted (spot)
Exchange and suffix are optional. Symbol may contain letters, digits, _, /, -, $, :.
Examples: BINANCE:BTCUSDT.p, BYBIT:ETHUSDT, OKX:BTC-USDT.m, HYPERLIQUID:cash:HOODUSDT0.p
Combo
POST /api/combo Open combo layout

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.

Connections
GET /api/connections List active exchange connections

Returns all currently active exchange connections. Use the id from the response to query orders, positions, balances, or to subscribe via WebSocket.

FieldTypeDescription
idintegerConnection ID — use for all exchange operations
namestringUser-defined connection name
exchangestringExchange name
exchangeIdintegerExchange identifier
marketstringMarket display name
marketTypeintegerMarket type enum value
stateinteger0=Disconnected, 1=Connecting, 2=Connected, 3=Reconnecting, 4=Resetting
viewModebooleanRead-only (no trading)
demoModebooleanPaper trading
GET /api/connections/{connectionId} Get single connection

Returns details for a single connection by ID.

Trading Operations
GET /api/connections/{connectionId}/tickers Get available tickers

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.

GET /api/connections/{connectionId}/orders?Ticker=... Get open orders

Returns open orders for a specific ticker on a connection. The Ticker query parameter is required.

FieldTypeDescription
idintegerExchange order ID
tickerstringTrading pair
clientIdstring?Client-generated order ID
sideinteger0 None, 1 Buy, 2 Sell
pricedecimalOrder price
sizedecimalOrder size
filledSizedecimalFilled amount
filledPricedecimalExecution price (0 if not yet filled)
remainingSizedecimalRemaining amount
statusinteger0 New, 1 Open, 2 Closed
typeinteger0 Limit, 1 Stop, 2 StopLoss, 3 TakeProfit, 4 Market
triggerPricedecimal?Trigger price for stop/conditional orders
createDatestring (ISO)Order creation timestamp
GET /api/connections/{connectionId}/leverage-limits?Ticker=... Get leverage limits

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.

FieldTypeDescription
connectionIdintegerConnection ID
tickerstringTrading pair
leveragedecimalThe leverage the max position is reported for — the supplied leverage query param if given, otherwise the venue's current leverage for this ticker
maxLeveragedecimal?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
maxPositiondecimal?Maximum position size available at the reported leverage (re-read per leverage). null means the venue reports no cap — never 0
QueryTypeDescription
TickerstringRequired. Trading pair.
LeveragedecimalOptional. Report the max position at this leverage instead of the venue's current leverage. Must be greater than 0.
GET /api/link-groups/{groupId}/orderbook-snapshots Order books of a link group

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 1500.

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.

FieldTypeDescription
groupIdintegerThe link group (binding) id, 1–500
membersarrayOne entry per panel currently live in the group (empty when the group has no live members)
members[].externalIdguidThe panel's document id
members[].kindstring?orderBook or chart (null when the document could not be resolved)
members[].connectionIdinteger?The panel's connection id (null when unset/unresolved)
members[].tickerstring?The panel's ticker (null when unset/unresolved)
members[].okbooleantrue when a snapshot was returned, else false
members[].bookobjectPresent only when ok: true. Same shape as orderbook-snapshot: updateId, asks, bids, bestAsk, bestBid
members[].reasonstringPresent only when ok: false. A machine-readable token (see below)
reason tokenMeaning
resolve_failedThe document could not be resolved from the layout
not_an_order_bookThe member is a chart (no order book to snapshot)
no_connectionThe panel has no connection set
no_tickerThe panel has no ticker set
connection_not_openThe panel's connection is not currently open
market_service_unavailableThe connection has no active market service
unsupported_exchangeThe exchange has no REST snapshot endpoint (e.g. the Bybit family)
snapshot_unavailableThe exchange returned no snapshot
snapshot_failedFetching the snapshot threw

Status codes: 200 (including an empty group); 400 for a non-numeric or out-of-range groupId.

GET /api/connections/{connectionId}/positions Get open positions

Returns all open positions on a connection (futures/margin markets).

GET /api/connections/{connectionId}/balance Get account balance

Returns account balances for all assets on a connection.

POST /api/connections/{connectionId}/orders Place order

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).

FieldTypeRequiredDescription
tickerstringyesTrading pair symbol
sideintegeryes1 Buy, 2 Sell
pricedecimalyes*Order price (*required for non-market orders)
sizedecimalyesOrder size (must be > 0)
typeintegerno0 Limit (default), 1 Stop, 2 StopLoss, 3 TakeProfit, 4 Market
reduceOnlybooleannoClose-only, default false
POST /api/connections/{connectionId}/orders/cancel Cancel order

Cancels an existing order on the exchange.

FieldTypeRequiredDescription
tickerstringyesTrading pair symbol
orderIdintegeryesExchange order ID
typeintegernoOrder type (default 0 Limit)
POST /api/connections/{connectionId}/orders/cancel-all Cancel all orders

Cancels all open orders for a given ticker on the exchange.

FieldTypeRequiredDescription
tickerstringyesTrading pair symbol

Returns { status, cancelledCount }cancelledCount: 0 if there are no open orders for that ticker.

Market Data
GET /api/connections/{connectionId}/cluster-snapshot Cluster snapshot

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.

ParameterTypeRequiredDefaultDescription
TickerstringyesTrading pair symbol
TimeFramestringyesS30, M1, M5, M10, M15, M30, H1, H4, D1
ZoomIndexintno1Price aggregation factor (1 = raw levels)

When ZoomIndex > 1, prices are grouped into buckets of ZoomIndex × PriceIncrement and volumes are summed.

Signal levels

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").

GET /api/connections/{connectionId}/signal-levels?Ticker=... Get signal levels

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.

POST /api/connections/{connectionId}/signal-levels Create a signal level

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.

FieldTypeRequiredDescription
tickerstringyesTrading pair symbol
pricedecimalyesTrigger price (must be > 0)
triggerRulestringnoFiring 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.
notestringnoFree-text note/label for the level. Omitted → no note (unchanged).
lineThicknessnumbernoStroke thickness. Omitted → unset (today's theme-driven stroke).
lineStylestringnoSolid / Dashed / Dotted (case-insensitive, or the enum int). Omitted → unset.
lineColorstringnoLine colour as a hex string. Omitted → unset.
textSizenumbernoLabel font size. Omitted → unset.
textColorstringnoLabel colour as a hex string. Omitted → unset.
textAlignmentstringnoLeft / Center / Right (case-insensitive, or the enum int). Omitted → unset.
textStylestringnoNormal / 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/price400; price ≤ 0400 Price must be greater than zero; unknown triggerRule400 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.

PUT /api/connections/{connectionId}/signal-levels/{signalLevelId} Modify an existing signal level (partial)

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.

FieldTypeRequiredDescription
pricedecimalnoNew trigger price (must be > 0). Leaves triggerRule untouched.
triggerRulestringnoLessThanEqual / GreaterThanEqual, case-insensitive. Leaves price untouched.
notestringnoNew note. Leaves the other fields untouched.
lineThicknessnumbernoNew stroke thickness. Leaves the other fields untouched.
lineStylestringnoSolid / Dashed / Dotted (or the enum int). Leaves the other fields untouched.
lineColorstringnoNew line colour (hex). Leaves the other fields untouched.
textSizenumbernoNew label size. Leaves the other fields untouched.
textColorstringnoNew label colour (hex). Leaves the other fields untouched.
textAlignmentstringnoLeft / Center / Right (or the enum int). Leaves the other fields untouched.
textStylestringnoNormal / 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 ≤ 0400 Price must be greater than zero; unknown triggerRule400 Invalid 'triggerRule'. Allowed values: LessThanEqual, GreaterThanEqual.; level not found → 404 Signal level {id} not found.

DELETE /api/connections/{connectionId}/signal-levels/{signalLevelId} Remove signal level

Removes a single signal level by ID.

DELETE /api/connections/{connectionId}/signal-levels?Ticker=... Remove all signal levels (by ticker)

Removes all signal levels for a specific ticker on a connection.

DELETE /api/signal-levels/triggered Remove all triggered signal levels

Removes all triggered signal levels across all connections and tickers.

Order Book Settings
GET /api/connections/{connectionId}/orderbook-settings?Ticker=... Get order book settings

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.

PUT /api/connections/{connectionId}/orderbook-settings?Ticker=... Update order book settings (partial)

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).

Order Book Snapshot
GET /api/connections/{connectionId}/orderbook-snapshot?Ticker=... Fresh one-shot order book snapshot from the exchange

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

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.

GET /api/connections/{connectionId}/user-levels?Ticker=... Get user levels

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.

POST /api/connections/{connectionId}/user-levels Create a user level

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.

FieldTypeRequiredDescription
tickerstringyesTrading pair symbol
pricedecimalyesLevel price (must be > 0)
namestringnoLabel. Omitted → the exact hand-drawn default the app uses when a level is drawn by hand: today's date as dd.MM.yyyy.
notestringno(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.
dateinteger (epoch seconds)noOptional timestamp (the hand-drawn horizontal ray stores the chart time here). Omitted → null (a plain level).
lineThicknessnumbernoStroke thickness. Omitted → unset (today's theme-driven stroke).
lineStylestringnoSolid / Dashed / Dotted (case-insensitive, or the enum int). Omitted → unset.
lineColorstringnoLine colour as a hex string. Omitted → unset.
textSizenumbernoLabel font size. Omitted → unset.
textColorstringnoLabel colour as a hex string. Omitted → unset.
textAlignmentstringnoLeft / Center / Right (case-insensitive, or the enum int). Omitted → unset.
textStylestringnoNormal / 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/price400 Invalid request body. 'ticker' and 'price' are required.; price ≤ 0400 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.

PUT /api/connections/{connectionId}/user-levels/{userLevelId} Modify an existing user level (partial)

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.

FieldTypeRequiredDescription
pricedecimalnoNew price (must be > 0). Leaves name / date untouched.
namestringnoNew label. Leaves the other fields untouched.
notestringno(MC-421 Step 15) New note (partial-update: supplied → overwrite, omitted → untouched). Distinct from name.
dateinteger (epoch seconds)noNew timestamp. Leaves the other fields untouched.
lineThicknessnumbernoNew stroke thickness. Leaves the other fields untouched.
lineStylestringnoSolid / Dashed / Dotted (or the enum int). Leaves the other fields untouched.
lineColorstringnoNew line colour (hex). Leaves the other fields untouched.
textSizenumbernoNew label size. Leaves the other fields untouched.
textColorstringnoNew label colour (hex). Leaves the other fields untouched.
textAlignmentstringnoLeft / Center / Right (or the enum int). Leaves the other fields untouched.
textStylestringnoNormal / 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 ≤ 0400 Price must be greater than zero; level not found → 404 User level {id} not found.

DELETE /api/connections/{connectionId}/user-levels/{userLevelId} Remove user level

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.

DELETE /api/connections/{connectionId}/user-levels?Ticker=... Remove all user levels (by ticker)

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

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.

GET /api/connections/{connectionId}/annotations?Ticker=... Read all three annotation lists for a ticker

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}.

ListItem fields
lineAnnotationsindex, x1, x2 (ISO-8601 datetime), y1, y2 (venue price)
horizontalLineAnnotationsindex, y (venue price)
horizontalRayAnnotationsindex, 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 ≤ 0400 Ticker '{ticker}' not found on connection {connectionId}.

PUT /api/connections/{connectionId}/annotations/{type}?Ticker=... Replace the whole list for one type (primary op)

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 Ticker400; unknown ticker or PriceIncrement ≤ 0400 Ticker '{ticker}' not found on connection {connectionId}.

POST /api/connections/{connectionId}/annotations/{type}?Ticker=... Append one annotation of a type

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 Ticker400; unknown ticker or PriceIncrement ≤ 0400 Ticker '{ticker}' not found on connection {connectionId}.

DELETE /api/connections/{connectionId}/annotations/{type}/{index}?Ticker=... Remove one annotation by zero-based index

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 Ticker400.

DELETE /api/connections/{connectionId}/annotations?Ticker=... Clear all three lists for a ticker

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 Ticker400.

Screener templates

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.

GET /api/screener/templates List all screener templates

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.

GET /api/screener/templates/{templateId} Read one template's resolved configuration

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.

POST /api/screener/templates Create a template (name + optional settings)

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 name400 Request body must carry a non-empty 'name'.; body that is not valid JSON → 400 Request body is not valid JSON.

PUT /api/screener/templates/{templateId} Edit a template (partial: null keeps current)

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).

DELETE /api/screener/templates/{templateId} Delete a template

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).

GET /api/screener/templates/{templateId}/data Snapshot a template's full data (all rows, headless)

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).

Notifications

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.

POST /api/notifications Inject a custom notification into the feed

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).

FieldTypeRequiredDescription
timestring (ISO-8601)noNotification timestamp. Omitted → the request time (server UTC now).
connectionIdintegernoWhen 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.
tickerstringnoInstrument label. Omitted → empty cell.
eventTypestringnoFree-text label shown in the Event Type column, verbatim. Omitted → empty cell.
sizedecimalnoSize cell. Omitted → empty (null, no sentinel).
pricedecimalnoPrice cell. Omitted → empty (null, no sentinel).
tabNamestringnoTab-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 connectionId404 Connection {id} not found.

UI inventory (read-only)

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.

GET /api/ui/state Full open-UI inventory

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.

Window

FieldTypeNullableDescription
idintegernoPersisted window id.
typestringnoWindow 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.
geometryobjectno{ top, left, width, height, isFullScreen, pinOnTop, isTopBarHidden }. pinOnTop is nullable (tri-state in the model).
monitorintegeralways nullWould 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).
tabsarraynoThe window's tabs (see below).

Tab

FieldTypeNullableDescription
idintegernoPersisted tab id.
namestringyesTab name (the «title»).
orderIdintegernoTab ordering index.
isSelectedbooleannoWhether this is the active tab in its window (the only persisted «active» signal).
colorstringyesTab colour, when set.
documentsarraynoThe order books and charts on this tab.

Document (common fields)

FieldTypeNullableDescription
idintegernoPersisted document id.
externalIdstring (GUID)noThe stable id the docking layout XML joins on (its ContentId).
kindstringnoorderBook or chart.
connectionIdintegeryesOwning connection; null when the document is not bound to a connection.
tickerstringyesInstrument label; null when unset.
linkNumberintegeryesThe 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.
layoutPositionobjectyes{ 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.
orderBookobjectyesOrder-book details (below); null when kind != orderBook.
chartobjectyesChart details (below); null when kind != chart.

Order-book details (document.orderBook)

FieldTypeNullableDescription
baseAssetstringyesBase asset.
quoteAssetstringyesQuote asset.
priceIncrementdecimalnoPrice tick size.
sizeIncrementdecimalnoSize step.
minSizedecimalnoMinimum order size.
maxSizedecimalyesMaximum order size, when defined.
stepPricedecimalyesStep price, when defined.
statestringnoOrder-book state (Open / Closing).
isTradingAllowedbooleannoWhether trading is enabled on this book.
pinMarketTypebooleannoWhether the market type is pinned.
zoomIndexintegeryesThe 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.

Chart details (document.chart)

FieldTypeNullableDescription
timeFramestringnoChart time frame enum name (None, M1, …).
enginestringnoChart engine: Regular or TradingView.
xVisibleRangeDiffintegernoVisible x-range span.
pinMarketTypebooleannoWhether the market type is pinned.
geometryobjectyesOwn 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.

Other open window families

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.

FieldTypeNullableDescription
watchlistsarraynoOpen watchlist windows, each { id, externalId, geometry }. A watchlist row has no visibility flag — every listed row is an open window.
screenersarraynoOpen 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.
lineNotificationsobjectyesThe 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.
finresobjectyesThe finres singleton, same shape/semantics as lineNotifications.
userTradesobjectyesThe user-trades singleton, same shape/semantics.
listingobjectyesThe listing singleton, same shape/semantics.

Success → 200 with the object above.

GET /api/ui/windows/{windowId} One window's inventory

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 windowId400 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.

PUT /api/ui/documents/{externalId}/link-number Set a panel's link number

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 }
FieldTypeRequiredDescription
linkNumberintegeryesBinding-group id: 1 = active window, 2500 = 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 externalId400 Invalid document ID; a missing/omitted linkNumber400; a non-positive value → 400; an unknown document → 404 Document {externalId} not found (nothing is written).

POST /api/ui/windows Open a terminal window at a place/size

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 }
FieldTypeRequiredDescription
windowTypestringyesOne of: workspace, lightWorkspace, chart, tradingViewChart, watchlist, screener, lineNotifications, finres, userTrades, listing.
topnumbernoVirtual-desktop Y. Omit for today's default placement.
leftnumbernoVirtual-desktop X. Omit for today's default placement.
heightnumbernoWindow height. Omit for the type's default.
widthnumbernoWindow width. Omit for the type's default.
monitorintegerno0-based target screen. Omit to place by virtual-desktop coordinate.
templateIdintegernoScreener 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 windowType400 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 templateId404 Screener template {templateId} not found — all validated before any window is opened.

POST /api/ui/windows/{windowId}/close Close a terminal window

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" }
FieldTypeRequiredDescription
windowTypestringyesOne 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 windowType400 naming the supported list; an unknown window id → 404 — all validated before any window is closed.

POST /api/ui/windows/{windowId}/activate Bring a terminal window to the front

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" }
FieldTypeRequiredDescription
windowTypestringyesOne 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 windowType400 naming the supported list; the main workspace → 400; an unknown window id → 404.

PUT /api/ui/documents/{externalId}/ticker Re-point one panel to a market

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 }
FieldTypeRequiredDescription
tickerstringyesThe instrument to switch the panel to.
connectionIdintegernoTarget 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 externalId400 Invalid document ID; a missing ticker400; an unknown document → 404 Document {externalId} not found; a document with no current connection and no supplied connectionId400.

GATED UI control — deferred lifecycle routes Documented limitation

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:

RouteIntent
POST /api/ui/windows/{windowId}/order-booksOpen an order book in a window's active tab
POST /api/ui/windows/{windowId}/chartsOpen 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}/activateActivate a tab
Real-time WebSocket
WS ws://127.0.0.1:{port}/ Real-time updates via WebSocket

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.

Connection

The WebSocket server shares the same port as the HTTP API (1784517855). Connect to ws://127.0.0.1:{port}/.

Message format

All messages (inbound and outbound) are JSON with this envelope:

{ "Type": "message_type", "Data": { ... } }

Inbound messages (client → server)

Connection-level — subscribe by connection ID for order, position, balance, and finres updates:

TypeDataDescription
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:

TypeDataDescription
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.

Outbound messages (server → client)

Acknowledgements:

TypeWhenData fields
subscribedAfter successful connection subscribeconnectionId
unsubscribedAfter successful connection unsubscribeconnectionId
trade_subscribedAfter successful trade subscribeconnectionId, ticker, zoomIndex
trade_unsubscribedAfter successful trade unsubscribeconnectionId, ticker
orderbook_subscribedAfter successful order book subscribeconnectionId, ticker, zoomIndex; depthLevels / depthPercent echoed when non-null, fetchSnapshot only when false
orderbook_unsubscribedAfter successful order book unsubscribeconnectionId, ticker
mark_price_subscribedAfter successful mark price subscribeconnectionId, ticker
mark_price_unsubscribedAfter successful mark price unsubscribeconnectionId, ticker
index_price_subscribedAfter successful index price subscribeconnectionId, ticker
index_price_unsubscribedAfter successful index price unsubscribeconnectionId, ticker
funding_subscribedAfter successful funding subscribeconnectionId, ticker
funding_unsubscribedAfter successful funding unsubscribeconnectionId, ticker
errorInvalid message, bad connection ID, or missing tickererror (string)

Connection-level updates:

TypeWhenData fields
order_updateOrder created/changed/filled/cancelledconnectionId, orderId, ticker, side, type, price, filledPrice, size, filledSize, fee, feeCurrency, status, time
position_updatePosition opened/changed/closedconnectionId, positionId, ticker, side, size, avgPrice, avgPriceFix, avgPriceDyn, status
balance_updateAccount balance changedconnectionId, balances[] with coin, total, free, locked
finres_updateFinancial results updatedconnectionId, finreses[] with currency, result, fee, funds, available, blocked

Market data updates:

TypeWhenData fields
trade_updateNew trades for subscribed ticker (server-side aggregated — see note below)connectionId, ticker, trades[] with price, size, side, time, highPrice, lowPrice
orderbook_snapshotFull order book on subscribeconnectionId, ticker, asks[], bids[], bestAsk, bestBid — each with price, size, type
orderbook_updateIncremental order book changesconnectionId, ticker, updates[] with price, size, type
mark_price_updateMark price changed (futures only)connectionId, ticker, markPrice
index_price_updateIndex price changed (futures only)connectionId, ticker, indexPrice
funding_updateFunding 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:

TypeDirectionWhenData fields
signal_level_subscribeclient → serverStart receiving signal-level events
signal_level_unsubscribeclient → serverStop receiving them
signal_level_subscribedserver → clientSubscribe acknowledged (a signal_levels_snapshot follows immediately)
signal_level_unsubscribedserver → clientUnsubscribe acknowledged
signal_levels_snapshotserver → clientCurrent levels on subscribesignalLevels[] with id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule, and (MC-421) note, lineThickness, lineStyle, lineColor, textSize, textColor, textAlignment, textStyle (null when unset)
signal_level_placedserver → clientA level was createdid, connectionId, ticker, price, isTriggered, triggerTime, triggerRule, and the same MC-421 note + appearance fields
signal_level_updatedserver → clientA level was modified in place (price and/or rule changed, identity preserved)Same shape as signal_level_placed
signal_level_triggeredserver → clientA level firedid, triggerTime
signal_level_removedserver → clientA level was deletedid
signal_levels_removed_allserver → clientAll levels (for a ticker or globally) were deleted
signal_levels_removed_triggeredserver → clientAll triggered levels were cleared

User levels — subscribe (globally, no data payload) to receive user-level lifecycle events. date is a nullable ISO-8601 timestamp:

TypeDirectionWhenData fields
user_level_subscribeclient → serverStart receiving user-level events
user_level_unsubscribeclient → serverStop receiving them
user_level_subscribedserver → clientSubscribe acknowledged
user_level_unsubscribedserver → clientUnsubscribe acknowledged
user_levels_snapshotserver → clientCurrent levels on subscribeuserLevels[] 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_placedserver → clientA level was createdid, connectionId, ticker, price, name, note, date, and the same MC-421 appearance fields
user_level_updatedserver → clientA level was modified in place (identity preserved)Same shape as user_level_placed
user_level_removedserver → clientOne or more levels were deleted (covers DELETE {id} and DELETE ?Ticker=) — removal is a batch, so ids are carried as an arrayids[]
user_levels_removed_allserver → clientAll 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:

TypeDirectionWhenData fields
notification_subscribeclient → serverStart receiving notification-feed events
notification_unsubscribeclient → serverStop receiving them
notification_subscribedserver → clientSubscribe acknowledged (a notification_snapshot follows immediately)
notification_unsubscribedserver → clientUnsubscribe acknowledged
notification_snapshotserver → clientThe 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_updateserver → clientA 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.

TypeDirectionWhenData fields
annotation_subscribeclient → serverStart receiving annotations for one (connectionId, ticker)connectionId, ticker
annotation_unsubscribeclient → serverStop receiving them for that (connectionId, ticker)connectionId, ticker
annotation_subscribedserver → clientSubscribe acknowledged (an annotations_snapshot follows immediately)connectionId, ticker
annotation_unsubscribedserver → clientUnsubscribe acknowledgedconnectionId, ticker
annotations_snapshotserver → clientThe 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:

TypeDirectionWhenData fields
ui_subscribeclient → serverStart receiving UI change events (send "data": {})
ui_unsubscribeclient → serverStop receiving them
ui_subscribedserver → clientSubscribe acknowledged (a ui_snapshot follows immediately)
ui_unsubscribedserver → clientUnsubscribe acknowledged
ui_snapshotserver → clientThe whole open UI on subscribeSame shape as GET /api/ui/state: windows[], standaloneCharts[]
ui_updateserver → clientOne observed UI changekind plus a kind-specific payload — see below

ui_update payloads by kind:

kindWhenData fields
tabAddedA tab was added to a windowkind, windowId, window (the full window subtree, same shape as one entry of state.windows[])
documentOpenedA chart document was openedkind, 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.

Lifecycle

EventBehavior
Client connectsSession 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).
UnsubscribeServer responds with confirmation. Updates stop for that subscription.
Client disconnectsAll subscriptions (connection-level and market data) are cleaned up automatically.
Multiple connectionsA single client can subscribe to multiple connection IDs and multiple tickers simultaneously.

Example payloads

// 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" } }

Disconnected
Waiting for connection...
Subscribe to order book
to see live depth
Subscribe to trades
to see live feed

Reference

Exchange values

ValueExchange
2Binance
3GateIo
5KuCoin
6Bybit
7Bitget
8Mexc
10Okx
11BingX
12HTX
13BitMart
14LBank
15HyperLiquid
16UpBit
17AsterDex
18Moex
19Lighter

MarketType values

ValueTypeDescription
0SpotSpot trading
1FuturesGeneric futures
2UsdtFuturesUSDT-margined futures
3CoinFuturesCoin-margined futures
4InverseFuturesInverse futures
5UsdtPerpetualUSDT perpetual swaps
6UsdcPerpetualUSDC perpetual swaps
7MarginMargin (cross/isolated)
8OptionsOptions contracts
9StockStock / equity markets
Unsure which market type to use? Use the ticker pattern approach instead — the .p suffix auto-resolves to the right futures type. For explicit fields, 2 (UsdtFutures) is the most common choice for perpetual futures.
When the requested market is not Spot and no exact match is found, MetaScalp falls back to any non-Spot connection on the same exchange.

Error responses

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.

EndpointConditionHTTPError message
(any endpoint with a JSON body)Body is not valid JSON400Malformed request body: not valid JSON.
(any endpoint with a JSON body)Unknown property in body400Unknown field '{field}' in request body.
/api/change-tickerMissing fields400Invalid request body. Provide 'tickerPattern' or 'exchange'+'market'+'ticker'.
/api/change-tickerInvalid pattern400Invalid ticker pattern: '{pattern}'
/api/change-tickerBinding not found400Binding '{name}' not found. Available: {list}
/api/change-tickerNo connection400No connection found for exchange ... and market ...
/api/change-tickerTicker not on connection400Ticker '{ticker}' not found on connection {id}
/api/comboMissing ticker400Invalid request body. 'ticker' is required.
/api/connections/{id}Connection not found404Connection {id} not found
/api/connections/{id}/...Invalid connection ID400Invalid connection ID
/api/connections/{id}/...Connection not found404Connection {id} not found
/api/connections/{id}/...Connection not active400Connection {id} is not active
/api/connections/{id}/ordersMissing ticker query param400Query parameter 'Ticker' is required
/api/connections/{id}/ordersInvalid order fields400Invalid request body. 'ticker', 'side', 'price', and 'size' are required.
/api/connections/{id}/ordersSize <= 0400Size must be greater than zero
/api/connections/{id}/ordersPrice <= 0 (non-market)400Price must be greater than zero for non-market orders
/api/connections/{id}/orders/cancelMissing fields400Invalid request body. 'ticker' and 'orderId' are required.
POST /api/connections/{id}/signal-levelsMissing ticker/price400Invalid request body. 'ticker' and 'price' are required.
POST/PUT .../signal-levelsPrice <= 0400Price must be greater than zero
POST/PUT .../signal-levelsUnknown triggerRule400Invalid 'triggerRule'. Allowed values: LessThanEqual, GreaterThanEqual.
POST /api/connections/{id}/signal-levelsNo order book data400No market data for '{ticker}'. Subscribe to order book data for this ticker first.
PUT .../signal-levels/{signalLevelId}Invalid signal level ID400Invalid signal level ID
PUT .../signal-levels/{signalLevelId}Empty body / all fields null400Request body must set at least one of 'price' or 'triggerRule'.
PUT .../signal-levels/{signalLevelId}Level not found404Signal level {id} not found
POST /api/connections/{id}/user-levelsMissing ticker/price400Invalid request body. 'ticker' and 'price' are required.
POST/PUT .../user-levelsPrice <= 0400Price must be greater than zero
PUT/DELETE .../user-levels/{userLevelId}Invalid user level ID400Invalid user level ID
PUT .../user-levels/{userLevelId}Empty body / all fields null400Request body must set at least one of 'price', 'name', 'date', 'note' or an appearance field.
PUT .../user-levels/{userLevelId}Level not found404User level {id} not found
GET/DELETE .../user-levelsMissing Ticker query400Query parameter 'Ticker' is required
POST /api/notificationsUnknown connectionId404Connection {id} not found
GET /api/ui/windows/{windowId}Non-numeric window id400Invalid window ID
GET /api/ui/windows/{windowId}Unknown window404Window {id} not found
POST /api/ui/windowsMalformed body400Malformed request body.
POST /api/ui/windowsMissing windowType400POST /api/ui/windows requires 'windowType' in the body to know which window to open. (+ supported list)
POST /api/ui/windows/{windowId}/close|activateMissing windowType400POST /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/windowsUnknown / unsupported-standalone type400Unsupported windowType '…'. (+ supported list)
POST /api/ui/windowsUnknown screener templateId404Screener template {templateId} not found
PUT /api/ui/documents/{externalId}/link-numberNon-GUID document id400Invalid document ID
PUT /api/ui/documents/{externalId}/link-numberMissing linkNumber400Request body must set 'linkNumber'.
PUT /api/ui/documents/{externalId}/link-numberNon-positive linkNumber400linkNumber must be a positive binding-group id (1-500, or 9999 to cancel).
PUT /api/ui/documents/{externalId}/link-numberUnknown document404Document {externalId} not found
PUT /api/ui/documents/{externalId}/tickerNon-GUID document id400Invalid document ID
PUT /api/ui/documents/{externalId}/tickerMissing ticker400Request body must set 'ticker'.
PUT /api/ui/documents/{externalId}/tickerNo connection to inherit400connectionId is required: the document has no current connection to inherit.
PUT /api/ui/documents/{externalId}/tickerUnknown document404Document {externalId} not found
anyUnknown path400Not found
anyWrong HTTP method400Method not allowed

Socket error messages

Socket errors are sent as { "Type": "error", "Data": { "error": "..." } }

ConditionError message
Subscribe to non-existent connectionConnection {id} not found or not active
Trade/orderbook subscribe with missing tickerTicker is required for trade subscription / Ticker is required for order book subscription
Trade/orderbook subscribe with invalid connectionConnection {id} not found or not active
Mark price / funding subscribe with missing tickerTicker is required for mark price subscription / Ticker is required for funding subscription
Mark price / funding subscribe with invalid connectionConnection {id} not found or not active
Unknown message typeUnknown message type: {type}
Invalid JSONInvalid message format

📄 MetaScalp-Api.md

⬇ Download .md

MetaScalp API

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.

  • Host: 127.0.0.1 (localhost only)
  • HTTP port range: 1784517855 (first available)
  • WebSocket: Same port as HTTP (upgrade requests accepted automatically)
  • Content-Type: application/json
  • CORS: All responses include Access-Control-Allow-Origin: *.

Overview

HTTP REST endpoints

Use HTTP to discover connections, query data, and execute trades:

EndpointPurpose
GET /pingFind the running MetaScalp instance and check its version
POST /api/change-tickerSwitch the active ticker in the MetaScalp UI
POST /api/comboOpen a combo layout for a ticker
GET /api/connectionsList all active exchange connections
GET /api/connections/{id}/...Query tickers, orders, positions, balances for a connection
POST /api/connections/{id}/ordersPlace or cancel orders on a connection
POST /api/connections/{connectionId}/orders/cancel-allCancel all open orders for a ticker on a connection (body: { ticker }) → { status, cancelledCount }
GET /api/connections/{connectionId}/orderbook-snapshotREST order-book snapshot for a ticker (optional DepthLevels / DepthPercent; 501 when the exchange has no REST snapshot)
GET /api/link-groups/{groupId}/orderbook-snapshotsRead-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-settingsRead, 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..5tradeAmount1..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-snapshotRemote cluster (footprint) snapshot for a ticker at a timeframe (optional ZoomIndex)
GET/POST/PUT/DELETE /api/connections/{id}/signal-levelsRead, create, modify (in place) and delete signal levels for a ticker
DELETE /api/signal-levels/triggeredRemove all triggered signal levels (across every connection)
GET/POST/PUT/DELETE /api/connections/{id}/user-levelsRead, create, modify (in place) and delete user (plain) levels for a ticker
GET/PUT/POST/DELETE /api/connections/{id}/annotationsRead 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/notificationsInject a custom row into the notification feed (all fields optional, arbitrary event type)
GET /api/ui/stateFull 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/windowsOpen a terminal window at a requested place/size (and monitor); omitted geometry → today's default placement
PUT /api/ui/documents/{externalId}/link-numberSet one panel's link (binding-group) number; unknown document → 404
PUT /api/ui/documents/{externalId}/tickerRe-point one addressed panel to a different market (that panel only)

WebSocket streaming

Connect via WebSocket to receive real-time updates for your exchange connections.

  • Connect to ws://127.0.0.1:{port}/ (same port as HTTP — scan 1784517855)
  • Connection-level: Send subscribe with a connection ID to receive order, position, balance, and finres updates
  • Market data: Send trade_subscribe, orderbook_subscribe, mark_price_subscribe, or funding_subscribe with connection ID + ticker to receive trade, order book, mark price, or funding updates
  • You can subscribe to multiple connections and tickers simultaneously
  • All subscriptions are automatically cleaned up when you disconnect

Getting started

1. Launch MetaScalp — both HTTP and WebSocket servers start automatically.

2. Discover the port — scan 1784517855 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.

Typical REST client flow

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

Typical WebSocket client flow

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}}

WebSocket message reference

Messages you send

TypeDataDescription
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.

Messages you receive

TypeData fields
subscribedconnectionId
unsubscribedconnectionId
trade_subscribedconnectionId, ticker, zoomIndex
trade_unsubscribedconnectionId, ticker
orderbook_subscribedconnectionId, ticker, zoomIndex (+ depthLevels / depthPercent when non-null, fetchSnapshot when false)
orderbook_unsubscribedconnectionId, ticker
order_updateconnectionId, orderId, ticker, side, type, price, filledPrice, size, filledSize, fee, feeCurrency, status, time
position_updateconnectionId, positionId, ticker, side, size, avgPrice, avgPriceFix, avgPriceDyn, status
balance_updateconnectionId, balances[]: coin, total, free, locked
finres_updateconnectionId, finreses[]: currency, result, fee, funds, available, blocked
trade_updateconnectionId, ticker, trades[]: price, size, side, time, highPrice, lowPrice
orderbook_snapshotconnectionId, ticker, asks[], bids[], bestAsk, bestBid — each: price, size, type
orderbook_updateconnectionId, ticker, updates[]: price, size, type
mark_price_subscribedconnectionId, ticker
mark_price_unsubscribedconnectionId, ticker
mark_price_updateconnectionId, ticker, markPrice
funding_subscribedconnectionId, ticker
funding_unsubscribedconnectionId, ticker
funding_updateconnectionId, ticker, fundingRate, fundingTime (ISO 8601)
signal_levels_snapshotsignalLevels[]: id, connectionId, ticker, price, isTriggered, triggerTime, triggerRule
signal_level_placedid, connectionId, ticker, price, isTriggered, triggerTime, triggerRule
signal_level_updatedSame shape as signal_level_placed (level modified in place)
signal_level_triggeredid, triggerTime
signal_level_removedid
user_levels_snapshotuserLevels[]: id, connectionId, ticker, price, name, note, date
user_level_placedid, connectionId, ticker, price, name, note, date
user_level_updatedSame shape as user_level_placed (level modified in place)
user_level_removedids[] (batch removal)
user_levels_removed_all
annotation_subscribedconnectionId, ticker
annotation_unsubscribedconnectionId, ticker
annotations_snapshotconnectionId, ticker, lineAnnotations[]: index, x1, x2, y1, y2; horizontalLineAnnotations[]: index, y; horizontalRayAnnotations[]: index, y, x1y/y1/y2 are venue prices (same as GET; unresolvable ticker → error frame) (no annotations_updated — snapshot is one-shot)
errorerror (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.