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
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
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.
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:
| Step | What happens |
|---|---|
| 1. Subscribe | POST a strategy + assetPair. The timeframe comes from the strategy's own configuration.timeframe. |
| 2. Candle closes | Every closed candle for that asset+timeframe evaluates your strategy — the same StrategyRunner the Backtesting API uses. |
| 3. Signal fires | If entry or exit evaluates true, it's recorded immediately — poll GET .../signals or check the Console dashboard. |
| 4. Stop anytime | DELETE 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.
| model | Behavior |
|---|---|
| "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
/subscriptionsSubscribe 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
| Field | Type | Required | Description |
|---|---|---|---|
| strategySnapshotJson | object | Yes | Strategy object — configuration, inputs, conditions, score, decision, riskManagement. configuration.timeframe drives evaluation frequency. |
| assetPair | string | Yes | Trading 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. |
| provider | string | No | Which market data provider to listen on: "coinbase" or "binance". Defaults to "coinbase" when omitted. |
| webhookUrl | string | No | Absolute https:// URL. When set, every emitted signal is also POSTed here — see Webhooks below. |
| model | string | No | "stateless" (default when omitted) or "stateful" — see Signal Model above. |
| isPositioned | boolean | No | Only 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
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique identifier for the subscription. |
| status | string | "Active" immediately after creation. |
| model | string | "Stateless" unless model was set to "stateful" in the request. |
| isPositioned | boolean | Reflects the request's isPositioned when model is "Stateful"; false otherwise. |
| webhookSecret | string | null | Only present when webhookUrl was set. Returned once, here only — no endpoint ever echoes it back again. |
#POST /subscriptions/batch — Batch Create
/subscriptions/batchSubscribe 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
| Field | Type | Required | Description |
|---|---|---|---|
| strategySnapshotJson | object | Yes | Shared by every subscription created in this call. |
| assetPairs | string[] | Yes | One entry per subscription to create, each in the chosen provider's own format. Max 50 per call. |
| provider | string | No | Shared by every subscription in this batch. Defaults to "coinbase" when omitted. |
| webhookUrl | string | No | Shared by every subscription in this batch — each still gets its own signed deliveries and its own webhookSecret. |
| model | string | No | Shared by every subscription in this batch. "stateless" (default when omitted) or "stateful" — see Signal Model above. |
| isPositioned | boolean | No | Shared by every subscription in this batch, only meaningful when model is "stateful". Defaults to false when omitted. |
Response
| Field | Type | Description |
|---|---|---|
| items | array | One entry per requested asset pair, in the order submitted. |
| items[].assetPair | string | Echoes the requested asset pair. |
| items[].id | string | null | Set on success. |
| items[].status | string | null | Set on success — "Active". |
| items[].webhookSecret | string | null | Set on success, only when webhookUrl was provided. Returned once, here only. |
| items[].error | string | null | Set 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
/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
/subscriptions/stop-allStops every currently active subscription on your account, in one call.
No request body. Idempotent — already-stopped subscriptions are simply skipped, not an error.
Response
| Field | Type | Description |
|---|---|---|
| items | array | One entry per subscription that was active when this call started. |
| items[].id | string | UUID of the subscription. |
| items[].status | string | null | Set on success — "Stopped". |
| items[].error | string | null | Set instead of status when this specific item failed. |
#POST /subscriptions/stop — Stop By Ids
/subscriptions/stopStops a specific list of subscriptions, in one call.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| ids | string[] | Yes | Subscription ids to stop. Max 200 per call. |
Response
| Field | Type | Description |
|---|---|---|
| items | array | One entry per requested id, in the order submitted. |
| items[].id | string | Echoes the requested id. |
| items[].status | string | null | Set on success — "Stopped". Idempotent, same as single-stop. |
| items[].error | string | null | Set instead of status when this specific id failed (e.g. not found). |
#PATCH /subscriptions/{id} — Update
/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
| Field | Type | Description |
|---|---|---|
| model | string | "stateless" or "stateful" — see Signal Model below. |
| isPositioned | boolean | Only meaningful when model is (or is being set to) "stateful". A manual correction, never inferred automatically — see Signal Model below. |
| webhookUrl | string | A new absolute https:// URL, or an empty string to remove the existing webhook. Any non-empty change generates a fresh webhookSecret. |
Response
| Field | Type | Description |
|---|---|---|
| id | string | UUID of the subscription. |
| model | string | The subscription's model after this update. |
| isPositioned | boolean | The subscription's position flag after this update. |
| webhookSecret | string | null | Only 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
/subscriptions/{id}Fetch a single subscription's current status.
Response fields
| Field | Type | Description |
|---|---|---|
| id | string | UUID of the subscription. |
| assetPair | string | Asset pair being watched, exactly as submitted. |
| provider | string | Market data provider this subscription listens on — "coinbase" or "binance". |
| timeframe | string | Resolved from the strategy's own configuration at creation time. |
| status | string | "Active", "Stopped", or "Errored" (the provider could never produce data for this assetPair — see the create-request Callout above). |
| lastEvaluatedCandleOpenTime | number | null | Unix seconds — OpenTime of the most recent candle this subscription evaluated. null if none yet. |
| model | string | "Stateless" or "Stateful" — see Signal Model below. |
| isPositioned | boolean | Only meaningful when model is "Stateful" — see Signal Model below. |
| createdAtUtc | string | Creation timestamp (UTC). |
| stoppedAtUtc | string | null | When the subscription was stopped, if it has been. |
#GET /subscriptions — List
/subscriptionsList subscriptions on your account, paginated.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number, starting at 1. |
| pageSize | integer | 20 | Results per page. Must be between 1 and 100. |
| status | string | (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:
| Field | Type | Description |
|---|---|---|
| items | array | Subscriptions for this page — same shape as GET /subscriptions/{id}. |
| totalCount | number | Total subscriptions matching the status filter (or all of them, if omitted) across every page. |
| page | number | The page returned. |
| pageSize | number | The page size used. |
| totalPages | number | Total number of pages at this pageSize. |
#GET /subscriptions/{id}/signals — Signal Format
/subscriptions/{id}/signalsThe emitted signal log for a subscription, paginated — the live analogue of the Backtesting API's tradesDetail.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number, starting at 1. |
| pageSize | integer | 20 | Results per page. Must be between 1 and 100. |
| type | string | (none) | Optional exact-match filter: "Entry" or "Exit". Omit to return both. |
Response fields
| Field | Type | Description |
|---|---|---|
| items | array | Signals for this page, newest first. |
| items[].candleOpenTime | number | Unix seconds — OpenTime of the candle whose close triggered this signal. |
| items[].type | string | "Entry" or "Exit". |
| items[].price | number | Close price of the triggering candle. |
| items[].score | number | Sum of the weights of every condition that evaluated true on this candle. |
| items[].conditions | object | Every named condition from the strategy, mapped to whether it was true on this candle. |
| items[].scoreBreakdown | object | Per-condition weight contributed to score — only true conditions contribute. |
| items[].stopLossPrice | number | null | Entry signals only. The stop-loss price riskManagement would place for this entry, computed fresh — null on Exit signals. |
| items[].takeProfitPrice | number | null | Entry signals only. The take-profit price riskManagement would place for this entry — null on Exit signals. |
| totalCount | number | Total signals matching the type filter (or all of them, if omitted) across every page. |
| page | number | The page returned. |
| pageSize | number | The page size used. |
| totalPages | number | Total 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
| Field | Type | Required | Description |
|---|---|---|---|
| role | string | No | "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. |
| confirmationSources | array | No | When set, an Entry/Exit signal from this subscription only becomes "Confirmed" once every entry here matches. Omit for the current, unconditional behavior. |
| confirmationSources[].sourceId | string (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[].signalType | string | No | "entry" (default) or "exit" — which of the source's signal types counts as confirmation. |
| confirmationSources[].validityWindow.count | integer | No | Default 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. |
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
| Field | Type | Description |
|---|---|---|
| role | string | "Tradeable" or "Support", copied from the subscription at the time this signal was emitted. |
| confirmationStatus | string | null | "Confirmed" or "Unconfirmed". null when the subscription doesn't declare confirmationSources — an ordinary signal is unaffected by this feature end to end. |
| matchedSources | array | null | One entry per confirmationSources requirement that matched — { sourceId, signalType, signalCandleOpenTime }. Empty/absent when confirmationStatus is "Unconfirmed" or null. |
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>" }.
| Status | Code | Description |
|---|---|---|
| 400 | invalid_payload | The request body is malformed, missing required fields, or the strategySnapshotJson fails validation. |
| 401 | api_key_missing | The x-api-key header wasn't provided. |
| 401 | api_key_invalid | The provided API key is invalid, revoked, or inactive. |
| 403 | service_not_enabled | This API key is not scoped for the live execution service — enable it in the Console when creating or editing the key. |
| 404 | not_found | The subscription doesn't exist or doesn't belong to your account. |
| 429 | rate_limit_exceeded | Too many requests for this account. Back off and retry. |