Skip to main content

Observations & events

An observation is one detection from one sensor. POST a batch, fusion turns the stream into tracks, and everything the pipeline produces lands in a queryable event log.

Push observations, POST /api/signals​

The body is a batch of observations in the frozen obs.v1 shape, and a source. Send a batch per flush, not a request per detection: every batch is one pipeline tick, so a request per detection is a tick per detection.

source is live (something out there saw something) or simulated, which is how a generator that knows it is inventing feeds a run without its output being mistaken for a real detection. Your credential names the workspace; the body never does.

curl -X POST "https://<your-backend-host>/api/signals" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $DOME_KEY" -H "X-Workspace-Id: $WORKSPACE" \
-d '{ "source": "live", "observations": [
{
"schema": "obs.v1",
"obs_id": "rf-north-000481",
"t": "2026-08-28T08:14:22Z",
"platform": { "id": "rf-north" },
"sensor": { "id": "rf-north-df", "modality": "rf" },
"detection": {
"class": "uav_multirotor",
"class_conf": 0.74,
"signal": { "modality": "rf", "freq_mhz": 5806.0, "bearing_deg": 206.0 }
},
"provenance": { "decoder": "my-bridge/1.2" },
"quality": { "confidence": 0.74, "bearing_sigma_deg": 4.0 }
}
] }'
{ "ok": true, "data": { "accepted": 1 } }

The observation shape​

Every field, commented, with the signal forms for each modality: Observation model.

A bearing-only sensor sends its bearing on the signal (bearing_deg on an rf or acoustic signal) and no position; fusion performs a bearing-space update rather than inventing a range.

The source gate​

FieldValuesMeaning
sourcelive (default), simulatedWhich door the batch enters through. Simulated observations produce simulated tracks, kept apart from the real picture.

A batch from a real sensor never says simulated, and a simulation replay never says live. The picture keeps the two apart end to end.

Limits​

A batch holds at most 5000 observations, because a batch is one tick. An empty batch and an oversized one are both 400, and the error says which. An observation missing a required field is 400 too, with a plain-text sentence naming the field (missing field `signal` ). A read-only workspace role gets 403.

Read a track back, GET /api/tracks​

Observations go in; tracks come out. GET /api/tracks is the live track set, for anyone who is not hydrating a console:

curl https://<your-backend-host>/api/tracks

GET /api/picture is the heaviest thing this server serves and it exists for one moment, hydration. Reading the whole picture to get at part of it is what this route is here to stop.

Read the log, GET /api/events​

Everything the pipeline persists (observations, tracks, alerts, plans, taskings, engagements, battle-damage assessments) is queryable back out of the event store:

curl "https://<your-backend-host>/api/events?kind=track,alert&since=2026-08-28T08:00:00Z&limit=500"
Query paramDefaultNotes
kindallComma-separated: obs, track, alert, coa, engagement, bda, swarm_command, tasking, tasking_status, threat, plan, influence.
sinceOnly events after this ISO-8601 timestamp (exclusive).
untilOnly events at or before this timestamp.
track_idOnly events about this track.
run_idOnly events from one simulation run.
foldlatest keeps only the newest event per object.
limit500Maximum rows.

Each element is the event's frozen wire shape (track.v1, alert.v1, …), exactly as it was persisted. This is the after-action surface: a run can be replayed, a track's history reconstructed, an alert's lifecycle audited, from this one route.