Documentation

Live Execution API

Subscribe a strategy to a live asset and timeframe — get a signal the moment its entry or exit condition fires on a real, closed candle.

#Overview

Live Execution is in beta. The API is stable and available on every plan, including Free — expect the surrounding tooling (dashboards, webhooks) to keep growing from here.

A live subscription watches one asset pair at one timeframe. Every time a candle closes for that pair, your strategy is evaluated against it — the same Strategy DSL and the same evaluation engine the Backtesting API uses, just running continuously instead of over a historical date range.

Base URL: https://live.emidlabs.com/api/public/v1/live

All requests require the x-api-key header with a valid API key scoped for the live service. Keys are generated in the Console — the Live Execution toggle appears there next to Backtesting.

#Supported providers & assets

Listens directly onCoinbaseBinance

A live subscription connects straight to the chosen exchange's own market data feed — there is no fixed asset list to consult first. Any pair the provider actually lists can be subscribed, in that provider's own assetPair format (see provider below). This is broader, on purpose, than what Backtesting supports.

Live vs. Backtesting isn't the same asset universe. Backtesting only covers assets EmidLabs has already extracted and stored historical candles for — a fixed, curated list (see the Data Referencepage). Live Execution has no such list: it subscribes straight to Coinbase or Binance's own live feed, so it covers everything either exchange lists, not just what's been backtested before. The trade-off is validation happens differently — see the assetPair/provider fields below for what happens when a pair turns out not to exist.

#How it works

There's no polling loop to build. Subscribe once; a signal only exists because a real entry or exit condition became true on a real, closed candle:

StepWhat happens
1. SubscribePOST a strategy + assetPair. The timeframe comes from the strategy's own configuration.timeframe.
2. Candle closesEvery closed candle for that asset+timeframe evaluates your strategy — the same StrategyRunner the Backtesting API uses.
3. Signal firesIf entry or exit evaluates true, it's recorded immediately — poll GET .../signals or check the Console dashboard.
4. Stop anytimeDELETE the subscription — no more evaluations happen for it, no partial-period charge.

#Signal Model: Stateless vs Stateful

Every candle close re-evaluates decision.entry/decision.exitfresh — there's no built-in memory of whether the last candle already fired the same signal. If a condition isn't edge-triggered (a plain comparison like rsi14 > 55 instead of crossUp(...)/crossDown(...)), it stays true for many consecutive candles, and the default behavior re-fires that same signal on every one of them.

modelBehavior
"stateless" (default)Every subscription starts here. A signal is recorded/delivered exactly when decision.entry/decision.exit evaluates true — every time, even repeatedly for a condition that stays true across several candles.
"stateful"The subscription tracks its own Entry/Exit position internally (isPositioned) — never queried from your exchange — and only records/delivers a signal on an actual transition: Entry while not positioned, or Exit while positioned. Everything else is silently suppressed, no signal recorded, no webhook sent.
isPositioned is never inferred from a real exchange balance or position — this API has no broker integration of its own. If it drifts from reality (a trade placed manually outside this system), correct it yourself via PATCH /subscriptions/{id}. You can set model directly on POST /subscriptions/POST /subscriptions/batch to create an already-"stateful" subscription in one call, or switch an existing one any time the same way.

#POST /subscriptions — Create

POST/subscriptions

Subscribe a strategy to live evaluation on one asset pair.

Same strategySnapshotJson shape as the Backtesting API — see Strategy System for the full DSL reference. configuration.timeframe inside it is what determines the candle boundary your strategy evaluates on (5M, 15M, 1H, etc.) — there's no separate timeframe field on the subscription itself.

Request body

FieldTypeRequiredDescription
strategySnapshotJsonobjectYesStrategy object — configuration, inputs, conditions, score, decision, riskManagement. configuration.timeframe drives evaluation frequency.
assetPairstringYesTrading pair, in the exact format the chosen provider uses — see provider below. No translation happens anywhere: an asset that doesn't exist under that exact string on that provider is accepted here and fails asynchronously, not rejected on creation.
providerstringNoWhich market data provider to listen on: "coinbase" or "binance". Defaults to "coinbase" when omitted.
webhookUrlstringNoAbsolute https:// URL. When set, every emitted signal is also POSTed here — see Webhooks below.
modelstringNo"stateless" (default when omitted) or "stateful" — see Signal Model above.
isPositionedbooleanNoOnly meaningful when model is "stateful". Defaults to false (not positioned yet) when omitted.
assetPair format depends entirely on provider — they are not interchangeable. Coinbase uses a hyphen ("BTC-USDC"); Binance does not ("BTCUSDC"). There is no existence check against either exchange at creation time: an invalid pair, or the right pair in the wrong provider's format, returns 201 immediately and then the subscription's status moves to "Errored" within about a minute — check back after creating, or watch for the failure via the webhook/signal log staying empty longer than expected.

Response

FieldTypeDescription
idstring (UUID)Unique identifier for the subscription.
statusstring"Active" immediately after creation.
modelstring"Stateless" unless model was set to "stateful" in the request.
isPositionedbooleanReflects the request's isPositioned when model is "Stateful"; false otherwise.
webhookSecretstring | nullOnly present when webhookUrl was set. Returned once, here only — no endpoint ever echoes it back again.

#POST /subscriptions/batch — Batch Create

POST/subscriptions/batch

Subscribe the SAME strategy to live evaluation on multiple asset pairs at once.

One subscription per entry in assetPairs, all sharing the same strategySnapshotJson/provider/webhookUrl. Best-effort per item — one bad asset pair (unknown, wrong provider format, concurrent-subscription cap reached) never fails the rest of the batch.

Request body

FieldTypeRequiredDescription
strategySnapshotJsonobjectYesShared by every subscription created in this call.
assetPairsstring[]YesOne entry per subscription to create, each in the chosen provider's own format. Max 50 per call.
providerstringNoShared by every subscription in this batch. Defaults to "coinbase" when omitted.
webhookUrlstringNoShared by every subscription in this batch — each still gets its own signed deliveries and its own webhookSecret.
modelstringNoShared by every subscription in this batch. "stateless" (default when omitted) or "stateful" — see Signal Model above.
isPositionedbooleanNoShared by every subscription in this batch, only meaningful when model is "stateful". Defaults to false when omitted.

Response

FieldTypeDescription
itemsarrayOne entry per requested asset pair, in the order submitted.
items[].assetPairstringEchoes the requested asset pair.
items[].idstring | nullSet on success.
items[].statusstring | nullSet on success — "Active".
items[].webhookSecretstring | nullSet on success, only when webhookUrl was provided. Returned once, here only.
items[].errorstring | nullSet instead of id/status/webhookSecret when this specific item failed.

#Webhooks

Pass webhookUrl on creation and every signal that subscription emits is also POSTed there as JSON, in the same shape as the signal object below, signed via an X-Emidlabs-Signature header — t={unix timestamp},v1={hex HMAC-SHA256} of `${timestamp}.${rawBody}` keyed with webhookSecret. Delivery retries up to 3 times on network errors, timeouts, 429s, and 5xx responses from your endpoint — a slow or unreachable webhook never blocks signal evaluation itself.

#DELETE /subscriptions/{id} — Stop

DELETE/subscriptions/{id}

Stop a subscription. No further candles are evaluated for it.

Returns 404 if the subscription doesn't exist or doesn't belong to your account.

#POST /subscriptions/stop-all — Stop All

POST/subscriptions/stop-all

Stops every currently active subscription on your account, in one call.

No request body. Idempotent — already-stopped subscriptions are simply skipped, not an error.

Response

FieldTypeDescription
itemsarrayOne entry per subscription that was active when this call started.
items[].idstringUUID of the subscription.
items[].statusstring | nullSet on success — "Stopped".
items[].errorstring | nullSet instead of status when this specific item failed.
There is no way to scope this to a subset of your subscriptions — it acts on the whole account. Use POST /subscriptions/stop with an explicit id list if you only want some stopped.

#POST /subscriptions/stop — Stop By Ids

POST/subscriptions/stop

Stops a specific list of subscriptions, in one call.

Request body

FieldTypeRequiredDescription
idsstring[]YesSubscription ids to stop. Max 200 per call.

Response

FieldTypeDescription
itemsarrayOne entry per requested id, in the order submitted.
items[].idstringEchoes the requested id.
items[].statusstring | nullSet on success — "Stopped". Idempotent, same as single-stop.
items[].errorstring | nullSet instead of status when this specific id failed (e.g. not found).

#PATCH /subscriptions/{id} — Update

PATCH/subscriptions/{id}

Update a subscription's signal model, position state, or webhook.

assetPair, timeframe, provider, and the strategy itself can never be changed after creation — stop the subscription and create a new one for that. Every field below is optional; omit a field to leave it unchanged.

Request body

FieldTypeDescription
modelstring"stateless" or "stateful" — see Signal Model below.
isPositionedbooleanOnly meaningful when model is (or is being set to) "stateful". A manual correction, never inferred automatically — see Signal Model below.
webhookUrlstringA new absolute https:// URL, or an empty string to remove the existing webhook. Any non-empty change generates a fresh webhookSecret.

Response

FieldTypeDescription
idstringUUID of the subscription.
modelstringThe subscription's model after this update.
isPositionedbooleanThe subscription's position flag after this update.
webhookSecretstring | nullOnly present when webhookUrl was just changed to a non-empty value in this call. Returned once, here only.

Returns 404 if the subscription doesn't exist or doesn't belong to your account.

#GET /subscriptions/{id} — Status

GET/subscriptions/{id}

Fetch a single subscription's current status.

Response fields

FieldTypeDescription
idstringUUID of the subscription.
assetPairstringAsset pair being watched, exactly as submitted.
providerstringMarket data provider this subscription listens on — "coinbase" or "binance".
timeframestringResolved from the strategy's own configuration at creation time.
statusstring"Active", "Stopped", or "Errored" (the provider could never produce data for this assetPair — see the create-request Callout above).
lastEvaluatedCandleOpenTimenumber | nullUnix seconds — OpenTime of the most recent candle this subscription evaluated. null if none yet.
modelstring"Stateless" or "Stateful" — see Signal Model below.
isPositionedbooleanOnly meaningful when model is "Stateful" — see Signal Model below.
createdAtUtcstringCreation timestamp (UTC).
stoppedAtUtcstring | nullWhen the subscription was stopped, if it has been.

#GET /subscriptions — List

GET/subscriptions

List subscriptions on your account, paginated.

Query parameters

ParamTypeDefaultDescription
pageinteger1Page number, starting at 1.
pageSizeinteger20Results per page. Must be between 1 and 100.
statusstring(none)Optional exact-match filter: "Active", "Stopped", or "Errored". Omit to return every status.

Response

Same per-item fields as the single-subscription response above, under items, plus pagination metadata:

FieldTypeDescription
itemsarraySubscriptions for this page — same shape as GET /subscriptions/{id}.
totalCountnumberTotal subscriptions matching the status filter (or all of them, if omitted) across every page.
pagenumberThe page returned.
pageSizenumberThe page size used.
totalPagesnumberTotal number of pages at this pageSize.

#GET /subscriptions/{id}/signals — Signal Format

GET/subscriptions/{id}/signals

The emitted signal log for a subscription, paginated — the live analogue of the Backtesting API's tradesDetail.

Query parameters

ParamTypeDefaultDescription
pageinteger1Page number, starting at 1.
pageSizeinteger20Results per page. Must be between 1 and 100.
typestring(none)Optional exact-match filter: "Entry" or "Exit". Omit to return both.

Response fields

FieldTypeDescription
itemsarraySignals for this page, newest first.
items[].candleOpenTimenumberUnix seconds — OpenTime of the candle whose close triggered this signal.
items[].typestring"Entry" or "Exit".
items[].pricenumberClose price of the triggering candle.
items[].scorenumberSum of the weights of every condition that evaluated true on this candle.
items[].conditionsobjectEvery named condition from the strategy, mapped to whether it was true on this candle.
items[].scoreBreakdownobjectPer-condition weight contributed to score — only true conditions contribute.
items[].stopLossPricenumber | nullEntry signals only. The stop-loss price riskManagement would place for this entry, computed fresh — null on Exit signals.
items[].takeProfitPricenumber | nullEntry signals only. The take-profit price riskManagement would place for this entry — null on Exit signals.
totalCountnumberTotal signals matching the type filter (or all of them, if omitted) across every page.
pagenumberThe page returned.
pageSizenumberThe page size used.
totalPagesnumberTotal number of pages at this pageSize.

v1 emits signals only — there's no position-tracking or order-execution layer here. What you do with a signal (place a trade, alert someone, log it) is entirely up to your own integration.

#Confirmation Sources

Have one subscription require corroboration from another before its own signal counts as real — e.g. an XRP entry on M30 only firing once a BTC subscription reports a neutral trend, or once the same asset's own H4 trend subscription agrees. Two fields on create_subscription make one subscription (A) depend on another (B) — no new endpoint, no change to either strategy's DSL.

Request fields

FieldTypeRequiredDescription
rolestringNo"tradeable" (default) or "support". A "support" subscription is a pure confirmation source — it still evaluates and emits signals normally, but is meant to be referenced by other subscriptions' confirmationSources, not traded on directly.
confirmationSourcesarrayNoWhen set, an Entry/Exit signal from this subscription only becomes "Confirmed" once every entry here matches. Omit for the current, unconditional behavior.
confirmationSources[].sourceIdstring (UUID)Yes*The id of another subscription on your own account. A sourceId that doesn't exist, isn't yours, or never emits a matching signal simply never confirms — it does not fail creation.
confirmationSources[].signalTypestringNo"entry" (default) or "exit" — which of the source's signal types counts as confirmation.
confirmationSources[].validityWindow.countintegerNoDefault 1. How many candles of the SOURCE's own timeframe a matching signal stays valid for. Scales automatically with whichever strategy is confirming — an H4 source's window is measured in H4 candles, regardless of A's own timeframe.
Both fields are set-once — there's no way to add, remove, or change confirmationSources or role via Update after creation, same as assetPair/provider. Stop the subscription and create a new one to change the pairing.

How confirmation is decided

Confirmation looks backward, not forward: when A's candle closes and a candidate signal fires, EmidLabs checks whether the source already has a matching signal recent enough to still be inside the validity window — it never waits for a source signal that hasn't happened yet. If A declares more than one confirmationSources entry, all of them must match (logical AND).

One narrow exception: if B's own signal for the same real-world moment is still being processed when A's check runs (both close a candle around the same time), the "Unconfirmed" verdict can still flip to "Confirmed" a moment later once B catches up — bounded to one candle of A's own timeframe, not a general wait. See the response fields below for how that shows up.

Signal response fields

FieldTypeDescription
rolestring"Tradeable" or "Support", copied from the subscription at the time this signal was emitted.
confirmationStatusstring | null"Confirmed" or "Unconfirmed". null when the subscription doesn't declare confirmationSources — an ordinary signal is unaffected by this feature end to end.
matchedSourcesarray | nullOne entry per confirmationSources requirement that matched — { sourceId, signalType, signalCandleOpenTime }. Empty/absent when confirmationStatus is "Unconfirmed" or null.
An "Unconfirmed" signal is still persisted and still returned by GET .../signals— it's an audit trail, not a silently dropped candidate. Use confirmationStatus as a query filter (same param name) to see only one bucket. Delivery to webhookUrl is where the real gating happens: an "Unconfirmed" signal never triggers a webhook, so anything downstream of the webhook (an execution bridge, an alert) only ever sees confirmed signals.

When a race resolves late (the "one narrow exception" above), the same signal is delivered to your webhook a second time, now "Confirmed" — treat webhook deliveries as keyed by the signal's id, not as exactly-once.

#Strategy DSL

Live Execution and Backtesting share the exact same DSL and evaluation engine — a strategy that passes a backtest can be subscribed live unchanged. See the full Strategy System reference for indicators, conditions, and the built-in function list.

warmupBars still matters live: the first candles after a subscription starts are excluded from evaluation until enough history has accumulated for your indicators to stabilize — same rule as backtesting, just measured from subscription time instead of the backtest's start date.

#Error Codes

Every error response has the same shape: { "error": "<code>", "message": "<text>" }.

StatusCodeDescription
400invalid_payloadThe request body is malformed, missing required fields, or the strategySnapshotJson fails validation.
401api_key_missingThe x-api-key header wasn't provided.
401api_key_invalidThe provided API key is invalid, revoked, or inactive.
403service_not_enabledThis API key is not scoped for the live execution service — enable it in the Console when creating or editing the key.
404not_foundThe subscription doesn't exist or doesn't belong to your account.
429rate_limit_exceededToo many requests for this account. Back off and retry.