XpectraFlow docs
API reference

Events

Annotate a run — including live, mid-stream, with absolute timestamps.

An event marks a span on a dataset's timeline: a link loss, a mode change, an anomaly somebody wants to find again. Events are what turn a wall of samples into something a person can navigate.

This is a write-in API. You post events to us; we do not post events to you. Nothing here delivers anything outbound — the only thing XpectraFlow sends to your systems is a command, and that is a separate mechanism.

Append events

POST/api/datasets/eventsscope events:write

Append-only. This never replaces a dataset's event list, so a producer reporting one link loss cannot erase what was already recorded for the flight.

Prop

Type

Each Event:

Prop

Type

Two ways to say when

Send either startTime/endTime or startAt/endAt — one pair per event, never both, never half of one. Sending both, or only one of a pair, is a 400.

Use this while a stream is running. Your producer knows what time it is; it does not know the dataset's T=0, because that is a property of the first sample it may never have observed.

curl -s -X POST https://app.xpectraflow.com/api/datasets/events \
  -H "x-api-key: $XPECTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "experimentId": "'"$EXPERIMENT_ID"'",
    "datasetId": "'"$DATASET_ID"'",
    "events": [{
      "label": "LINK_LOSS",
      "startAt": "2026-07-31T09:20:54.500Z",
      "endAt": "2026-07-31T09:21:00.900Z",
      "relParam": "mavlink.HEARTBEAT",
      "description": "No heartbeat for 6.4s"
    }]
  }'

Mixing the two forms inside one batch is allowed — a batch may be assembled from two producers.

Response

{
  "accepted": 1,
  "anchor": { "source": "hypertable", "at": "2026-07-31T09:14:02.310Z" },
  "events": [
    { "id": "b31d…", "startTime": 412.19, "endTime": 418.59 }
  ]
}
  • accepted is the number of rows that actually landed, not the number you sent. A retry that collapses onto rows already written reports 0, which is the honest answer.
  • anchor is the T=0 your absolute timestamps were measured against, and null when you sent relative seconds. source is hypertable when it came from the data and startedAt when the dataset has no rows yet.
  • events echoes the resolved relative seconds. Check them once when you build the integration — it is much cheaper than discovering a conversion was wrong from a chart a week later.

Read events back

GET/api/datasets/eventsscope events:read

Prop

Type

curl -s "https://app.xpectraflow.com/api/datasets/events?experimentId=$EXPERIMENT_ID&datasetId=$DATASET_ID" \
  -H "x-api-key: $XPECTRA_API_KEY"
[
  {
    "id": "b31d…",
    "label": "IGNITION",
    "startTime": 0.0,
    "endTime": 3.0,
    "duration": 3.0,
    "relParam": "",
    "description": "Valve open, thrust ramping",
    "color": null,
    "createdAt": "2026-07-31T12:00:04.180Z"
  }
]

In timeline order. startTime and endTime are always seconds from T=0, whichever form they were written in — so a client that posted startAt reads back the resolved relative value, which is also what the charts draw.

How the conversion works

For an absolute instant, the stored value is:

relSeconds = (yourInstant − anchor) / 1000

where anchor is the earliest stored timestamp for the dataset — the same value the charts use to place telemetry.

timeOffsetMs deliberately plays no part in this. It is a render-time alignment, applied equally to events and to telemetry when they are drawn, so it cancels out of the conversion. Subtracting it here would shift every event by the offset — an error invisible on the majority of datasets, whose offset is zero, and silently wrong on every aligned one.

Instants before T=0 are fine and produce negative seconds. A pre-trigger annotation on the arming sequence is a legitimate thing to record.

Idempotency

Supply your own id and retrying is free — a redelivered append collapses onto the row it already wrote instead of double-marking the timeline. Omit it and every call creates new rows.

{ "id": "0d6a5b1c-2e94-4a7f-9c31-8b02d5e6f7a1", "label": "LINK_LOSS", "startAt": "…", "endAt": "…" }

This is how the MAVLink agent reports link loss without duplicating markers across a reconnect.

Errors

StatuscodeWhen
400invalid_requestBoth time forms, one half of a pair, empty label, more than 500 events
403insufficient_scopeKey lacks events:write (or events:read to list)
404not_foundNo such dataset in this organization, or it is not under that experiment
409conflictAbsolute timestamps on a dataset with no timeline yet

The 409 is worth planning for. A dataset that has registered but received no telemetry has no T=0 to measure against, and guessing one would put events somewhere plausible and wrong. Send relative seconds, or post the events once data has started arriving.

What to record

On this page