XpectraFlow docs
API reference

Datasets

Create and read datasets, and append rows to one over HTTP.

A dataset is one run: a flight, a test firing, a bench session. It belongs to an experiment, it owns a set of channels, and its rows live in a dedicated time-series table named hyper-<datasetId>.

Two things on this page are easy to conflate, so start here:

You wantUse
An empty dataset you will fill laterPOST /api/datasets/create
A dataset ready for live telemetry, with channelsPOST /api/streams/register

create makes a row and nothing else — no storage and no channels. Until one exists, appending to it fails with a 409. register makes the row, the channel snapshot and the storage in one call, which is what you want in almost every case.

There is no delete endpoint. Deleting a dataset destroys its telemetry, and a key pasted into a producer's config should not be able to do that. Delete from the console.

List datasets

GET/api/datasets/listscope datasets:read

Prop

Type

curl -s "https://app.xpectraflow.com/api/datasets/list?experimentId=$EXPERIMENT_ID" \
  -H "x-api-key: $XPECTRA_API_KEY"
[
  {
    "id": "3d81f0a2-55c7-4f9e-9a1b-77c2e0d43a10",
    "name": "sortie-2026-07-31-0914-sysid1",
    "experimentId": "9f2c1b44-7e30-4a11-b8d2-1f4a6c0e8b91",
    "status": "completed",
    "rowCount": 1204998,
    "startedAt": "2026-07-31T09:14:02.310Z",
    "endedAt": "2026-07-31T09:41:55.002Z",
    "timeOffsetMs": 0,
    "codec": "mavlink",
    "storageLocation": "timescale",
    "meta": { "storageShape": "narrow", "dialect": "ardupilotmega" },
    "createdAt": "2026-07-31T09:13:58.771Z"
  }
]

Three fields deserve attention:

  • statusqueuedrunningcompleted or failed. A live stream sits at running until the consumer sees no frames for 30 seconds, then closes it.
  • storageLocationtimescale means queryable now. s3 means the run has been archived to Parquet and needs restoring first. A poller checking "is my data ready" wants this, not status.
  • rowCount — approximate. Both the ingest pipeline and the append route maintain it, and neither takes a lock to do so.

Get one dataset

GET/api/datasets/getscope datasets:read

Prop

Type

Both ids are required. The dataset id alone would be enough to find the row — asking for the experiment as well is what makes this run the same ownership check the console runs, rather than a second implementation of it.

curl -s "https://app.xpectraflow.com/api/datasets/get?experimentId=$EXPERIMENT_ID&datasetId=$DATASET_ID" \
  -H "x-api-key: $XPECTRA_API_KEY"

Create a dataset

POST/api/datasets/createscope datasets:write

Prop

Type

curl -s -X POST https://app.xpectraflow.com/api/datasets/create \
  -H "x-api-key: $XPECTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "experimentId": "'"$EXPERIMENT_ID"'",
    "name": "bench-run-014"
  }'
{
  "id": "3d81f0a2-55c7-4f9e-9a1b-77c2e0d43a10",
  "name": "bench-run-014",
  "experimentId": "9f2c1b44-7e30-4a11-b8d2-1f4a6c0e8b91",
  "status": "queued",
  "rowCount": 0,
  "startedAt": null,
  "endedAt": null,
  "timeOffsetMs": 0,
  "codec": "frame",
  "storageLocation": "timescale",
  "meta": null,
  "createdAt": "2026-07-31T12:00:00.000Z",
  "hypertableName": "hyper-3d81f0a2-55c7-4f9e-9a1b-77c2e0d43a10",
  "ingestToken": "b7f1…9ac2"
}

ingestToken is returned once. Only a bcrypt hash is stored and there is no endpoint that recovers it — the same treatment as an API key, for the same reason. If you lose it, create another dataset.

Update a dataset

POST/api/datasets/updatescope datasets:write

Prop

Type

Like the experiment update, this replaces rather than patches: name and status are required, and startedAt/endedAt are cleared when omitted. timeOffsetMs and daqName are the exceptions and are only touched when you send them.

Append rows

POST/api/datasets/appendscope telemetry:write

The HTTP way to add data to a dataset that already has storage. Good for backfills, slow sensors, and filling in what a producer missed. It is not the high-rate path — a sustained feed belongs on gRPC, where a columnar frame avoids re-sending a timestamp per row.

Note the scope: telemetry:write, not datasets:write. This writes telemetry, not metadata, and it is the same scope gRPC ingest requires — so a producer that streams and backfills needs one credential rather than two.

Prop

Type

curl -s -X POST https://app.xpectraflow.com/api/datasets/append \
  -H "x-api-key: $XPECTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "experimentId": "'"$EXPERIMENT_ID"'",
    "datasetId": "'"$DATASET_ID"'",
    "channels": ["ch_0", "chamber_pressure"],
    "rows": [
      { "time": "2026-07-31T12:00:00.000Z", "values": [1.50, 214.7] },
      { "time": "2026-07-31T12:00:00.010Z", "values": [1.52, 215.1] },
      { "time": "2026-07-31T12:00:00.020Z", "values": [1.49, null] }
    ]
  }'
{ "inserted": 3, "duplicates": 0, "rowCount": 1204998 }

Rules that will bite you

Append errors

StatuscodeWhen
400invalid_requestDuplicate timestamps, unknown channel, values length mismatch, unparseable time
403insufficient_scopeKey lacks telemetry:write
404not_foundNo such dataset, or not yours
409conflictNarrow dataset, or the dataset has no storage yet

Next

On this page