Skip to main content
RotorLab API

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.

The RotorLab API: an analysis request and its JSON answer beside the OpenAPI reference

Quickstart

A key, a bearer header and one request. The same endpoint analyzes a multirotor, a wing or a VTOL.

Step 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. It is shown once.

Step 2

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.

Step 3

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.

43 endpoints in 11 areas. Open an area, or expand them all.
MethodPathDescriptionKey areaMetered
GET/versionAPI name, version, authorstatusno
GET/usageYour quota, credits, and the key making this callstatusno
GET/airframesSupported airframe types and their defaultsengineyes
GET/catalogPayload parts catalog (controllers, computers, sensors, radios)engineyes
POST/analyzeAnalyze a build and return full performance resultsengineyes
POST/rfRadio link budget and rangeengineyes
POST/cgCenter of gravity and balance for a buildengineyes
GET/buildsList your saved buildsbuildsyes
POST/buildsCreate or overwrite a build by namebuildsyes
GET/builds/{id}Fetch one of your buildsbuildsyes
DELETE/builds/{id}Delete one of your buildsbuildsyes
POST/crashAnalyze a flight log after an incidentengineyes
GET/crash/{token}A queued log readingengineno
GET/flightsList flights on the organization’s recordflightsno
POST/flightsRecord a flight (create, or correct one already sent)flightsno
GET/flights/{id}One flightflightsno
DELETE/flights/{id}Remove a flight from the recordflightsno
POST/flights/{id}/logAttach the log file to a flight already on the recordflightsno
GET/checklistsChecklist templates, and completed runschecklistsno
POST/checklistsFile a completed checklistchecklistsno
GET/missionsList the organization’s mission recordsflightsno
POST/missionsSave or update a mission recordmissionsno
GET/missions/{id}One mission record, payload includedflightsno
DELETE/missions/{id}Delete a mission recordmissionsno
GET/missions/{id}/planThe mission as a QGroundControl .planflightsno
POST/missions/importConvert a .plan into a mission payloadmissionsno
GET/workordersList work ordersflightsno
GET/workorders/{id}One work orderflightsno
GET/componentsList componentsflightsno
GET/typesList aircraft typesflightsno
GET/types/{id}One typeflightsno
GET/bulletinsList service bulletinsflightsno
GET/bulletins/{id}Bulletin complianceflightsno
GET/healthFleet airworthiness mapflightsno
GET/health/{id}One aircraft’s healthflightsno
GET/pilotsWho a flight may be filed for, with where each pilot standsflightsno
GET/aircraftThe aircraft registerflightsno
GET/jobsJobs 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 itjobsno
GET/jobs/{id}One jobjobsno
GET/clientsClients by name and handle, with no contact detailsjobsno
GET/clients/{id}One clientjobsno
GET/ml/scoresAdvisory model scores on the organization’s recordsflightsno
POST/ml/scoresWrite an advisory scoreflightsno

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 GET and DELETE answer 405 with Allow: 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 structuredContent and 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_flights returns at most 50 flights a page (20 by default); send its next_cursor back as cursor for the next page.
  • Errors. A missing, misnamed or unreadable argument is JSON-RPC -32602 saying what the tool takes; aircraft_health lists the aircraft names in error.data.aircraft. An unknown tool name is -32602 too, naming the tools in error.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 HTTP 401, and a key without the MCP area, or a holder still owing two-factor or single sign-on, HTTP 403, 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 429 and JSON-RPC -32029, with Retry-After giving 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.

Every metered response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Credits. Check 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.

StatusMeaningBranch on
400A 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.
401Missing or invalid API key: none was sent, or it was revoked, has ended, or its holder’s access has ended.
402Something 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"
403The 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
404No such record in your organization, a build that is not yours, or a path that is not an endpoint. Never metered.
405The path exists but does not take this method. The Allow header names the ones it does.Allow
409Not 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
410A queued log reading that has expired: readings are kept 7 days after they are ready.
413The 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.
429Wait 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
503A 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
RotorLab API · RotorLab.app · the API is available when the server runs in multi-user mode (--web --auth).

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.