Your First Request
With a token in hand, your program makes its first call: GET /me, which says what the client may do. This step walks through that call and the rules every request and answer follows, so your program can read any answer the API gives.
Your first call
With curl, where TS_CLIENT_ID and TS_CLIENT_SECRET hold the values you copied when you created the client:
TOKEN=$(curl -s -u "$TS_CLIENT_ID:$TS_CLIENT_SECRET" \
-d grant_type=client_credentials \
https://api.tradesteward.com/oauth/token | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')
curl -s -H "Authorization: Bearer $TOKEN" https://api.tradesteward.com/rest/v1/me
curl -s -H "Authorization: Bearer $TOKEN" https://api.tradesteward.com/rest/v1/bots
GET /me answers with what this client may do and your plan, and, when it may view bots, your broker accounts and whether trading is paused. Every client may call it, whatever permissions it holds, so it is the right first call and the first thing to check when a request is refused. Part of its data:
{
"connection": {"kind": "api_client", "label": "Portfolio dashboard"},
"permissions": {"bots.view": true, "bots.create": true, "bots.enable": false},
"constraints": {"accounts": null, "onlyOwnBots": false},
"trading": {"paused": false, "pauseLocked": false}
}
Every field is explained in the GET /me reference. GET /bots then lists your bots, with whether each is enabled and live.
The same thing in Python, using only the standard library. It follows the sample on the Developers page: the token request is a form post with Basic authentication, and every API answer, success or error, is JSON of the same shape, so an error body is read too.
import base64, json, os, urllib.error, urllib.parse, urllib.request
API = "https://api.tradesteward.com"
def get_token(client_id, client_secret):
basic = base64.b64encode(
(urllib.parse.quote(client_id, safe="") + ":" + urllib.parse.quote(client_secret, safe="")).encode()
).decode()
request = urllib.request.Request(
API + "/oauth/token",
data=urllib.parse.urlencode({"grant_type": "client_credentials"}).encode(),
headers={"Authorization": "Basic " + basic, "Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)["access_token"]
except urllib.error.HTTPError as err:
body = json.load(err)
raise RuntimeError("token request refused: " + body.get("error", "") + ": " + body.get("error_description", ""))
def get(path, token):
request = urllib.request.Request(API + "/rest/v1" + path, headers={"Authorization": "Bearer " + token})
try:
with urllib.request.urlopen(request, timeout=30) as response:
body = json.load(response)
except urllib.error.HTTPError as err:
body = json.load(err)
if not body["ok"]:
raise RuntimeError(body["error"]["code"] + ": " + body["error"]["message"])
return body["data"]
token = get_token(os.environ["TS_CLIENT_ID"], os.environ["TS_CLIENT_SECRET"])
me = get("/me", token)
print(me["connection"]["label"], "may:", [p for p, on in me["permissions"].items() if on])
for bot in get("/bots", token)["bots"]:
print("enabled" if bot["enabled"] else "disabled", bot["botName"])
Addresses, methods and the reference
- Every path is under
https://api.tradesteward.com/rest/v1. This guide writes paths relative to it:GET /botsmeansGET https://api.tradesteward.com/rest/v1/bots. The token address,/oauth/token, answers in the OAuth form, not in the API envelope. - The full reference is an OpenAPI 3.1 document,
openapi.json, downloaded from the Developers page. It names every operation, its parameters and its permission, and most API tools can generate a client from it. The page also has the two sample programs, Python and JavaScript (Node), that get a token and list your bots. - Use the method the reference names for each path. Another method answers
405 method_not_allowed, and theAllowheader names the methods the path serves. - Path segments after the ones a route names answer
404 not_found. A path with an empty segment, a dot segment or an encoded slash answers400 bad_request. - Query parameters are strings. A yes/no filter takes
trueorfalse, for exampleGET /bots?enabled=true. - The API acts only on the account that created the API client. There is no account parameter to switch.
Sending a request body
A request whose reference lists a request body takes a JSON object:
- Send
Content-Type: application/json. Any other content type answers400 bad_requestwith the message "Send the request body as JSON, with the header Content-Type: application/json." - The top level must be a JSON object, not a list or a bare value. A body may be at most 256 KB; a larger one is refused, with
413at the edge or400 bad_request. Nesting is limited to 64 levels, which no API object comes near. - Write numbers plainly. A bot setting takes a number without an exponent, with at most 15 significant digits and no more decimals than the setting keeps. The API compares what you wrote digit for digit with what it saved, so
0.10and0.1are the same setting while1e-1is refused. Any number with an exponent larger than 64 is refused before the body is read. - An on/off setting takes
trueorfalse;1and0mean the same. - Some requests take no body:
POST /bots/{botId}/enable,POST /bots/{botId}/disable,POST /bots/{botId}/preview,POST /trading/pause,POST /trading/resume,POST /backtests/{uid}/clone,/run,/cancel,/trashand/restore, the twoDELETErequests andPOST /portfolio-links/{linkId}/revoke. Nothing in a body sent to them is read. ForPOST /bots/{botId}/enableandPOST /trading/resumethe text sent still counts toward theIdempotency-Key, so send no body, or the same one on a retry.
The response envelope
Every answer under /rest/v1 is JSON with the same five members, on success and on error alike:
{
"ok": true,
"data": { "...": "the result" },
"error": null,
"apiVersion": "rest-v1",
"requestId": "..."
}
{
"ok": false,
"data": null,
"error": {
"code": "permission_denied",
"message": "This connection is not allowed to do that.",
"requestId": "...",
"data": { "permission": "bots.enable" }
},
"apiVersion": "rest-v1",
"requestId": "..."
}
oksays whether the request succeeded. Read it or the HTTP status; they always agree.dataholds the result, ornullon an error.errorisnullon success. On an error it holdscode(see Errors),message(for people: build oncode, never on the text),requestId, and sometimesdata(extra detail: the missing permission, the current version, a reason) andfieldErrors(one item per invalid setting, withvalidation_failed).apiVersionisrest-v1.requestIdidentifies this request on our side, and the same value comes in theX-Request-Idheader. Quote it when you contact support.
Every answer is sent with Cache-Control: no-store. The exceptions to the envelope are the event streams (GET /backtests/{uid}/events and GET /bots/events), which answer with Server-Sent Events once open, and the chart (GET /backtests/{uid}/results/chart), an SVG. A refusal of any of them still comes in the envelope.
Money, numbers, dates and times
| What | How it is written |
|---|---|
| Money | A decimal string in US dollars with two places, such as "1250.00" or "-37.50", never a float. A zero is "0.00", never "-0.00". A money field with no value is null. |
| Ids | Strings, including ones that look numeric: a bot id, a Backtick's backtestUID, an action id (a_ and 22 hex characters), an event id. Send them back exactly as read. |
| Versions | A bot's version is a string and a Backtick's editVersion an integer. Send back the value you last read, unchanged; see Versions. |
| Dates | YYYY-MM-DD. |
| Times | ISO 8601 in UTC with a Z: 2026-11-03T14:30:00Z. This is how createdAt, expiresAt, decidedAt and the time of a bot event are written. |
| Times for people | A field whose name ends in Eastern, such as expiresAtEastern on a chart link or a portfolio link, is the same moment as text in US Eastern time, such as Nov 3, 2026 4:15 PM EST. Show that one to a person; use the UTC field in code. Times on the tradeSteward website are US Eastern. |
| Times of day | A bot's or an entry's time of day, such as an entry's entryStart, is HH:MM:SS in US Eastern time, the market's clock. |
| Settings | Bot and Backtick position settings travel as one flat JSON object of named settings. GET /schema/bot-settings is the dictionary of every setting: its type, allowed values, units and when it applies. |
Paging
Lists that can grow long are paged with a cursor, the same way everywhere (bot trades, fills and logs, the list of Backticks, and the positions and trades results views):
- For the first page, send
limitfor the page size, within the endpoint's range, and nothing else. - The answer carries
nextCursor. Pass it back unchanged ascursorfor the next page. It is opaque: never build or edit one. nextCursorisnullon the last page. A page can hold fewer items thanlimit, or none, and still carry anextCursor, for example when a client limited to some broker accounts is not shown the lines of other accounts. Keep reading whilenextCursoris notnull.- A cursor or limit the endpoint cannot read answers
400 bad_requestwith the message "Invalid cursor.", "Invalid limit." or "Invalid cursor or limit." A Backtick trades cursor belongs to one run; after the Backtick is run again it is refused, so start again without one.
Each endpoint's article gives its page sizes; for example the Backtick positions view comes 25 to a page with limit up to 50, and its trades 50 to a page.
Compressed answers
JSON answers are compressed for any client that sends Accept-Encoding: gzip, once an answer is at least 256 bytes long. Most HTTP libraries send the header and unpack the answer by themselves; send it yourself when yours does not, because results views and bot logs can be large. Event streams are never compressed.
Where the field notes live
Most response fields mean what their names say. For the ones that do not, GET /schema/response-fields (operation id getResponseFields) answers a set of notes grouped by area: bots, trades, logs, pending actions, Backticks, results, links and so on. It needs bots.view (View bots) or backtick.view (View Backticks and results). Read it once when you start, and again when a field surprises you. It is described with the other schema endpoints in User API: Account, Trading and Schema Endpoints.