Skip to main content

User API: Running Backticks and Following a Run

Last updated November 4, 2026

This article shows how your program runs a Backtick, follows the run to the end by polling or by streaming events, and cancels one. It ends with a complete script sketch from create to results.

The plan you need is in Use the tradeSteward API From Your Own Code. Your API client needs backtick.run (Run Backticks) to start runs, backtick.cancel (Cancel runs) to cancel them, and backtick.view (View Backticks and results) to read status or open the event stream. Building the Backtick comes first: see Building Backticks and Positions. Only a draft or failed Backtick can be run; a completed one is cloned first.

How a run starts: queued or scheduled

When you ask for a run, the server either starts it now or schedules it to start later on its own. Which one you get depends on how busy the Backtick servers are and on your pace:

  • Queued (200, status queued): the run is in the Backtick queue. Follow it with the status read or the event stream.
  • Scheduled (202, status scheduled): the server holds the run and releases it when there is room. The answer carries positionInLine (1 is the next of your scheduled runs to start), reason (why it waits) and pollAfterSeconds. Do not send a scheduled run again: the server starts it, and a second request for the same Backtick answers the run that is already scheduled.

The pace follows Backtick server load: when the servers are busy your runs start more slowly, and one at a time as the servers pick them up. Your plan's Backtick allowance (how much work you may have queued at once) applies as well. A run that cannot start under these rules is scheduled rather than refused.

reasonWhy the run waits
servers_busyThe Backtick servers have no room for it yet.
pacingYour runs are spaced out to your plan's pace.
unverifiedThe pace could not be checked just now.
allowanceYour plan's Backtick allowance has no room for it yet.
faultA temporary problem on our side.
writes_offChanges are switched off for now.
grant_unavailableThe connection could not be checked just now.

The server tries again on its own in every case. Scheduled runs start in the order they were asked for. You can only have so many scheduled runs at once; past that a run request answers 429 rate_limited with the reason too_many_scheduled. A scheduled run that waits 24 hours without starting expires, and the Backtick goes back to being a draft (or a failed Backtick); the status read then reports it in lastScheduledRun.

While a run is scheduled the Backtick cannot be changed: an edit, a batch of changes, a position change or a SweepTick change answers 409 invalid_state with the reason scheduled. Cancel the run first. Trashing the Backtick, deleting it, running it on the website, revoking the connection that asked, or removing its backtick.run permission also ends a scheduled run.

  Runs, cancels, trashes and permanent deletes take turns for one account. A request that arrives while another is in progress answers 503 lock_busy with Retry-After: 2; nothing was done and it did not count toward your limit on changes. Send it again after that many seconds.

Starting runs

POST /backtests/validate Check a Backtick without saving it

Answers what POST /backtests or POST /backtests/{uid}/changes would answer, and keeps nothing. Operation id: validateBacktest. Permission: backtick.create (Create Backticks) to check a new Backtick, backtick.edit (Edit Backticks and positions) to check changes to one. No Idempotency-Key and no If-Match.

FormBodyChecked as
A new BacktickThe body of POST /backtests: name, the top-level fields, settings and positions. botIds is not taken.The create: its positions, its fields over your Backtick defaults, the date range of Backtick data, your plan's position limit and each position's settings.
Changes to a BacktickbacktestUID, changes (the list of POST /backtests/{uid}/changes) and, to check the version too, editVersion as a JSON whole number.The batch, on the Backtick as it is stored now: its state, a scheduled run, the version when sent, and every change in order. The changes are tried and undone.

A refusal is the one the write gives, and each field error carries a message naming the field, its allowed range and its unit. A pass answers 200 with valid true and runBlockers: what a run of the resulting Backtick would refuse, each with its field (null when it is about the positions) and the run's own message. Plan limits, your run pace and the free-trial data window are not checked. Checking changes to a Backtick is refused with 503 writes_disabled while API changes are switched off; checking a new Backtick is not. A check spends the validation limits that POST /bots/validate and POST /backtests/sweeps/preview spend.

POST /rest/v1/backtests/validate

{"name": "Condor 45 DTE", "positions": [{"name": "16d IC", "settings": {"...": "..."}}]}

"data": {
  "valid": true,
  "runBlockers": [{"field": "testPeriodEnd", "message": "Test period end date is required before queueing this backtest."}]
}

Refusals: those of POST /backtests for a new Backtick, and those of POST /backtests/{uid}/changes for changes, with 422 validation_failed on editVersion when it is not a whole number of 0 or more.

POST /backtests/{uid}/run Run one Backtick

Runs a draft or failed Backtick: started now, or scheduled for the server to start. Takes no body. Operation id: runBacktest. Permission: backtick.run (Run Backticks). Header: Idempotency-Key (required).

NameTypeRequiredMeaning
uidpath, stringYesThe Backtick id, from GET /backtests.

Returns 200 with backtestUID, status queued and pollAfterSeconds (15), or 202 with backtestUID, status scheduled, positionInLine, reason and pollAfterSeconds. The answer is the same few fields with or without backtick.view.

POST /rest/v1/backtests/a1B2c3D4e5F6/run
Idempotency-Key: run-a1B2c3D4e5F6-1

HTTP/1.1 202 Accepted
{"ok": true, "data": {"backtestUID": "a1B2c3D4e5F6", "status": "scheduled", "positionInLine": 1, "reason": "servers_busy", "pollAfterSeconds": 15}, "error": null, "apiVersion": "rest-v1", "requestId": "req_3xT8"}

Refusals: 409 invalid_state with the reason already_queued (it is already queued or running), already_run (completed; clone it) or in_trash; 409 backtick_quota with the reason position_limit (more positions than your plan allows in one run), run_allowance (the run would go over your plan's allowance) or trial (outside what a trial may test); 412 version_mismatch with the reason edited when the Backtick was edited while the run was being started (nothing was queued and nothing was counted; read it and send the run again); 429 rate_limited with the reason too_many_scheduled; 403 entitlement_required with the reason sweeptick_run when a SweepTick is run on a plan without SweepTick runs; 403 permission_denied with the reason connection_ended or permission_removed when the connection was revoked or lost backtick.run while the run was being started; 409 invalid_state with the reason queue_conflict (trying again does not help; contact support with the Backtick id); 503 lock_busy with Retry-After; 503 service_unavailable with the reason allowance_not_released (nothing was queued, part of your allowance may stay held for up to 24 hours, and the same key replays this answer: send a new request with a new key), with the reason allowance_unverified (the allowance could not be checked; try again shortly), or with outcome unknown (read the Backtick before trying again); 404 not_found.

POST /backtests/run Run up to 50 Backticks

Runs 1 to 50 draft or failed Backticks in one request, each as POST /backtests/{uid}/run would, in the order sent. One refused Backtick does not stop the others. Operation id: runBacktests. Permission: backtick.run (Run Backticks). Header: Idempotency-Key (required).

NameTypeRequiredMeaning
backtestUIDsarray of stringsYes1 to 50 Backtick ids, each once, in the order to run them.

Returns 200 whatever each Backtick answered, with data.items: one item per Backtick, in order, each with backtestUID and an outcome:

outcomeMeaningOther fields
queuedThe run is started.pollAfterSeconds
scheduledThe server starts it later on its own. Do not send it again.positionInLine, reason, pollAfterSeconds
refusedIt was not run.error with the code, message and data the single run would have answered (for example not_found, invalid_state, backtick_quota, or rate_limited with the reason too_many_scheduled)
notAttemptedThe request ended before it reached this Backtick. Send these Backticks again in a new request with a new Idempotency-Key; the same key answers this same result again.

Scheduled runs keep the order they were sent in, behind any run already scheduled. A request that runs many Backticks spends more of the limit on changes than a single run. Refusals of the whole request: 422 validation_failed on backtestUIDs for a repeated id, an empty list or more than 50 (spends nothing); 503 lock_busy with Retry-After while another run request of yours is being handled (nothing was done; send it again).

POST /backtests/live-history Backtick bots over their own live history

The website's Compare to Backtick: a saved Backtick of your bots from their first live entry to their last live exit, each bot with its settings of the time and without its days switched off, stored with a comparison and run. Operation id: createLiveHistoryBacktest. Permissions, all needed: backtick.create, backtick.run, backtick.view, bots.settings.view, bots.history.view, bots.performance.view. Header: Idempotency-Key (required). Body: botIds (1 to 20, required); optional name, from and to (the days searched), settingsAsOf (start or end), reconcileDisabledDays, excludeOneOff, excludePartial, excludeNormal, qtyOne, commissionProfile, startingBalance.

Returns 201 with backtestUID, window, bots, dropped, run and next (read the comparison with GET /backtests/{uid}/live-comparison once the run completes). Refusals: 422 validation_failed naming botIds (bot_not_found, no_enabled_entries, symbol_not_supported, no_live_trades, no_settings_history) or from (too_many_trades: over 2000 closed trades); 403 constraint_violation; 409 backtick_quota.

Following a run

There are two ways to find out how a run is going. Poll GET /backtests/{uid}/status no more than every 15 seconds when your program checks occasionally or watches many Backticks. Stream GET /backtests/{uid}/events when it wants each change as it happens and can hold a connection open; a stream sends the same status object the poll does, and ends on its own when the run ends. Both need backtick.view. Neither holds for a scheduled run: both answer straight away with the place in line, and you ask again after pollAfterSeconds.

GET /backtests/{uid}/status Where a Backtick stands

Returns the run status, progress and timing of one Backtick. Poll it no more than every 15 seconds. Operation id: getBacktestStatus. Permission: backtick.view (View Backticks and results).

NameTypeRequiredMeaning
uidpath, stringYesThe Backtick id.

Returns:

FieldMeaning
statusdraft, scheduled, queued, running, completed or failed.
progressWhile queued or running, the share of the run done (0 to 1); 1 when completed; otherwise null.
pollAfterSecondsWhile scheduled, queued or running: read again no sooner than this many seconds from now (15). Otherwise null.
completedAtWhen the run completed, in UTC; otherwise null.
errorMessageWhy a failed run failed. A draft whose scheduled run the server started and then put back carries the reason here too. A run you queued that was put back to a draft shows null here; the event stream gives its reason with the outcome refused. Otherwise null.
positionInLine, reasonOnly while scheduled: its place among your scheduled runs and why it waits.
lastScheduledRunOnly for a draft or failed Backtick with no scheduled run whose last scheduled run ended without starting, after the Backtick was last changed or run. state is refused (code is the error the run would have answered and reason its reason) or expired (it waited 24 hours; reason is ttl), and endedAt is when, in UTC. A run you ended yourself is not reported. Change the Backtick or run it again to start over.
{"ok": true, "data": {"status": "running", "progress": 0.42, "pollAfterSeconds": 15, "completedAt": null, "errorMessage": null}, "error": null, "apiVersion": "rest-v1", "requestId": "req_5bN0"}

Refusals: 404 not_found; 429 rate_limited when the reads limit is used up.

GET /backtests/{uid}/events Follow a run as a stream of events

Opens a Server-Sent Events stream (text/event-stream) that follows one Backtick's run until it ends, so your program does not poll. Operation id: streamBacktestEvents. Permission: backtick.view (View Backticks and results).

NameTypeRequiredMeaning
uidpath, stringYesThe Backtick id.

Send the access token in the Authorization header, as on every other request. The token is never accepted in the address. A browser's EventSource cannot set headers, and the stream sends no CORS headers and does not answer a browser's preflight request, so read it from a server-side or native client. A browser app needs its own backend that holds the token, opens the stream and relays the events. Any HTTP client that can read a response as it arrives works; with curl, -N prints each event as it comes:

curl -N -H "Authorization: Bearer $TOKEN" https://api.tradesteward.com/rest/v1/backtests/a1B2c3D4e5F6/events

The stream's format is standard SSE: each event is an event: line, a data: line holding JSON, and a blank line. The events, in the order a stream sends them:

EventWhenData
retry: 5000First, as a bare field.How many milliseconds to wait before opening a new stream after a drop.
statusWhen the stream opens, and each time the status, the progress or the outcome changes.The object GET /backtests/{uid}/status returns, without pollAfterSeconds, plus outcome (table below). errorMessage is set for a failed run and for a refused one. A scheduled Backtick keeps pollAfterSeconds: open a new stream after that many seconds.
doneOnce, after the last status, when there is nothing more to follow: the run ended, or the Backtick has nothing to wait for (not_run, pending). Read outcome to tell which.The final status object, outcome, and summary: the Backtick as GET /backtests lists it, with its headline metrics (a SweepTick has none, because its positions are alternatives), for a completed run; null for any other outcome, and null when it could not be read (read GET /backtests/{uid}/results then). If the run had already ended when you connected, you get status and then done at once.
errorJust before a close whose reason is error.code, message and requestId.
closeLast event of every stream.reason (table below). After done its reason is done.
: keep-aliveAfter 15 seconds with nothing else sent.A comment line. Event-stream parsers ignore it.
retry: 5000

event: status
data: {"status":"running","progress":0.42,"completedAt":null,"errorMessage":null,"outcome":"in_progress"}

event: done
data: {"status":"completed","progress":1,"completedAt":"2026-11-03T14:20:51Z","errorMessage":null,"outcome":"completed","summary":{"backtestUID":"a1B2c3D4e5F6","name":"SPX 10 Delta Put Spread 9:31a","status":"completed","metrics":{"totalProfit":"18420.00","tradeCount":488,"dayWinRate":0.8156,"maxDrawdown":"4210.00"}}}

event: close
data: {"reason":"done"}

Outcomes. An outcome says what became of a run. The stream and the MCP wait tool use the same words:

outcomeMeaning
completedThe run finished. Its headline metrics come with the done event (a SweepTick has none, because its positions are alternatives).
failedThe run failed. errorMessage says why.
cancelledThe run was scheduled, queued or running while it was watched and then went back to a draft (or, for a failed Backtick, its scheduled run was cancelled), or it was cancelled and queued or scheduled again as a new run. When a new run replaced it, the status shown is the new run's: open a new stream to follow it.
refusedThe run was sent back to a draft because it could not go ahead (for example a trial limit) and errorMessage says why, or a scheduled run ended without starting and lastScheduledRun says why.
not_runThe Backtick is a draft that was never queued or scheduled, so there is nothing to wait for.
pendingThe Backtick is waiting to be queued, so there is nothing to wait for yet.
in_progressThe run is queued or running.
scheduledThe Backtick has a scheduled run. The stream sends one status event with positionInLine, reason and pollAfterSeconds, then closes with the reason scheduled. Open a new stream after pollAfterSeconds.
not_foundNo Backtick in your account has that id, or it is in the trash or deleted. On the stream this is the close reason not_found.

When a stream ends. Every event is a full snapshot, so a new stream needs nothing from the old one: there are no event ids to replay. A stream lasts at most 5 minutes and never longer than the access token that opened it. Once a minute the server checks that the client may still read the Backtick. If the connection drops without a close event, wait the retry delay and open a new stream.

Close reasonWhat happenedOpen a new stream?
doneThe run has ended and the done event has been sent.No.
scheduledThe Backtick has a scheduled run the server has not started yet. Its status event came first and no done follows.Yes, after pollAfterSeconds. Opening is limited, so do not reopen at the retry delay.
max_durationThe stream reached its longest life, 5 minutes.Yes. The new stream starts with a full snapshot.
token_expiredThe access token that opened the stream reached its end. A stream never outlives its token.Yes, with a new token.
access_revokedThe API client was revoked or suspended, or it no longer holds backtick.view or a plan that includes API access to Backticks.No, until access is restored.
not_foundThe Backtick was moved to the trash or deleted while the stream was open.No.
lease_lostThe stream lost its place among your open streams: a newer stream for the same Backtick from the same client took it, or the server could not keep it.Yes, after the retry delay, unless you opened that newer stream yourself: opening again would end it.
server_shutdownThe server stopped serving the stream.Yes, after the retry delay.
errorA problem on our side. An error event came first.Yes, after the retry delay.

Limits and refusals. An account can have 1 stream open at a time, counting every kind of stream (a bot event stream too). A new stream for the same Backtick from the same client takes the place of its older one, which ends with lease_lost, so a reconnecting client is never refused by its own dead stream. Any other new stream while one is open (a different Backtick, a bot event stream, or another client) is refused with 429 rate_limited: close the first before opening the second. Opening a stream is limited. A stream does not count toward the requests that may run at the same time. A request that cannot start a stream gets the usual JSON envelope and no event: 401 invalid_token; 403 permission_denied or entitlement_required; 404 not_found; 429 rate_limited when the account already has a stream open or another client holds one (Retry-After 15), or when the open limit is reached (Retry-After is the seconds left in that minute); 503 service_unavailable with Retry-After 15 when streams are temporarily unavailable or the server has no room. Wait the seconds given before trying again; a 401, 403 or 404 does not clear by waiting.

Cancelling

POST /backtests/{uid}/cancel Cancel a run

Cancels a Backtick that is queued or running, or ends a run the server has scheduled and not yet started. Takes no body. Operation id: cancelBacktest. Permission: backtick.cancel (Cancel runs). No Idempotency-Key.

NameTypeRequiredMeaning
uidpath, stringYesThe Backtick id.

Returns 200 with the Backtick as every write answers it (see What a write answers). Cancelling a queued or running run makes the Backtick a draft, even one that had failed before. Cancelling a scheduled run queues nothing and leaves the Backtick as it was: a draft stays a draft and a failed Backtick stays failed. Either way it can be edited and run again. Refusals: 409 invalid_state with the reason not_running when the Backtick is neither queued, running nor scheduled, or in_trash; 503 lock_busy when the cancel meets a run of this Backtick being started at that moment (nothing changed; Retry-After is 5 seconds), or with Retry-After: 2 when another run, cancel, trash or delete of your account is in progress; 503 service_unavailable when the server cannot read whether the Backtick has a scheduled run (nothing changed); 404 not_found.

A complete script sketch

This Python sketch creates a Backtick, adds two positions in one batch, runs it, follows the run to the end (streaming when the run is queued, waiting when it is scheduled), and reads the results. It uses only the requests library. Replace the token step with the client-credentials exchange from Authentication and Access Tokens, and the position settings with real ones from GET /schema/bot-settings.

import json, time, uuid, requests

API = "https://api.tradesteward.com/rest/v1"
TOKEN = get_access_token()          # your client-credentials exchange
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

def call(method, path, body=None, if_match=None, idem=True):
    headers = dict(H)
    if idem:
        headers["Idempotency-Key"] = str(uuid.uuid4())
    if if_match is not None:
        headers["If-Match"] = str(if_match)
    r = requests.request(method, API + path, headers=headers, data=json.dumps(body) if body else None)
    env = r.json()
    if not env["ok"]:
        raise RuntimeError(f'{r.status_code} {env["error"]["code"]}: {env["error"]["message"]}')
    return r.status_code, env["data"]

# 1. Create
_, bt = call("POST", "/backtests", {
    "name": "SPX 10 Delta Put Spread 9:31a",
    "testPeriodStart": "2024-01-02", "testPeriodEnd": "2025-12-31",
    "startingBalance": "100000",
})
uid, version = bt["backtestUID"], bt["editVersion"]

# 2. Change: two positions in one batch
_, bt = call("POST", f"/backtests/{uid}/changes", {
    "changes": [
        {"action": "addPosition", "name": "10d PCS 9:31", "settings": POSITION_A},
        {"action": "addPosition", "name": "10d PCS 10:00", "settings": POSITION_B},
    ]
}, if_match=version)
version = bt["editVersion"]

# 3. Run
status_code, run = call("POST", f"/backtests/{uid}/run")

# 4. Follow to the end
def follow(uid):
    while True:
        with requests.get(API + f"/backtests/{uid}/events", headers=H, stream=True, timeout=330) as r:
            if r.status_code in (429, 503):
                time.sleep(int(r.headers.get("Retry-After", "15")))
                continue
            if r.status_code != 200:
                env = r.json()
                raise RuntimeError(f'{r.status_code} {env["error"]["code"]}: {env["error"]["message"]}')
            event, data = None, None
            for raw in r.iter_lines(decode_unicode=True):
                if raw == "":
                    if event == "done":
                        return data
                    if event == "close":
                        if data["reason"] == "done":
                            return None
                        if data["reason"] in ("access_revoked", "not_found"):
                            raise RuntimeError(data["reason"])
                        # scheduled: come back after pollAfterSeconds (sent in the status event).
                        # token_expired: get a new token first.
                        # max_duration, lease_lost, server_shutdown, error: reopen after the retry delay.
                        if data["reason"] == "token_expired":
                            H["Authorization"] = f"Bearer {get_access_token()}"
                        time.sleep(poll_after if data["reason"] == "scheduled" else 5)
                        break
                    event, data = None, None
                elif raw.startswith("event:"):
                    event = raw[6:].strip()
                elif raw.startswith("data:"):
                    data = json.loads(raw[5:].strip())
                    if event == "status" and data.get("pollAfterSeconds"):
                        poll_after = data["pollAfterSeconds"]
                # "retry:" and ": keep-alive" lines are ignored

done = follow(uid)
if done is None or done["outcome"] != "completed":
    raise SystemExit(f'run ended: {done and done["outcome"]} {done and done.get("errorMessage")}')

# 5. Read results
_, summary = call("GET", f"/backtests/{uid}/results", idem=False)
_, series = call("GET", f"/backtests/{uid}/results/series?maxPoints=120", idem=False)
print(summary["metrics"], len(series["points"]), "points")

Things the sketch takes care of that a real program should keep: it never opens a second stream while one is open, it waits pollAfterSeconds (not the retry delay) after a scheduled close, it stops after done, it treats a 429 or 503 on opening as a wait and any other refusal as an error, and it gets a new token before reopening after token_expired. If you follow many Backticks at once, poll GET /backtests/{uid}/status every 15 seconds for each instead, since an account has one stream.