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/ingest/obs
The body is a JSON array of observations in the frozen obs.v1 shape. Send a batch
per flush, not a request per detection: the pipeline ticks per batch.
curl -X POST "https://<your-backend-host>/api/ingest/obs?source=live" \
-H "Content-Type: application/json" \
-d '[
{
"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
| Field | Required | Type | Meaning |
|---|---|---|---|
schema | yes | string | The frozen wire kind: always obs.v1. |
obs_id | yes | string | Your unique id for this detection. |
t | yes | timestamp | When the sensor saw it (ISO-8601). |
platform | yes | object | The emplacement or vehicle doing the sensing. id is required; geo (lat, lon, alt_m_agl) localises it. |
sensor | yes | object | The sensor itself: id, and modality (radar, rf, eo, ir, acoustic, remote_id, adsb, telemetry, cot, or any other label, preserved verbatim). |
detection | yes | object | The estimate: optional class (frozen strings such as uav_multirotor, uav_fixedwing, bird, helicopter) with class_conf, position as geo or enu, vel_mps, sensor-relative rel (range_m, bearing_deg, elevation_deg, all three), optional cooperative_id, and the modality-specific raw signal. |
provenance | yes | object | Open, adapter-specific metadata. Carried through untouched, never read by domain logic. |
quality | yes | object | Per-detection quality: confidence, geoloc_sigma_m, bearing_sigma_deg, rssi_dbm and friends, all optional. |
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. A presence-only sensor (one
microphone, a CSI link) sends detection.signal.zone_radius_m on its signal and no
geometry at all.
The source gate
| Query param | 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.
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.