XpectraFlow docs
API reference

API keys

List and revoke keys from the API. Creating one is console-only, deliberately.

Two operations, and one conspicuous absence.

List the organization's live keysGET /api/keys/list
Revoke onePOST /api/keys/revoke
Create oneConsole only. No scope grants it

Why there is no create endpoint

Revocation is the only lever this product has against a leaked credential. revokedAt exists precisely so a key can be killed mid-flight.

A key that can mint keys defeats that lever entirely. Whoever holds the leaked one mints a fresh key, with a fresh hash and a long expiry, and revoking the original accomplishes nothing. Worse, the audit trail then reads "a legitimate key created a key" — a sentence with no anomaly in it, which is exactly the sentence an incident review needs to find.

The operational need people actually have is fleet rotation and incident response: list every key we hold, kill the ones on the compromised host. Both of those are read and revoke. Creating a credential is something a human does once, in a browser, and pastes into a config.

So: create a key in the console at Settings → API Keys → Generate. See Authentication for the walkthrough.

Scopes

Both operations use a keys scope, and neither is granted by default.

ScopeGrants
keys:readList the organization's live keys
keys:writeRevoke a key. Does not grant creating one

keys:write is the one scope whose name overstates what it does. It follows the <resource>:<action> convention for consistency; it revokes and nothing else.

It is also owner-only. Only a workspace owner can create a key carrying keys:write or webhooks:write, because both change the organization's security posture rather than its data — and a key should never reach further than the person who minted it.

List keys

GET/api/keys/listscope keys:read

No parameters. Revoked keys are omitted — a list that shows dead credentials trains people to ignore the list.

curl -s https://app.xpectraflow.com/api/keys/list \
  -H "x-api-key: $XPECTRA_API_KEY"
[
  {
    "id": "4c1e9a72-3b58-4f01-9d6a-2e7f0c8b1d43",
    "name": "sitl-gateway",
    "keyPrefix": "sk_xp_2f",
    "keySuffix": "9c4e",
    "scopes": ["telemetry:write", "datasets:write"],
    "lastUsedAt": "2026-07-31T09:41:02.884Z",
    "expiresAt": "2026-10-29T00:00:00.000Z",
    "createdAt": "2026-07-31T08:55:19.220Z"
  }
]

The key itself is not here and cannot be — only a SHA-256 digest is stored. keyPrefix and keySuffix are enough to render sk_xp_2f…9c4e and match a row against the value in a config file, and not enough to authenticate with.

lastUsedAt is the field worth building on. It turns "which of these eleven keys can we safely kill" from a guess into a query. It is written fire-and-forget on every authenticated request, so it can lag by a moment under load — treat it as "recently used", not as an exact timestamp.

Revoke a key

POST/api/keys/revokescope keys:write

Prop

Type

curl -s -X POST https://app.xpectraflow.com/api/keys/revoke \
  -H "x-api-key: $XPECTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "4c1e9a72-3b58-4f01-9d6a-2e7f0c8b1d43"}'
{ "success": true, "alreadyRevoked": false }

A repeat call returns alreadyRevoked: true and still succeeds. You asked for a state and the state holds; erroring would make a double-click look like a bug.

Revocation is a timestamp, not a delete. The row survives, so "what was this credential doing before we killed it" remains answerable — which is the only moment anyone asks.

A key cannot revoke itself

{
  "error": {
    "code": "conflict",
    "message": "A key cannot revoke itself. Revoke it from the console, or from a different key.",
    "details": [],
    "request_id": "31592257-1558-48ed-8328-a5086ccfa4ed"
  }
}

Without this, a rotation script with an off-by-one turns into an outage with no recovery path except a browser session.

Revocation lags on gRPC by up to five minutes. The ingest service caches key validity so a 20 kHz stream is not doing an HTTP round trip per connection. A revoked key can keep streaming for the length of that cache.

If you are revoking because a key leaked, stop the stream as well.

Rotating a fleet

The intended shape, using only these two endpoints plus one console visit:

Mint the replacement in the console. One key per host, named after the host.

Deploy it. The old key still works; there is no cutover window.

Confirm the new key is live. GET /api/keys/list and check lastUsedAt on the new key is recent.

Revoke the old one by id, from a different key — a script cannot revoke the credential it is authenticating with.

Wait five minutes before assuming gRPC ingest with the old key has stopped.

Errors

StatuscodeWhen
400invalid_requestid missing or not a UUID
403insufficient_scopeKey lacks keys:read or keys:write
404not_foundNo such key, or it belongs to another organization
409conflictThe key tried to revoke itself

On this page