Skip to main content

Data model

Every document the API takes and returns, commented field by field.

ModelSent to or read fromWalkthrough
ScenarioPOST /api/scenariosRunning a simulation
AssetPOST /api/assets, GET /api/assetsWorking with assets
CommandPOST /api/assets/{id}/commandCommanding assets
TaskGET /api/tasksCommanding assets
ObservationPOST /api/signalsWorking with assets

Every answer is wrapped: { "ok": true, "data": … }, or { "ok": false, "error": "…", "code": "…" }. Credentials: Authentication. Every route, with a console that sends it: API explorer.

Scenario​

The spec in POST /api/scenarios, alongside a name.

{
"seed": 20260918, // required. Fixes every random draw: same seed, same fight
"description": "Recon from the north, strike from the east", // optional, one line

"region": { // required. The site
"name": "Harbour",
"center": [1.3644, 103.9915], // [latitude, longitude]. Latitude first
"geofence_radius_m": 1500 // attackers spawn at this radius + 400 m
},

"environment": { "kind": "lite" }, // default. Kinematic, in the server, needs nothing
// { "kind": "gazebo", "environment": "SG_CBD", "vehicles": [ { "label": "BLUE-01", "model_name": "x500_0" } ] }
// { "kind": "air_sim", "environment": "CityLevel", "vehicles": [ { "label": "BLUE-01", "vehicle_name": "Drone1" } ] }
// a rendered world names its compiled level, and binds drone labels to its models

"force": { "kind": "synthetic" }, // default. Our side is invented for the run
// { "kind": "copy_of", "asset_ids": ["ast_…"] } fly copies of registered assets: their
// airframes, payloads and performance

"release": "at_start", // at_start (inbound from t=0) | on_command

"protects": [ // what the run defends. Empty means the site itself
{ "id": "berth-4",
"name": "Berth 4", // shown verbatim
"enu": { "x": 0, "y": 0, "z": 0 }, // metres east, north, up from region.center
"radius_m": 60, // counts as "at the asset" for impact and closest approach
"priority": 2 } // relative worth when several are threatened. 1 by default
],

"win": { // what ends the run, and who won it
"blue_holds": ["berth-4"], // the protected ids blue has to keep
"red_needs": 1 // how many of them red has to reach
},

"engagement": { // what an effect does when it connects, per rung
"terminal": { "connects": 0.9, "effect": "falls" },
"capture": { "connects": 0.8, "effect": "recovered" },
"emit": { "connects": 0.6, "effect": "link_lost",
"defeated_by": ["autonomous"] }, // a target this describes shrugs it off
"deterministic": true // the seed decides, so a run replays
},
// effect is falls | recovered | link_lost; defeated_by is autonomous | ins | gnss | rf_control

"forces": [ // any number. One per side, or one per wave

{
"id": "blue", // required. Every body and sensor is reported under it
"side": "defender", // defender (default) | adversary | neutral
"controller": { "kind": "c2" }, // c2 (default): yours to task through the decision loop
// scenario: the simulator flies it; the console cannot task it

"objective": { "kind": "patrol" }, // optional. What the whole force is trying to do.
// patrol | loiter | intercept (defender)
// kamikaze | payload_drop | recon (adversary)
// { "kind": "intercept", "target": { "kind": "label", "label": "RED-1" } }
// target is { "kind": "label", "label" } | { "kind": "point", "lat", "lon" } | { "kind": "asset", "asset_id" }

"drones": { // the bodies, as a list ...
"kind": "specific",
"drones": [
{
"label": "BLUE-01", // callsign. What an intercept targets, and what the run reports
"drone_type": "quadcopter", // airframe: quadcopter | fpv | fixed_wing | vtol | loitering_munition
"approach_bearing": 0, // degrees: an attacker's inbound axis, 0 = from the north
"formation": "station", // layout hint: line | wedge | column | swarm | loiter | station | single
"intent": { "kind": "loiter" }, // optional. Overrides the force objective for this drone
"behavior": { "strategy": "loiter_guard", "params": {} }, // optional. A named strategy
// from GET /api/strategies; wins over intent
"platform": { "kind": "model", "model": "DJI Mavic 3" }, // optional. Which catalogue airframe it flies
// platform can also be { "kind": "asset", "asset_id": "ast_…" } to fly as a registered asset

"start": { // optional. Where this body begins, and whether it is flying
"at": { "kind": "enu", "x": 300, "y": -200, "z": 0 },
// or { "kind": "polar", "bearing_deg": 180, "range_m": 400, "alt_m": 80 }
"posture": "landed" // landed: on the ground, armed and climbing only when tasked.
}, // airborne is the default, and a landed body ignores the height it names

"performance": { "cruise_mps": 30, "climb_mps": 8 }, // optional. Overrides the airframe's figures
"flight_stack": "kinematic", // px4 (a real autopilot, slower) | kinematic (a written pose)
"effect": "capture" // what it does on arrival: observe | identify | emit | capture | terminal
}
]
},

"assets": [ // sensors and effectors, one list
{
"id": "radar-north",
"roles": ["sensor"], // sensor | effector | platform. A set: an interceptor can be two
"domain": "ground", // aerial (default) | ground | maritime | ew
"profile": "ground-radar-360", // catalogue profile: reach, field of view, accuracy come from here
"sensor": { // present exactly when roles has "sensor"
"id": "radar-north",
"profile": "ground-radar-360",
"placement": { "kind": "fixed", "pos_enu": { "x": 0, "y": 200, "z": 10 } },
// metres east, north, up from region.center
// or mounted: { "kind": "on_asset", "asset_id": "BLUE-01", "mount_offset_enu": { "x": 0, "y": 0, "z": 0 } }
"boresight_deg": 0 // where a directional sensor faces
}
},
{
"id": "battery-1",
"roles": ["effector"],
"domain": "ground",
"effector": { // present exactly when roles has "effector"
"id": "battery-1",
"pk": 0.7, // what it claims per shot. The outcome is read off the picture
"magazine": 4, // rounds. Empty stops it offering
"cooldown_s": 8, // seconds between launches
"range_m": 2000,
"x": -150, "y": 0 // where it stands, metres from region.center
}
}
]
},

{
"id": "red-recon", // wave 1
"side": "adversary",
"controller": { "kind": "scenario" }, // write every adversary force this way
"objective": { "kind": "recon" }, // approach, then orbit at a standoff
"drones": { // ... or as a swarm: N identical drones on one bearing
"kind": "swarm",
"count": 1,
"drone_type": "quadcopter",
"approach_bearing": 0,
"formation": "single"
}
},

{
"id": "red-strike", // wave 2
"side": "adversary",
"controller": { "kind": "scenario" },
"objective": { "kind": "kamikaze" }, // fly into the defended point
"drones": { "kind": "swarm", "count": 3, "drone_type": "fpv",
"approach_bearing": 90, "formation": "line" }
}
],

"time": {
"duration_s": 240, // optional. Simulated seconds; absent runs until stopped
"scale": 1.0 // 2.0 runs twice as fast
},

"video": { "enabled": false, "fps": 10 } // required. The simulated camera feed
}

What happens to it​

Stored in canonical formA document written in the older shorthand, one friendlies fleet and one attack fleet, is converted to forces as it is saved, and the defending force gets the default sensor layout written into it. Read the answer rather than assuming it matches what you sent.
Refused when writtenA spec that does not parse is 400, naming the field.
Swarm labelsA swarm's drones are labelled by the run. Use a specific list for a drone you need to name.
No startA body with no start takes its force's laydown: defenders on a ring about the site, attackers at the geofence radius plus 400 m on their bearing.
Sensors in Dome LiteDome Lite adds its default layout of nine ground sensors to every run. A force's declared sensors run alongside it.

A landed defender is on the ground until something gives it work: it arms, climbs and flies the order, which is what a site defended from its pads actually costs.

Routes: Simulation. Walkthrough: Running a simulation.

Asset​

Registering one​

The body of POST /api/assets. A vehicle:

{
"name": "HAWK-1", // required. The callsign, unique in the workspace
"catalog": "dji/mavic-3-pro", // a type id from GET /api/catalogue/types,
// or a name from GET /api/catalog/builtins ("DJI Mavic 3 Pro")
"kind": "vehicle", // default. Spell it exactly
"spec": { // optional. Laid over the catalogue entry key by key
"remote_id_serial": "1581F5FHD228Q00A1234",
"performance": { "cruise_mps": 12, "max_mps": 21, "endurance_min": 43, "ceiling_m": 6000 }
// a nested object replaces the catalogue's whole
},
"connection": { // optional. Adopt something a link has heard
"integration": "mavlink", // from GET /api/discoveries
"address": "mav-udp-14550-42" // 404 if nobody heard it, 409 if another asset holds it
}
}

A fixed sensor:

{
"name": "MAST-1", // also the sensor.id its detections carry
"kind": "sensor", // required for a sensor
"catalog": "ground-radar-360", // required. A profile id from GET /api/catalog/sensor-profiles.
// Reach, field of view and accuracy come from it
"placement": { // required for a sensor
"lat": 1.3644, // latitude first; a swapped pair is refused
"lon": 103.9915,
"height_m": 12, // optional, above ground
"boresight_deg": 90 // optional, the direction it faces
}
}

POST /api/assets/{id} edits: send only name, model, performance, placement or switches. DELETE /api/assets/{id} removes.

Reading one​

What GET /api/assets and GET /api/assets/{id} return, for vehicles and sensors alike:

{
"id": "ast_339a78dc2b9146aa8736bf2c05a4ccbb", // every call names it by this. A scenario's drone is
// sim/<run>/BLUE-01: URL-encode it in a path
"name": "HAWK-1",
"kind": "vehicle", // vehicle | sensor
"domain": "aerial", // aerial | ground | maritime | ew
"affiliation": "unknown",
"performance": { "cruise_mps": 12.0, "max_mps": 21.0, "endurance_min": 43.0, "ceiling_m": 6000.0 },
"payloads": [ { "profile": "eo-turret", "label": "Hasselblad 4/3 main", "source": "catalog" } ],
"link": { // what is carrying it
"state": "connected", // unlinked | connected | stale
"protocol": "mavlink",
"link_id": "GCS"
},
"health": { // from its own telemetry
"armed": true, "in_air": true, "mode": "loiter",
"energy": {}, "nav": { "fix": "no_fix", "satellites": 0 }
},
"switches": { // what an operator has turned on or off
"receiving": true, // its reports enter the picture
"commanding": false, // it may be commanded
"video": false,
"automation": "site"
},
"capabilities": [ // absent until a link carries it. See Command
{ "verb": "move_to", "params": "point", "class": "navigation", "available": true },
{ "verb": "takeoff", "params": "altitude", "class": "lifecycle",
"available": false, "reason": "already airborne" }
],
"track_id": "T-09001", // its own track on the picture, once heard
"saved": true, // in the registry, as opposed to heard only

// sensors only
"profile": "ground-radar-360",
"sensor_id": "MAST-1",
"placement": { "lat": 1.3644, "lon": 103.9915, "height_m": 12.0, "boresight_deg": 90.0 }
}

Filters on GET /api/assets: kind, free=true (not committed to a plan), nearest=lat,lon (sorted by distance). Routes and refusals: Assets & commands.

Command​

The body of POST /api/assets/{id}/command: a verb, and parameters tagged by kind.

{
"verb": "move_to", // one the asset advertises in capabilities, and available now
"params": { "kind": "point", "lat": 1.3700, "lon": 103.9950, "alt_m": 100 }
}

// params, by kind
{ "kind": "none" } // arm, disarm, land, hold, stop, return_to_base, cue_release
{ "kind": "altitude", "alt_m": 50 } // takeoff
{ "kind": "point", "lat": 1.37, "lon": 103.995, "alt_m": 100 } // move_to. alt_m optional
{ "kind": "area", "lat": 1.3644, "lon": 103.9915, "radius_m": 300, "alt_m": 90 }
// orbit, surveil. alt_m optional
{ "kind": "target", "track_id": "T-00481" } // follow, intercept, cue_sensor, jam, spoof, designate
{ "kind": "route", // follow_route
"points": [
{ "lat": 1.3660, "lon": 103.9900, "alt_m": 80 },
{ "lat": 1.3680, "lon": 103.9950, "alt_m": 80,
"speed_mps": 10, // optional
"hold_s": 30, // optional, wait here
"action": "loiter", // optional: pass | loiter | land
"params": {} } // optional, namespaced vendor parameters
],
"on_complete": "hold" } // hold | return_to_base | land | loop
ClassVerbsActs on
lifecyclearm disarm takeoff land return_to_base hold stopthe asset
navigationmove_to follow_route orbitthe asset
effectsurveil follow intercept cue_sensor cue_release jam spoof designatea track or a place, and passes the rules of engagement

The answer is a Task, 201 with Location: /api/tasks/tsk_…. A verb not advertised, or not available now, is 409, and the refusal is kept as a task too.

The ladder​

Every verb sits on one rung. The autonomy line answers each task: runs at or below it, asks above it, not_permitted above the deployment's ceiling.

// observe < manoeuvre < watch < shadow < deny < destroy
// seen on a simulated drone:
// move_to, orbit, hold manoeuvre
// surveil watch
// follow shadow
// intercept destroy

// GET /api/decision-config → .data.autonomy, and PUT /api/decision-config/autonomy with:
{ "kind": "linear", "line": "watch" } // takes effect on the next tick

Task​

Every command writes one, accepted or refused. GET /api/tasks, GET /api/tasks/{id}:

{
"id": "tsk_d2082c75efe246b7881b5213b0e8c9ba",
"actor": "sim/96cfff7e-…/BLUE-01", // the asset told to act
"verb": "follow",
"params": { "kind": "target", "track_id": "T-00481" }, // as sent
"object": { "kind": "track", "track_id": "T-00481" }, // what it is aimed at: a track or a place
"origin": { "source": "operator" }, // operator | plan (with plan_id) | autonomy (with the line)
"level": "shadow", // the rung, stamped when written
"gate": { // the line's answer, stamped when written
"gate": "runs", // runs | asks | not_permitted
"stop_within": 0 // seconds left to take it back, when it runs
},
"status": "issued", // proposed → issued → executing → complete,
// or refused | superseded | reverted
"reason": null, // why it was refused, superseded or reverted, verbatim
"issued_at": "2026-09-18T04:54:43.115264Z"
}

Filters: actor, status, origin, plan, object, since, limit. A release with a stop window is taken back at POST /api/tasks/{id}/stop. See Plans & tasks.

Observation​

One detection from one sensor. A batch of them is the body of POST /api/signals:

{
"source": "live", // live | simulated. Simulated stays out of the live picture
"observations": [ // one tick. 1 to 5000
{
"schema": "obs.v1", // required, always this
"obs_id": "mast1-000148", // required, yours, unique. Returned as track evidence
"t": "2026-09-18T08:14:22.000Z", // required. When the sensor saw it
"platform": { // required. What is doing the sensing
"id": "MAST-1",
"geo": { "lat": 1.3644, "lon": 103.9915, "alt_m_agl": 12 } // height above ground
},
"sensor": { // required
"id": "MAST-1", // a registered sensor's name counts the return against it
"modality": "radar" // radar | rf | eo | ir | acoustic | remote_id | adsb | …
},
"detection": { // required
"class": "uav_multirotor", // optional: uav_multirotor | uav_fixedwing | bird | helicopter | …
"class_conf": 0.9,
"geo": { "lat": 1.3712, "lon": 103.9950, "alt_m": 110 }, // alt_m here, not alt_m_agl
// or "enu": { "x", "y", "z" }, or sensor-relative "rel": { "range_m", "bearing_deg", "elevation_deg" }
"vel_mps": { "vx": 0, "vy": -8, "vz": 0 }, // optional
"signal": { "modality": "radar", "rcs_m2": 0.05 } // required. The raw measurement
// rf: { "modality": "rf", "freq_mhz": 5806, "bearing_deg": 206 } bearing only: no geo
// acoustic: { "modality": "acoustic", "bearing_deg": 120 }
// signal.modality is one of radar | rf | eo | ir | acoustic | remote_id | adsb | telemetry | cot | other
},
"provenance": { "decoder": "my-bridge/1.2" }, // required, free-form, carried through untouched
"quality": { // required, every field optional
"confidence": 0.9,
"geoloc_sigma_m": 15, // position error, metres
"bearing_sigma_deg": 4 // bearing error, degrees
}
}
]
}

→ { "ok": true, "data": { "accepted": 1 } }. A missing field is 400 with a sentence that names it. What comes out is a track; its evidence lists the obs_ids behind it. See Observations & events.

Also worth knowing​

Picture & streamHydrate once from GET /api/picture, then hold the stream. The events and their payloads.
Plans & tasksCandidate plans, approving part of one, the veto window.
ConfigurationRules, thresholds, the autonomy line, and doctrine as one YAML document.
SimulationScenario, run lifecycle and recorded-run routes.
API explorerEvery operation, with a console that sends the request.