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
| Field | Values | Meaning |
|---|---|---|
source | live (default), simulated | Which 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 param | Default | Notes |
|---|---|---|
kind | all | Comma-separated: obs, track, alert, coa, engagement, bda, swarm_command, tasking, tasking_status, threat, plan, influence. |
since | Only events after this ISO-8601 timestamp (exclusive). | |
until | Only events at or before this timestamp. | |
track_id | Only events about this track. | |
run_id | Only events from one simulation run. | |
fold | latest keeps only the newest event per object. | |
limit | 500 | Maximum 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.