Skip to main content

Your First Request

Last updated November 4, 2026

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 /bots means GET 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 the Allow header 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 answers 400 bad_request.
  • Query parameters are strings. A yes/no filter takes true or false, for example GET /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 answers 400 bad_request with 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 413 at the edge or 400 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.10 and 0.1 are the same setting while 1e-1 is refused. Any number with an exponent larger than 64 is refused before the body is read.
  • An on/off setting takes true or false; 1 and 0 mean 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, /trash and /restore, the two DELETE requests and POST /portfolio-links/{linkId}/revoke. Nothing in a body sent to them is read. For POST /bots/{botId}/enable and POST /trading/resume the text sent still counts toward the Idempotency-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": "..."
}
  • ok says whether the request succeeded. Read it or the HTTP status; they always agree.
  • data holds the result, or null on an error.
  • error is null on success. On an error it holds code (see Errors), message (for people: build on code, never on the text), requestId, and sometimes data (extra detail: the missing permission, the current version, a reason) and fieldErrors (one item per invalid setting, with validation_failed).
  • apiVersion is rest-v1.
  • requestId identifies this request on our side, and the same value comes in the X-Request-Id header. 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

WhatHow it is written
MoneyA 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.
IdsStrings, 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.
VersionsA bot's version is a string and a Backtick's editVersion an integer. Send back the value you last read, unchanged; see Versions.
DatesYYYY-MM-DD.
TimesISO 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 peopleA 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 dayA 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.
SettingsBot 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 limit for the page size, within the endpoint's range, and nothing else.
  • The answer carries nextCursor. Pass it back unchanged as cursor for the next page. It is opaque: never build or edit one.
  • nextCursor is null on the last page. A page can hold fewer items than limit, or none, and still carry a nextCursor, for example when a client limited to some broker accounts is not shown the lines of other accounts. Keep reading while nextCursor is not null.
  • A cursor or limit the endpoint cannot read answers 400 bad_request with 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.