Quickstart
Key, experiment, dataset, data, back out again — in about ten minutes, with curl.
Everything below is curl. No SDK, no gRPC, no local stack. By the end you will
have written telemetry and read it back, which is enough to know the shape of
everything else.
Before you start
Create an API key
In the console: Settings → API Keys → Generate. Pick the Read & write preset — this walkthrough writes datasets, telemetry and events.
The key is shown once. Put it in your shell:
export XPECTRA_API_KEY=sk_xp_…
export XPECTRA_BASE=https://app.xpectraflow.comCheck it works
curl -s $XPECTRA_BASE/api/experiments/list -H "x-api-key: $XPECTRA_API_KEY"An array — possibly empty — means you are through. A 401 means the key is
wrong; a 403 names the scope you are missing. See
Authentication.
1. Create an experiment
The container everything else lives in.
curl -s -X POST $XPECTRA_BASE/api/experiments/create \
-H "x-api-key: $XPECTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Quickstart", "status": "active"}'{ "id": "9f2c1b44-…", "name": "Quickstart", "status": "active", "…": "…" }export EXPERIMENT_ID=9f2c1b44-…2. Register a stream
This is the call that matters, and it is easy to reach for the wrong one.
POST /api/datasets/create makes an empty dataset row with no storage and no
channels. POST /api/streams/register makes the dataset, its channel snapshot,
and the time-series table the data goes into — which is what you almost
always want.
curl -s -X POST $XPECTRA_BASE/api/streams/register \
-H "x-api-key: $XPECTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"experimentId": "'"$EXPERIMENT_ID"'",
"datasetName": "quickstart-run-1",
"sampleRateHz": 100,
"channels": [
{ "name": "thrust", "unit": "kN", "dataType": "float" },
{ "name": "chamber_pressure", "unit": "bar", "dataType": "float" }
]
}'{
"datasetId": "3d81f0a2-…",
"hypertableName": "hyper-3d81f0a2-…",
"channelIndices": [0, 1],
"ingestToken": "b7f1…9ac2"
}export DATASET_ID=3d81f0a2-…channelIndices is the important part. Your channels were assigned columns
ch_0 and ch_1, in the order you declared them. That index — not the
channel's UUID — is how you address the data from here on.
3. Write some rows
curl -s -X POST $XPECTRA_BASE/api/datasets/append \
-H "x-api-key: $XPECTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"experimentId": "'"$EXPERIMENT_ID"'",
"datasetId": "'"$DATASET_ID"'",
"channels": ["thrust", "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, 214.9] }
]
}'{ "inserted": 3, "duplicates": 0, "rowCount": 3 }Run it again. You get {"inserted": 0, "duplicates": 3, "rowCount": 3} — the
primary key on (time, dataset_id) makes a replayed batch a no-op, which is
what makes retrying safe.
4. Mark something interesting
curl -s -X POST $XPECTRA_BASE/api/datasets/events \
-H "x-api-key: $XPECTRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"experimentId": "'"$EXPERIMENT_ID"'",
"datasetId": "'"$DATASET_ID"'",
"events": [{
"label": "Ignition",
"startAt": "2026-07-31T12:00:00.010Z",
"endAt": "2026-07-31T12:00:00.010Z"
}]
}'{
"accepted": 1,
"anchor": { "source": "hypertable", "at": "2026-07-31T12:00:00.000Z" },
"events": [{ "id": "b31d…", "startTime": 0.01, "endTime": 0.01 }]
}You sent an absolute instant; we converted it against the dataset's T=0 — the first sample you wrote in step 3 — and echoed back the stored value so you can check the conversion. Ten milliseconds after the start, which is right.
5. Read it back
curl -s "$XPECTRA_BASE/api/datasets/channels?datasetId=$DATASET_ID&experimentId=$EXPERIMENT_ID" \
-H "x-api-key: $XPECTRA_API_KEY"[
{ "id": "c1a7…", "name": "thrust", "unit": "kN", "dataType": "float", "hypertableColName": "ch_0" },
{ "id": "de92…", "name": "chamber_pressure", "unit": "bar", "dataType": "float", "hypertableColName": "ch_1" }
]curl -s "$XPECTRA_BASE/api/datasets/list?experimentId=$EXPERIMENT_ID" \
-H "x-api-key: $XPECTRA_API_KEY"rowCount is 3 and startedAt/endedAt now bracket what you wrote.
Open the experiment in the console and the run is there, three points and an ignition marker on the chart.
Reading values back is not on the HTTP API. Bulk queries go over gRPC
Query, which returns downsampled series without shipping every raw point. The
console uses its own internal path for the same reason. See
gRPC streaming.
Where to go next
Choosing an ingestion path
HTTP is fine up to a few hundred points a second. Above that, read this before building.
Core concepts
What the four nouns mean, and what wide versus narrow storage changes.
API conventions
Errors, request ids, verbs — the rules every endpoint follows.
Commands
Getting a command out of XpectraFlow and into your own system.