Using the API from an AI coding agent
Machine-readable surfaces
| URL | What it is |
|---|---|
https://thrustlab.com/openapi.json | The full OpenAPI document. Generate a client from it. |
https://thrustlab.com/llms.txt | A one-line-per-page index of the documentation. |
https://thrustlab.com/llms-full.txt | Every documentation page as one plain-text file. |
https://thrustlab.com/docs/guides/errors#<code> | The row for one error code. Every error's doc_url is this URL. |
An agent that hits an error can fetch the doc_url it was given and read that code's row without searching.
The base URL is https://thrustlab.com, so every endpoint is https://thrustlab.com/v1/....
The Python SDK reads THRUSTLAB_API_KEY from the environment. THRUSTLAB_BASE_URL overrides the base URL.
Drop this into your agent's instructions
Put this block in a project's AGENTS.md or CLAUDE.md.
## ThrustLab API
Read the key from the `THRUSTLAB_API_KEY` environment variable.
Never write a key into a file, a commit, or a log line.
Use base URL `https://thrustlab.com`; every endpoint is under `/v1/`.
Send `Authorization: Bearer $THRUSTLAB_API_KEY`.
Prefer the Python SDK (`pip install thrustlab`) over hand-rolled HTTP.
Send an `Idempotency-Key` header on every POST, PATCH and DELETE.
Reuse the same key when retrying the same logical write; a different body under
the same key is rejected with 409.
Poll async runs every 2 to 5 seconds and back off when nothing changes.
Never poll in a tight loop.
On any 429, wait the number of seconds in the `Retry-After` header. Do not use a
fixed backoff: the four budgets have windows of a minute, an hour and a day.
Submission limits, per account, per minute: 30 to `POST /v1/sweeps`, 30 to
`POST /v1/dynamic-simulations`, 120 to `POST /v1/simulations`.
To evaluate many combinations, send ONE sweep with a `component_axes` axis
rather than a loop of single-point runs. A loop burns the submission limit and
recompiles the solver for every point.
`sweep.<axis>.steps` is the NUMBER OF POINTS, not a step size:
{"start": 40, "stop": 100, "steps": 4} gives 40, 60, 80, 100.
Do not scrape the component catalog. `GET /v1/components` filters and sorts
server-side (`kv[gte]`, `resistance[lte]`, `sort_by`), and `ids_only=true`
returns up to 5000 ids in one response with a `truncated` flag.
`GET /v1/components/{id}/specs` is metered at 60 distinct components an hour and
300 a day, so read it only for rows you have already narrowed to.
For a dynamic run, read `progress` on the resource to see how far it has got.
The `estimated_duration_s` from `/estimate` is simulated mission time, not wall
time.
To stop work, call `POST /v1/simulations/cancel-selected` with the run ids
rather than cancelling one at a time. Ids returned under `requested` are still
running; poll them until they turn `canceled`.
Every error carries `type`, `code`, `request_id` and `doc_url`. Branch on
`code`, log `request_id`, and fetch `doc_url` when the code is unfamiliar.
Treat ramps from near-zero duty as a risk: one external study observed
non-converged rows in their first milliseconds, enough of a short flight's rows
to trigger `solver_no_convergence`.
Start ramps at a duty the ESC runs at, or step to the target.
One sweep beats a loop of single points
A component_axes entry is {"axis": "propeller"|"motor"|"battery", "slots": [<rotor group index>, ...], "component_ids": [...]} and needs at least two ids.
It crosses with the numeric axes, so four propellers and four throttles is one 16-point grid rather than sixteen requests.
Each returned point names the component it was solved with in point["inputs"]["component"], a list of {axis, slot, id, name}.
A sweep returns names and ids only, never a catalog component's spec values.
sweep_config.component_axes[] on the sweep resource mirrors the same structure: {axis, slot, slots, components: [{id, name}]}.
"""Component axis x throttle: auth -> submit -> wait() -> inspect points.
Run it:
export THRUSTLAB_API_KEY=key_... # never hard-code the key
python examples/sweeps/component_axis_grid.py
Crosses a propeller component axis with a throttle range, so one request solves
every propeller at every throttle. It takes the first four catalog propellers
from client.components.list(type="propeller") and crosses them with four
throttle settings: 16 points in one submission.
Each returned point names the propeller that produced it at
point["inputs"]["component"], a list of {axis, slot, id, name}.
Sixteen single-point submissions would be sixteen requests against the
per-minute submission limit and sixteen cold solves; this is one request and
one warm solver.
`sweep.throttle.steps` is the NUMBER OF POINTS, not a step size.
"""
from thrustlab import Client
client = Client() # reads $THRUSTLAB_API_KEY from the environment
project = client.projects.create(name="sdk component-axis example")
# The fixed half of the configuration — same verification combo as
# examples/sweeps/run_and_poll.py. Resolve by name, or paste explicit IDs.
motor = client.components.find(name="BadAss 2826-820Kv")
battery = client.components.find(
name="Liperior 5000mAh 4S 35C 14.8V Lipo Battery With XT90 Plug"
)
# The swept half: the first four catalog propellers. `.data` is the first page
# of the cursor pager, so this is one request. A component axis needs at least
# two IDs — a one-value axis is not a sweep, and the server rejects it with
# `invalid_component_axis`.
props = client.components.list(type="propeller", limit=4).data
prop_ids = [p["id"] for p in props]
print("propeller axis:", ", ".join(p["name"] for p in props))
sweep = client.sweeps.create(
project_id=project["id"],
battery_component_id=battery["id"],
airspeed_m_s=0.0,
density_kg_m3=1.225,
battery_charge_pct=100,
# `propeller_component_id` here is the BASE value for slot 0. The component
# axis below overrides it at every point, so it only has to be valid.
rotor_groups=[
{
"label": "main",
"count": 4,
"motor_component_id": motor["id"],
"propeller_component_id": prop_ids[0],
"throttle_pct": 70,
}
],
sweep={
# One bool per rotor group: this group follows the swept throttle axis.
"rotor_sweep_mask": [True],
# `steps` is the NUMBER OF POINTS, not a step size: 40, 60, 80, 100.
"throttle": {"mode": "range", "start": 40, "stop": 100, "steps": 4},
# `slots` indexes rotor_groups. Two or more slots would make this ONE
# shared grid dimension applied to each of them, not a cross-product.
"component_axes": [
{"axis": "propeller", "slots": [0], "component_ids": prop_ids},
],
},
)
print(f"created {sweep['id']}: {sweep['total_points']} points in one request")
result = client.sweeps.wait(sweep["id"], timeout=900)
print(f"final status: {result['status']}")
if result["status"] == "completed":
print("\npropeller | throttle | thrust/rotor (N)")
for pt in client.sweeps.list_points(sweep["id"]):
# `inputs["component"]` names the component(s) this point was solved
# with: one {axis, slot, id, name} entry per active component axis.
# It carries names and IDs only — never the component's spec values.
chosen = ", ".join(c["name"] for c in pt["inputs"]["component"])
thrust = pt["rotors"]["1"]["thrust_n"]
print(f" {chosen:<18} | {pt['inputs']['throttle']:>7}% | {thrust:.2f}")
The sweep resource carries credits_breakdown: {points, rotors, cu_per_point_per_rotor, total}, so the cost of a grid is readable before and after the run rather than inferred from the balance.
input_snapshot on a sweep is the request body as submitted, so an agent can read back exactly what it asked for instead of keeping its own copy.
What the errors mean
The full catalog is at Errors, one row per code, and every error's doc_url deep-links to its row.
code | What to do |
|---|---|
create_rate_limited | Wait Retry-After seconds. Consider one sweep instead of many submissions. |
spec_rate_limited | The datasheet budget is spent. Use the list projection instead of /specs. |
unknown_filter | The filter key is not one this component type carries. The message lists the valid keys. |
solver_no_convergence | The run was rejected. Check error.details, and start throttle ramps above the ESC's minimum duty. |
See Rate limits for the budgets and Async resources for the run lifecycle.