RotorLab logoRotorLabDeveloper API
RotorLab API

Build with the physics engine

A small, key-authenticated HTTP API over the same engine the app uses. Analyze any build — multirotor, fixed-wing, or VTOL — work out radio link budgets and center-of-gravity balance, read the parts catalog and airframe types, and manage your own saved builds, from your scripts, notebooks, or apps.

Download OpenAPI spec Download Postman collection Get an API key

Import the OpenAPI file into editor.swagger.io or any client to get a full interactive reference, or load the Postman collection and set base_url + api_key.

Quickstart

1 · Create your key

Sign in, open My account → API access, 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. 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 flight and checklist endpoints.

2 · Authenticate

Send your key as a bearer token (the header X-API-Key is also accepted). Base URL for this instance is /api/v1.

curl -H "Authorization: Bearer rl_your_key" \
  http://127.0.0.1:8765/api/v1/version

3 · Analyze a build

curl -X POST http://127.0.0.1:8765/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.

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 http://127.0.0.1:8765/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 http://127.0.0.1:8765/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 http://127.0.0.1:8765/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

Log forensics

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 "http://127.0.0.1:8765/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 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.

Python

import requests
BASE = "http://127.0.0.1:8765/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 — 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 = "http://127.0.0.1:8765/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

All paths are under /api/v1 and scoped to the calling user. Builds you don’t own return 404.

MethodPathDescriptionMetered
GET/versionAPI name, version, authorno
GET/usageYour quota, remaining, credit balanceno
GET/pilotsYour organization’s pilots and where each one stands on currency. No email addresses. Currency is reported so a crew knows — it must never be used to prevent a flight.no
GET/aircraftYour aircraft registry: name, registration, serial, model, status, hours flownno
GET/airframesSupported airframe typesyes
GET/catalogPayload parts catalogyes
POST/analyzeAnalyze a build (any airframe) → full resultsyes
POST/rfRadio link budget and range (one link or several)yes
POST/cgCenter of gravity and balance for a buildyes
GET/buildsList your saved buildsyes
GET/builds/{id}Fetch one of your buildsyes
POST/buildsCreate or overwrite a build by nameyes
DELETE/builds/{id}Delete one of your buildsyes

MCP: your AI assistant, your records

RotorLab serves a read-only Model Context Protocol endpoint at POST https://rotorlab.app/mcp. Point any MCP-speaking assistant (Claude, or your own agent) at it with an API key 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).

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. Keys carrying scopes need the mcp scope; unscoped keys pass as they do everywhere else. Supported protocol revisions: 2025-06-18, 2025-03-26 and 2024-11-05, single JSON responses.

Live telemetry adapters (RLT)

The live cockpit natively decodes MAVLink and MSP byte streams through the cloud relay. 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 authenticated relay endpoint the standard agent uses and sends one JSON object per binary WebSocket frame. The relay never parses telemetry, so an adapter needs nothing server-side beyond your API key.

wss://<your-server>/relay/agent?channel=<label>      (Authorization: Bearer rl_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 and ignore anything sent to your adapter. Viewers pick Any aircraft (RotorLab telemetry) as the protocol on the cockpit; 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 & purchasing

Most requests spend one unit. version and usage are free so a blocked client can still read its own status, and flights and checklists are also free — filing your own records is never metered, and a lapsed plan never blocks a flight record. Your plan sets a daily quota (resets at 00:00 UTC), spent first. Once it’s gone, requests draw from your credit balance — credits come included with some plans, can be granted by an admin, or purchased from My account when monetization is enabled. When both are exhausted the API returns 429.

Every metered response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Credits. A 429 also includes Retry-After (seconds until the daily reset). Check GET /api/v1/usage at any time; it is free.

Errors

StatusMeaning
401Missing or invalid API key
403Key valid but the plan doesn’t allow it (e.g. an ended trial or lapsed billing period)
404Unknown endpoint, or a build you don’t own
400Malformed request body
429Daily quota and credits exhausted
RotorLab API · RotorLab.app · the API is available when the server runs in multi-user mode (--web --auth).