Build with the physics engine
A key-authenticated HTTP API over the same engine and records the app uses. Analyze any build, work out radio link budgets and balance, read a flight log after an incident, and file and read your organization's flights, checklists, missions and maintenance records from your scripts, ground stations, notebooks or AI assistant.
Import the OpenAPI file into editor.swagger.io or any client for a full interactive reference, or load the Postman collection and set base_url and api_key.

Quickstart
A key, a bearer header and one request. The same endpoint analyzes a multirotor, a wing or a VTOL.
Create your key
Sign in, open Organization, Integrations, API keys, tick the areas the key needs, choose read only or read and write and when it ends, and click Create API key. It is shown once.
Authenticate
Send the key as a bearer token (the header X-API-Key is also accepted). The base URL for this instance is https://app.rotorlab.app/api/v1.
Analyze a build
POST a build to /analyze and read out: all-up weight, thrust-to-weight, hover and cruise endurance, currents and top speed, with charts and checks beside them.
1 · Create your key
Sign in, open Organization → Integrations, API keys, tick the areas the key needs, choose read only or read and write and when it ends, and click Create API key. The key is shown once: copy it somewhere safe. It looks like rl_…. Create a separate named key for each
integration; revoking a compromised key leaves the others working. A read-only key reads records and runs the engine and never changes a record.
Your organization decides who may create keys (by default organization admins and roles with Integrations, manage) and the longest a key may last, and its admins can see and revoke every person's key. Ground stations and other
equipment use a device credential instead: it belongs to your organization rather than to
an individual, so it remains valid after the person who created it leaves, and it reaches only
the status, flights and checklists areas: filing flights, checklists and logs, and reading the
roster, the register, mission plans and the maintenance records.
2 · Authenticate
Send your key as a bearer token (the header X-API-Key is also accepted). Base URL for this instance is https://app.rotorlab.app/api/v1.
curl -H "Authorization: Bearer rl_your_key" \
https://app.rotorlab.app/api/v1/version
3 · Analyze a build
curl -X POST https://app.rotorlab.app/api/v1/analyze \
-H "Authorization: Bearer rl_your_key" \
-H "Content-Type: application/json" \
-d '{"airframe_type":"Quad X","motor_count":4,"prop_diameter_in":7,"motor_kv":1700}'
The response has out (headline numbers: AUW, TWR, hover/cruise endurance, currents, top speed…), charts (inline SVG), checks ([level, message] pairs), plus lint, platform, and cg.
More examples: wings and VTOL, RF, CG, Python and JavaScript
4 · The same endpoint analyzes a wing or a VTOL
Send a fixed-wing or VTOL airframe_type with your wing dimensions and out carries the winged figures (stall and cruise speed, best climb rate, wing loading) instead of (or, for a VTOL, alongside) the hover numbers.
curl -X POST https://app.rotorlab.app/api/v1/analyze \
-H "Authorization: Bearer rl_your_key" -H "Content-Type: application/json" \
-d '{"airframe_type":"Fixed wing (tractor)","wing_span_mm":1800,"wing_chord_mm":220,"wing_cl_max":1.2,"motor_kv":900,"prop_diameter_in":11}'
# out.stall_speed_kmh · out.cruise_speed_kmh · out.climb_rate_ms · out.wing_loading_n_m2
GET /api/v1/airframes lists every supported type. Two more physics endpoints stand on their own:
# radio link budget: one link, or control + video together
curl -X POST https://app.rotorlab.app/api/v1/rf \
-H "Authorization: Bearer rl_your_key" -H "Content-Type: application/json" \
-d '{"freq_mhz":5800,"tx_power_mw":1000,"rx_gain_dbi":13,"rx_sens_dbm":-90,"tx_height_m":100,"rx_height_m":2}'
# link.max_range_km · link.fresnel_radius_m · link.limiter
# center of gravity and balance for a build
curl -X POST https://app.rotorlab.app/api/v1/cg \
-H "Authorization: Bearer rl_your_key" -H "Content-Type: application/json" \
-d '{"airframe_type":"Quadplane (lift + pusher)","wing_span_mm":1800,"body_length_mm":1000}'
# cg.cg_mm · cg.cg_pct_mac · cg.total_mass_g
Python
import requests
BASE = "https://app.rotorlab.app/api/v1"
h = {"Authorization": "Bearer rl_your_key"}
r = requests.post(f"{BASE}/analyze", headers=h,
json={"airframe_type": "Quad X", "motor_count": 4, "prop_diameter_in": 7})
out = r.json()["out"]
print(f"AUW {out['auw_g']:.0f} g · TWR {out['twr']:.2f} · hover {out['hover_min']:.1f} min")
# save it to your library
requests.post(f"{BASE}/builds", headers=h,
json={"name": "My quad", "params": {"airframe_type": "Quad X", "motor_count": 4}})
# read a log after an incident (.bin/.tlog/.ulg/DJI .txt/Parrot or GUTMA
# .json/flight-log .csv/Blackbox .bbl: detected from the bytes)
with open("flight.bin", "rb") as f:
A = requests.post(f"{BASE}/crash", headers=h, params={"name": "flight.bin"},
data=f.read()).json()
print(A["meta"]["vehicle_label"], A["crash"]["reason"], A["crash"]["confidence"])
for cat, why in A.get("preconditions", {}).get("findings", []):
print(f" [{cat}] {why}")
# the deeper diagnostics: severity-sorted, each naming the section it came from
for f in A.get("diagnostics", {}).get("findings", []):
print(f" [{f['level']}] {f['section']}: {f['title']}")
bat = A.get("diagnostics", {}).get("battery", {})
if bat.get("available"): # always check before reading
print("pack internal resistance:", bat["internal_resistance_mohm"], "mohm")
JavaScript (fetch)
const BASE = "https://app.rotorlab.app/api/v1";
const h = { "Authorization": "Bearer rl_your_key", "Content-Type": "application/json" };
const res = await fetch(`${BASE}/analyze`, { method: "POST", headers: h,
body: JSON.stringify({ airframe_type: "Quad X", motor_count: 4, prop_diameter_in: 7 }) });
const { out } = await res.json();
console.log(out.twr, out.hover_min);
Endpoints
Records are your organization's and builds are your own; anything else answers 404. The OpenAPI file carries the same facts on every operation.
All paths are under https://app.rotorlab.app/api/v1.
Key area is the area a key must include (status comes with every key);
Metered says whether a call spends a unit. The OpenAPI file carries the same two facts on every
operation as x-area and x-metered, with request and
response schemas.
| Method | Path | Description | Key area | Metered |
|---|---|---|---|---|
| GET | /version | API name, version, author | status | no |
| GET | /usage | Your quota, credits, and the key making this call | status | no |
| GET | /airframes | Supported airframe types and their defaults | engine | yes |
| GET | /catalog | Payload parts catalog (controllers, computers, sensors, radios) | engine | yes |
| POST | /analyze | Analyze a build and return full performance results | engine | yes |
| POST | /rf | Radio link budget and range | engine | yes |
| POST | /cg | Center of gravity and balance for a build | engine | yes |
| GET | /builds | List your saved builds | builds | yes |
| POST | /builds | Create or overwrite a build by name | builds | yes |
| GET | /builds/{id} | Fetch one of your builds | builds | yes |
| DELETE | /builds/{id} | Delete one of your builds | builds | yes |
| POST | /crash | Analyze a flight log after an incident | engine | yes |
| GET | /crash/{token} | A queued log reading | engine | no |
| GET | /flights | List flights on the organization’s record | flights | no |
| POST | /flights | Record a flight (create, or correct one already sent) | flights | no |
| GET | /flights/{id} | One flight | flights | no |
| DELETE | /flights/{id} | Remove a flight from the record | flights | no |
| POST | /flights/{id}/log | Attach the log file to a flight already on the record | flights | no |
| GET | /checklists | Checklist templates, and completed runs | checklists | no |
| POST | /checklists | File a completed checklist | checklists | no |
| GET | /missions | List the organization’s mission records | flights | no |
| POST | /missions | Save or update a mission record | missions | no |
| GET | /missions/{id} | One mission record, payload included | flights | no |
| DELETE | /missions/{id} | Delete a mission record | missions | no |
| GET | /missions/{id}/plan | The mission as a QGroundControl .plan | flights | no |
| POST | /missions/import | Convert a .plan into a mission payload | missions | no |
| GET | /workorders | List work orders | flights | no |
| GET | /workorders/{id} | One work order | flights | no |
| GET | /components | List components | flights | no |
| GET | /types | List aircraft types | flights | no |
| GET | /types/{id} | One type | flights | no |
| GET | /bulletins | List service bulletins | flights | no |
| GET | /bulletins/{id} | Bulletin compliance | flights | no |
| GET | /health | Fleet airworthiness map | flights | no |
| GET | /health/{id} | One aircraft’s health | flights | no |
| GET | /pilots | Who a flight may be filed for, with where each pilot stands | flights | no |
| GET | /aircraft | The aircraft register | flights | no |
| GET | /jobs | Jobs with the client's PO, status, flying minutes and billed-through date. The key must name this area: a key with no areas does not read it | jobs | no |
| GET | /jobs/{id} | One job | jobs | no |
| GET | /clients | Clients by name and handle, with no contact details | jobs | no |
| GET | /clients/{id} | One client | jobs | no |
| GET | /ml/scores | Advisory model scores on the organization’s records | flights | no |
| POST | /ml/scores | Write an advisory score | flights | no |
A path that is not listed answers 404
and is never metered. A listed path asked with another method answers 405 with an
Allow header naming the methods it takes.
The specifics
Log forensics, the MCP endpoint, live telemetry adapters, quotas and every error, in full.
Log forensics: POST /crash
POST /api/v1/crash reads a flight log
after an incident. The body is the log file itself, raw binary, not multipart and not
base64, and the format is detected from the file’s own bytes: ArduPilot dataflash
(.bin), MAVLink telemetry (.tlog), PX4 ULog
(.ulg), DJI flight records (DJIFlightRecord
.txt, including current encrypted ones), Parrot FreeFlight JSON, GUTMA
flight-logging JSON, Litchi / Airdata / DJI GO flight-log CSV, and Betaflight / INAV Blackbox
(.bbl).
Needs the Crash Analyzer add-on on the account (otherwise
402); 64 MB maximum; one metered unit, charged only once the upload is
accepted.
curl -X POST "https://app.rotorlab.app/api/v1/crash?name=flight.bin" -H "Authorization: Bearer rl_your_key" -H "Content-Type: application/octet-stream" --data-binary @flight.bin
# crash.detected · crash.reason · crash.confidence (high | medium | low)
# preconditions.findings[] · timeline[] · meta.vehicle · meta.source_format
# diagnostics.findings[] · diagnostics.battery.internal_resistance_mohm
# diagnostics.ekf.worst_ratio · diagnostics.compass.throttle_field_r
# add &charts=1 for ready-to-embed SVG charts (roughly triples the response)
The log waits in line to be read, like every log
uploaded to RotorLab. By default the call waits for the reading and answers with it. To be
answered at once instead, add &async=1 (or send the header
Prefer: respond-async): the answer is 202
with a token, the log’s place in line and a
status_url. Ask GET /api/v1/crash/{token}
with the same key every few seconds: 202 while it waits, then the
reading in the same shape as above. Asking is not metered, only the member whose key sent the
log can ask, and the reading is kept for 7 days after it is ready. A call whose log is not read
within nine minutes, or that arrives while many calls are already waiting, is answered
503 with a Retry-After header, and its
body carries the same
token and status_url, so the reading is never
lost: asking for it costs nothing more. Five logs from one member may wait at once; a sixth is
answered 429 before it is metered.
curl -X POST "https://app.rotorlab.app/api/v1/crash?name=flight.bin&async=1" -H "Authorization: Bearer rl_your_key" -H "Content-Type: application/octet-stream" --data-binary @flight.bin
# 202 {"queued": true, "token": "...", "status_url": "/api/v1/crash/...", "position": 1, "ahead": 0}
curl "https://app.rotorlab.app/api/v1/crash/TOKEN" -H "Authorization: Bearer rl_your_key"
# 202 while it waits, then 200 with the reading
The diagnostics block covers
pilot input against autopilot action, estimator innovation test ratios (1.0 is the rejection
threshold), pack internal resistance fitted from the flight’s own current draw, compass
interference, GPS quality and accelerometer clipping. Every section carries
available: when it is false it carries a
reason rather than numbers, so a log that never recorded a magnetometer
returns no compass reading instead of a zero. Check available before
reading a section.
MCP: your AI assistant, your records
This instance serves a read-only Model Context Protocol endpoint at
POST https://app.rotorlab.app/mcp. Point any MCP-speaking assistant
(Claude, or your own agent) at it with an API key that includes the
AI assistant (MCP) reads area, sent as
Authorization: Bearer rl_…, and it can ask questions of your
organization’s records: the aircraft register with airworthiness states,
per-aircraft health trends, recent flights, open work and occurrences, battery
packs with their retirement horizons, the readiness meter, and the measured
maintenance experience (repair verification and component survival).
curl -X POST https://app.rotorlab.app/mcp \
-H "Authorization: Bearer rl_your_key" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recent_flights","arguments":{"limit":10}}}'
# result.structuredContent.flights[] · result.structuredContent.next_cursor
The endpoint is read-only by construction: there is no writing tool, the key’s organization is the boundary for every read, and no tool returns member lists, emails, or pilot identities. A key’s holder sees only the tools their role reads. Unscoped keys from before areas existed pass as they do everywhere else.
- Transport. Streamable HTTP with one JSON response per POST; protocol revisions
2025-06-18, 2025-03-26 and 2024-11-05. It opens no event stream and keeps no sessions, so
GETandDELETEanswer405withAllow: POST. A JSON-RPC batch is answered for 2025-03-26 and 2024-11-05 and refused for 2025-06-18, which removed batches. - Tools. Every tool has an input schema describing each argument, an output
schema, and the read-only annotations, so a client can run them without asking. Each
result carries
structuredContentand the same JSON as text. - Times and units. Times are ISO 8601 with your organization’s offset, like
2026-10-03T14:05:00-05:00; a date alone is a calendar day. Units are in the field names or the output schema:hours,duration_min,threshold_mohm. - Paging.
recent_flightsreturns at most 50 flights a page (20 by default); send itsnext_cursorback ascursorfor the next page. - Errors. A missing, misnamed or unreadable argument is JSON-RPC
-32602saying what the tool takes;aircraft_healthlists the aircraft names inerror.data.aircraft. An unknown tool name is-32602too, naming the tools inerror.data.tools. A body that is not JSON is-32700, and one that is not a JSON-RPC request-32600. A bad or missing key is HTTP401, and a key without the MCP area, or a holder still owing two-factor or single sign-on, HTTP403, both with JSON-RPC-32001. - Limits. MCP calls spend no quota or credits. Each key may send 60 requests a
minute and 1,000 an hour, each request in a batch counting once; past that the answer is HTTP
429and JSON-RPC-32029, withRetry-Aftergiving the seconds until the request fits.
Live telemetry adapters (RLT)
The live cockpit natively decodes MAVLink and MSP byte streams that a relay agent sends
through your organization's cloud relay. The relay is view-only and belongs to your
organization: each agent signs in with one of the organization's device keys, minted on the
Live Telemetry page and shown once, only members of that organization can watch it, and
nothing travels back toward the aircraft.
For a source with no byte protocol, a vendor cloud API, a custom tracker, a simulator,
RotorLab telemetry (RLT) is the documented third way in: your adapter dials the
same relay endpoint the standard agent uses, with a device key in the
Authorization header (never in the address), and sends one JSON
object per binary WebSocket frame. The relay never parses telemetry, so an adapter needs
nothing server-side beyond its device key.
wss://<your-server>/relay/agent (Authorization: Bearer rlrd_xxx)
{"rlt": 1, "src": "my-adapter", "t": 1755672000.1,
"lat": 42.5006, "lon": -90.6646, "alt_m": 241.0, "rel_alt_m": 41.0,
"roll": 2.1, "pitch": -4.8, "yaw": 231.0,
"volt": 22.9, "curr": 14.2, "rem_pct": 71, "thr_pct": 48,
"gs_ms": 8.4, "as_ms": 8.9, "vs_ms": -0.3, "sats": 17, "fix": "3D",
"vehicle": "My quad", "serial": "SN123", "mode": "Auto",
"armed": true, "commandable": false}
rlt, src and t
(unix seconds) are required; every other field is optional. Absent means your source does
not report it: the cockpit shows the gap rather than a made-up value, so never send
zeros for readings you don’t have. Units are fixed in the field names (meters, m/s, volts,
amps, degrees, percent). mode and fix are
plain display strings; map your source’s enums yourself. RLT links are
receive-only: declare commandable: false; the relay sends
your adapter no data. Viewers in your organization open the device on
the cockpit and pick Any aircraft (RotorLab telemetry) as the protocol; recording and replay work unchanged. A complete reference
adapter that flies a scripted flight ships in the server tree as
tools/rlt_demo_agent.py.
Quotas, credits and purchasing
A metered call spends one unit; the endpoint table says which are. The
engine, your saved builds and uploading a log for forensics are metered. Everything that
files or reads your organization’s own records is not: flights, checklists,
missions, pilots, aircraft, work orders, components, types, service bulletins, health and
scores are free, so filing a record is never stopped by a quota and a lapsed plan never
blocks a flight record. version and usage are
free too, so a blocked client can still read its own status, and asking after a queued log
reading costs nothing. A refused call and an unknown path spend nothing: a body is checked
whole before anything is metered, and a call refused after that gives its unit back. The one exception is a crash log that is accepted and then cannot be read: it counts as one call.
Your plan sets a daily quota (resets at 00:00 UTC), spent first. Once it’s
gone, calls draw from your organization’s credit balance: credits come included
with some plans, can be granted by an admin, or purchased from
Organization, Billing, API credits when purchasing is enabled. When both are spent the API
answers 429 with Retry-After set to the reset.
A plan that includes no API calls at all answers 402 instead, with
quota: none and no Retry-After: waiting will not
help, credits or a plan that includes calls will.
GET /api/v1/usage at any time; it is free, and its key describes the key that asked: its areas, whether it is read only, and when it ends.Errors
Every error is JSON with an error
sentence that says what to do; the fields beside it let a program branch without reading the sentence.
| Status | Meaning | Branch on |
|---|---|---|
| 400 | A body or value the call cannot read. The error names the field and what it takes: a number that is not one, a whole number out of its range, an unknown airframe type, a field the call does not take (the error lists the ones it does), a missing aircraft. | |
| 401 | Missing or invalid API key: none was sent, or it was revoked, has ended, or its holder’s access has ended. | |
| 402 | Something the account has not bought: the add-on this endpoint needs (the error names it), or a plan that includes no API calls with no credits left. No Retry-After: waiting will not help. | quota: "none" |
| 403 | The key is valid but not allowed this call, and the error says which: the key does not include the area (it is named, with how to get a key that has it); the key is read only and the call would change a record; the key holder’s role does not see or manage that area; the organization requires two-factor authentication or single sign-on that the holder has not completed; the holder is an organization admin who has not set up two-factor authentication within 14 days of becoming one; or the plan has ended. | area, read_only, mfa_setup_required, sso |
| 404 | No such record in your organization, a build that is not yours, or a path that is not an endpoint. Never metered. | |
| 405 | The path exists but does not take this method. The Allow header names the ones it does. | Allow |
| 409 | Not done because of the record’s state, which the error names: a flight on legal hold, a void request already waiting for an organization admin, or a log whose bytes are already stored with another flight. | duplicate_of |
| 410 | A queued log reading that has expired: readings are kept 7 days after they are ready. | |
| 413 | The body is over the endpoint’s limit: 64 MB for a forensics log, 512 MB for a log attached to a flight, 4 MB for a JSON body. | |
| 429 | Wait and send again: today’s quota and the credits are spent (Retry-After is the seconds to the 00:00 UTC reset), or five of your logs are already waiting to be read (Retry-After 30). | quota: "exhausted", waiting |
| 503 | A forensics log not read in time to answer the call. It stays in line; collect its reading from status_url at no further cost. | token, status_url |
Build it into what you already run.
A read-only key reads records and runs the engine; a device credential lets a ground station file flights for the organization.