Skip to main content

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": "…" } }
FieldRequiredTypeMeaning
nameyesstringThe callsign. What operators call it.
catalogyesstringA catalog entry's name. Case-tolerant in, canonical out.
domainnostringThe domain you were adding under (aerial, ground, maritime, ew). A catalog entry that disagrees is refused, not re-filed.
remote_id_serialnostringThe platform's Remote-ID serial. This is the identity fusion correlates on, and what marks its detections friendly.
simulatednoboolReal (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.

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.

ClassVerbsParams kind
Lifecyclearm, disarm, takeoff, land, return_to_base, hold, stopnone (takeoff: altitude)
Navigationmove_to, follow_route, orbitpoint, route, area
Tasking (ROE-gated)surveil, follow, intercept, cue_sensor, jam, spoof, designatearea, target, none

params is tagged by kind:

kindFields
none
altitudealt_m
pointlat, lon, optional alt_m
routepoints (each lat, lon, optional alt_m), loop_route
arealat, lon, radius_m, optional alt_m
targettrack_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.