Assets & commands
An asset advertises what it can be told. The client reads that list and never guesses, so the same command call works for a quadcopter, a turret and a simulated twin.
Two views of the same thing
The registry (/api/assets) is what you have declared: rows with a stored
specification. The fleet (/api/fleet) is what those declarations look like
right now: link state, kinematics, health, mission progress, and the capability list
that drives commanding. Write to the registry; read the fleet.
Register an asset
The catalog is the normal door. You name the product; the specification comes from the catalog entry, so nobody types an endurance figure by hand:
curl -X POST https://<your-backend-host>/api/assets/from-catalog \
-H "Content-Type: application/json" \
-d '{
"name": "BLUE-04",
"catalog": "DJI Mavic 3",
"remote_id_serial": "1581F5FHD228Q00A1234"
}'
{ "ok": true, "data": { "id": "6f0c1a3e-…", "workspace_id": "…", "kind": "vehicle",
"name": "BLUE-04", "spec": { "…": "…" }, "created_at": "…", "updated_at": "…" } }
| Field | Required | Type | Meaning |
|---|---|---|---|
name | yes | string | The callsign. What operators call it. |
catalog | yes | string | A catalog entry's name. Case-tolerant in, canonical out. |
domain | no | string | The domain you were adding under (aerial, ground, maritime, ew). A catalog entry that disagrees is refused, not re-filed. |
remote_id_serial | no | string | The platform's Remote-ID serial. This is the identity fusion correlates on, and what marks its detections friendly. |
simulated | no | bool | Real (default), or provided by a simulator. |
POST /api/assets with a raw spec object exists for a shape the catalog does not
hold. POST /api/assets/{id} updates name, performance overrides or model;
POST /api/assets/{id}/payloads replaces what a vehicle carries; DELETE /api/assets/{id} removes the row.
Adopt what a link hears
A vehicle that announces itself on an open link appears under
GET /api/links/discovered with a deterministic key. Adopting it registers it
without typing anything the vehicle already said:
curl https://<your-backend-host>/api/links/discovered
curl -X POST https://<your-backend-host>/api/links/discovered/adopt \
-H "Content-Type: application/json" \
-d '{ "key": "mav-udp0-3" }'
The key is a protocol address, stable across restarts, so re-adopting after a reboot
re-binds the same asset rather than creating a twin. Links themselves are managed at
GET/POST /api/links and DELETE /api/links/{id}; a malformed or already-bound
endpoint is refused with 409.
Read the fleet
curl https://<your-backend-host>/api/fleet # every asset's live view
curl https://<your-backend-host>/api/fleet/6f0c1a3e-… # one asset, by its id
An asset's live view carries id, name, domain, kind, affiliation, link
state, kinematics (the same shape fusion produces, absent when nothing is heard,
never zero-filled), health, payloads, mission progress, and capabilities,
the list that matters next. For a running client, prefer the fleet_slice event on
the stream over re-fetching these routes.
Command an asset
Every command is the same call: a verb, its parameters, and who ordered it.
curl -X POST https://<your-backend-host>/api/assets/6f0c1a3e-…/command \
-H "Content-Type: application/json" \
-d '{
"verb": "move_to",
"params": { "kind": "point", "lat": 1.3521, "lon": 103.8198, "alt_m": 60 }
}'
await fetch(`https://<your-backend-host>/api/assets/${assetId}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
verb: "follow_route",
params: {
kind: "route",
points: [
{ lat: 1.3521, lon: 103.8198, alt_m: 60 },
{ lat: 1.3540, lon: 103.8210, alt_m: 60 },
],
loop_route: false,
},
}),
});
The verbs
Three classes, governed differently. Lifecycle and navigation execute when the asset accepts them; tasking verbs additionally pass rules-of-engagement gating.
| Class | Verbs | Params kind |
|---|---|---|
| Lifecycle | arm, disarm, takeoff, land, return_to_base, hold, stop | none (takeoff: altitude) |
| Navigation | move_to, follow_route, orbit | point, route, area |
| Tasking (ROE-gated) | surveil, follow, intercept, cue_sensor, jam, spoof, designate | area, target, none |
params is tagged by kind:
kind | Fields |
|---|---|
none | |
altitude | alt_m |
point | lat, lon, optional alt_m |
route | points (each lat, lon, optional alt_m), loop_route |
area | lat, lon, radius_m, optional alt_m |
target | track_id |
The capability contract
An asset's capabilities list names each verb it accepts, its parameter kind, its
class, whether it is available right now, and a reason when it is not. A client
renders and sends only what is advertised. A verb outside the list, or advertised
but currently unavailable, is refused with 409 and an error naming the verb and
the reason:
{ "ok": false, "error": "intercept is not advertised by this asset" }
The path takes the asset's id (the value the registry returned, or the fleet
list shows), never its callsign.
An asset with an empty capability list is monitor-only. That is a state, not an error: a radar has nothing to be told.
Addressing is always by asset id. No protocol identifier appears in any path; which adapter carries the command is the server's business.