Picture & stream
One GET returns everything current. One held connection then delivers every change. A client built this way is never stale and never polls.
Hydrate once, GET /api/picture
The picture is the whole current state in one response: tracks, threats, the active plan and its candidates, sensors, the fleet, protected assets, the region, the autonomy line, fusion health, and what every integration is doing.
curl https://<your-backend-host>/api/picture
{
"ok": true,
"data": {
"generation": 4182,
"t": 341.5,
"tracks": [ { "track_id": "T-00492", "classification": { "…": "…" }, "…": "…" } ],
"threats": [],
"plan": null,
"candidate_plans": [],
"sensors": [ "…" ],
"assets": [ "…" ],
"engagement_mode": "watch",
"fusion_health": { "…": "…" }
}
}
It is the heaviest response the server builds, and it exists for one moment: the
moment a client connects. Fetch it once, open the stream, and never fetch it again
unless the stream tells you to (see resync below). Reading one field of the picture
by re-fetching all of it is the pattern the stream exists to replace.
For a client that is not hydrating a console, GET /api/tracks returns the live
track set on its own. That is the read for a service that wants tracks and nothing
else.
generation is a counter that increments whenever the picture changes. Compare it
against the generation in stream events to know your copy is current.
Then hold the stream, GET /api/stream
The stream does not take your key or token. It takes a one-time ticket, good for a minute, minted by an authenticated call:
# Mint a ticket with your credential, then open the stream with it
TICKET=$(curl -s -X POST https://<your-backend-host>/api/auth/stream-ticket \
-H "X-Api-Key: $KEY" -H "X-Workspace-Id: $WS" | jq -r '.data.ticket')
curl -N "https://<your-backend-host>/api/stream?ticket=$TICKET"
const stream = new EventSource(`https://<your-backend-host>/api/stream?ticket=${ticket}`);
stream.addEventListener("track_update", (e) => {
const track = JSON.parse(e.data);
map.upsert(track.track_id, track);
});
stream.addEventListener("resync", async () => {
// The server dropped events this client never saw. Refetch the picture.
const { data } = await (await fetch("https://<your-backend-host>/api/picture", { headers })).json();
map.replaceAll(data.tracks);
});
The stream is standard text/event-stream with a keep-alive comment every 15
seconds. Events arrive named, so an EventSource listener per event type is the
whole client.
Events every client receives
These carry the connection's own lifecycle and the console's live slices. They are always sent, whatever filter you set:
| Event | Payload | When |
|---|---|---|
snapshot | { tracks, generation }, up to 5000 tracks folded to the latest per id | once, on connect |
resync | { generation, dropped } | the server dropped events for this client; re-fetch the picture |
live_state | { engagement_mode, stream_gen } or { fusion_health, stream_gen } | either changes. engagement_mode carries the autonomy line: observe, manoeuvre, watch, shadow, deny, destroy |
engagement_slice | { threats, plan, candidate_plans, sim, stream_gen } | the engagement state changes |
fleet_slice | { assets, stream_gen } | any asset's live state changes |
sensor_slice | { sensors, stream_gen } | any sensor's state or pointing changes |
integration_slice | { integrations, candidates, stream_gen } | a protocol's state or the discovery list changes |
Domain events
One event per pipeline artefact, named by its kind:
| Event | Carries |
|---|---|
track_update | a full fused track: id, classification, kinematics, uncertainty, custody, threat score, identification |
track_removed | the id of a track that left the picture |
obs | one raw observation. High rate, excluded by default; opt in with ?kinds=obs |
alert | a reflexive alert with its lifecycle state |
threat | a threat assessment |
plan | a plan (carries its own generation) |
posture (the frozen name; carries the autonomy line now), influence | decision-state changes |
coa, engagement, bda, swarm_command, tasking, tasking_status | the decision-chain artefacts, as their frozen wire shapes |
A bda is an assessment, not an outcome. verdict: "kill" is what an effector claims,
and the picture may refute it: a contact assessed killed that keeps being tracked is still
a contact. What became of a threat is on the threat, in resolution, and it is inferred
from the end of the target's track rather than reported by whatever shot at it.
Filtering
?kinds= takes a comma-separated list of domain event names and limits the stream to
them (the lifecycle and slice events above are always included):
curl -N "https://<your-backend-host>/api/stream?ticket=$TICKET&kinds=track_update,alert"
Omitting kinds sends every domain event except obs, which is per-detection and
high-rate. Ask for it only when you are building something that consumes raw
detections.
The contract, in short
GET /api/pictureonce, remembergeneration.- Open
/api/stream, apply events as they arrive. - On
resync, go back to 1. Nothing else requires a second fetch.