Skip to main content

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:

EventPayloadWhen
snapshot{ tracks, generation }, up to 5000 tracks folded to the latest per idonce, 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:

EventCarries
track_updatea full fused track: id, classification, kinematics, uncertainty, custody, threat score, identification
track_removedthe id of a track that left the picture
obsone raw observation. High rate, excluded by default; opt in with ?kinds=obs
alerta reflexive alert with its lifecycle state
threata threat assessment
plana plan (carries its own generation)
posture (the frozen name; carries the autonomy line now), influencedecision-state changes
coa, engagement, bda, swarm_command, tasking, tasking_statusthe 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​

  1. GET /api/picture once, remember generation.
  2. Open /api/stream, apply events as they arrive.
  3. On resync, go back to 1. Nothing else requires a second fetch.