XpectraFlow docs

API conventions

The rules every endpoint follows, so the endpoint pages can be short.

Read this once and the rest of the reference gets much shorter. Every endpoint below obeys all of it.

Base URL and endpoints

https://app.xpectraflow.com

This is the whole HTTP surface. Each row links to the page that documents it in full.

EndpointScope
GET/api/experiments/listexperiments:read
GET/api/experiments/getexperiments:read
POST/api/experiments/createexperiments:write
POST/api/experiments/updateexperiments:write
GET/api/datasets/listdatasets:read
GET/api/datasets/getdatasets:read
POST/api/datasets/createdatasets:write
POST/api/datasets/updatedatasets:write
POST/api/datasets/appendtelemetry:write
GET/api/datasets/channelsdatasets:read
POST/api/datasets/channels/createdatasets:write
POST/api/datasets/channels/updatedatasets:write
GET/api/datasets/eventsevents:read
POST/api/datasets/eventsevents:write
POST/api/commands/createcommands:write
GET/api/commands/listcommands:read
POST/api/commands/dispatchcommands:write
POST/api/commands/ack(one-time token)
POST/api/datasets/armcommands:write
POST/api/datasets/disarmcommands:write
GET/api/datasets/armingcommands:read
GET/api/webhooks/listwebhooks:read
GET/api/webhooks/deliverieswebhooks:read
POST/api/webhooks/createwebhooks:write
POST/api/webhooks/rotatewebhooks:write
POST/api/webhooks/deletewebhooks:write
POST/api/streams/registerdatasets:write
GET/api/keys/listkeys:read
POST/api/keys/revokekeys:write

Sustained ingestion does not go over HTTP — it goes over gRPC at app.xpectraflow.com:50051, authenticated with the same key. append is for batches, backfills and slow sensors, not for a 10 kHz feed. Anything you find outside the table above is internal and may change without notice.

There is no /api/v1 prefix. An earlier draft of these docs described one; the endpoints under it were never built, and versioning will be introduced when there is a second version to distinguish from — not before.

Authentication

One header, everywhere, including gRPC metadata:

x-api-key: sk_xp_…

See Authentication for creating and scoping a key.

Errors

Every failure has the same shape:

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key lacks the datasets:write scope.",
    "details": [],
    "request_id": "31592257-1558-48ed-8328-a5086ccfa4ed"
  }
}

Branch on code. It is stable for a given condition. message is prose for a human reading logs and may be reworded.

StatuscodeMeans
400invalid_requestMalformed body or parameters; see details
401unauthorizedMissing, malformed, expired or revoked key
403insufficient_scopeAuthenticated, but the key lacks the scope
403forbiddenAuthenticated and scoped, but not allowed
404not_foundNo such resource — or it is not yours
409conflictName already taken, or a state conflict
413payload_too_largeBody exceeded the limit
500internal_errorOur fault. request_id is what to quote

On validation failures details carries per-field messages:

"details": [{ "field": "channels.0.name", "message": "String must contain at least 1 character(s)" }]

404 deliberately conflates "does not exist" with "exists but belongs to another organization". Distinguishing them would confirm that an id is real, which is the one thing worth withholding from someone guessing ids.

Request ids

Send X-Request-Id and it is echoed back and used in our logs. Send nothing and one is generated. Either way it appears on the response — on success as well as failure — and inside every error body. Quote it when reporting a problem.

curl -si https://app.xpectraflow.com/api/experiments/list \
  -H "x-api-key: $XPECTRA_API_KEY" \
  -H "X-Request-Id: my-job-42"

Types

CasingcamelCase in JSON bodies and query parameters
TimestampsRFC 3339, UTC, milliseconds — 2026-07-30T12:00:00.000Z
IdsUUID v4 strings
Content typeapplication/json

The protobuf definitions use snake_case and epoch nanoseconds. That boundary is the gRPC wire format only; nothing in the HTTP API uses it.

Verbs

GETRead, with query parameters. Never changes anything
POSTCreate or change. Returns 200 and the resulting resource

PATCH is not offered, and neither is pagination: the list endpoints return everything they match, ordered newest first. Both will arrive with a documented shape rather than appearing quietly.

Nothing here deletes anything

There is no endpoint that deletes an experiment, a dataset or a channel, and this is not an omission waiting to be filled.

An API key is a static string that lives in a config file on a machine you may not fully control. Deleting an experiment cascades to its datasets and their telemetry, so a single leaked producer credential would be enough to erase a test campaign — and the scope that credential needs for its actual job, telemetry:write, is one a producer legitimately holds.

The demand for scripted deletion is close to zero next to that. Delete from the console, where a human is present and the action is attributable to them.

The one exception is /api/webhooks/delete, which unregisters a delivery target. That destroys no data, and being unable to stop deliveries over the API would make the feature less safe rather than more.

Updates replace, they do not merge

POST /api/experiments/update and POST /api/datasets/update are full replacements of the mutable fields. Omitting description clears it; omitting a required field is a 400.

This is stated rather than smoothed over because the alternative — accepting a partial body and merging — makes it possible to erase a field by forgetting it, and a validation error is a much better outcome than a silently blanked description.

On this page