XpectraFlow docs

Authentication

Create an API key in the console, send it as x-api-key, and scope it to what it actually needs.

XpectraFlow has one credential: a static API key that you create in the console and send as a header. There is no token exchange, no refresh, no expiry dance. The same key authenticates an HTTP call and a gRPC stream.

The tradeoff is deliberate. A key is a long-lived secret, so it does not expire on its own and a leak lasts until somebody revokes it. What makes that manageable is the other two properties: a key carries scopes, which are enforced on every request, and it carries an expiry date, which you set when you create it.

1. Create a key

In the console: Settings → API Keys → Generate. You choose three things.

NameWhat it is for. One key per integration, so revoking one is surgical.
Expires30 days, 90 days, 1 year, a custom date, or never. Default is 90 days.
AccessEither preset (read only / read & write), or tick individual scopes.

The key is shown once, on the screen that creates it. Only a SHA-256 digest is stored, so it cannot be recovered — losing it means creating a new key.

Keys look like sk_xp_ followed by 64 hex characters. The console shows only the first and last few afterwards, so you can tell two keys apart in the table without the table being a place to read secrets from.

2. Use it

curl -s https://app.xpectraflow.com/api/experiments/list \
  -H "x-api-key: $XPECTRA_API_KEY"

The same header works as gRPC metadata:

metadata = [("x-api-key", api_key)]
stub.Submit(request, metadata=metadata)

The Python SDK reads XPECTRA_API_KEY from the environment, so in most cases you never write the header yourself:

export XPECTRA_API_KEY=sk_xp_…
from xpectra import Client

client = Client()          # picks up XPECTRA_API_KEY
print(client.experiments.list())

Scopes

<resource>:<action>. No wildcards — a wildcard quietly grants a resource added two years later.

ScopeGrantsRequired by
experiments:readList and read experiments/api/experiments/{list,get}
experiments:writeCreate and update experiments/api/experiments/{create,update}
datasets:readList datasets and their channels/api/datasets/{list,get,channels}
datasets:writeCreate and update datasets and channels; register streams/api/datasets/{create,update}, /api/datasets/channels/{create,update}, /api/streams/register
telemetry:readQuery telemetry and signalsgRPC Query
telemetry:writeIngest telemetry points, frames and batches/api/datasets/append, gRPC Submit, StreamSubmit
events:readRead dataset events/api/datasets/events (GET)
events:writeAppend dataset events/api/datasets/events
commands:readRead the command log and arming state/api/commands/list, /api/datasets/arming
commands:writeLog and dispatch commands; arm datasets/api/commands/{create,dispatch}, /api/datasets/{arm,disarm}
keys:readList this organization's API keys/api/keys/list
keys:writeRevoke API keys — not create them/api/keys/revoke
webhooks:readList webhook endpoints and delivery attempts/api/webhooks/{list,deliveries}
webhooks:writeRegister, rotate and remove webhook endpoints/api/webhooks/{create,rotate,delete}

Scopes with no endpoint in the third column are part of the vocabulary but nothing requires them yet. Granting one today does nothing; it will start meaning something when the matching endpoint ships.

:write implies :read on the same resource. A writer that cannot read back what it wrote is a support ticket, not a security boundary — so a key granted telemetry:write also holds telemetry:read, and the console shows it that way when you tick the box.

Owner-only scopes

keys:write and webhooks:write can only be granted by a workspace owner. Both change the organization's security posture rather than its data — one revokes credentials, the other decides where command traffic is delivered — and a key should never reach further than the person who minted it.

They are also absent from the Read only and Read & write presets. Nobody ticking "Read & write" expects to hand out the ability to revoke other credentials, so those scopes have to be chosen deliberately, one at a time.

keys:write is the one scope whose name overstates what it does. It revokes keys. It does not grant creating them, and no scope does — see API keys for why a key that can mint keys would defeat the only lever we have against a leak.

Grant the narrowest set that works. A telemetry producer wants telemetry:write, not everything.

When a key lacks a scope

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

403, and the message names the scope that was missing. Over gRPC the same condition is PERMISSION_DENIED with the same text.

This is the failure mode worth designing for. A key created with the "Read only" preset can list experiments and datasets, and will be refused when it tries to register a stream or append an event. If an integration starts failing after a key change, the scope named in the message is the whole diagnosis.

Expiry and revocation

An expired key returns 401 with This API key has expired. Nothing warns you in advance, so set the expiry to something you will notice — a key that expires mid-flight is worse than one that expires on a Tuesday morning.

Revoking is immediate in the console (Revoke on the key's row) and takes effect on the HTTP API on the next request. It can also be scripted — see API keys for POST /api/keys/revoke and the fleet-rotation sequence.

gRPC lags by up to 5 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 a key because it leaked, stop the stream as well as revoking the key.

The same cache applies in the other direction: widening a key's scopes takes up to 5 minutes to be visible on the gRPC path.

Keeping a key safe

  • Never commit one. Read it from the environment. A key in a public repository is live from the moment it is pushed, and history keeps it live after the commit that removes it.
  • One key per integration. Shared keys mean revoking the leaked one takes down four things you did not intend to touch.
  • Set an expiry. It is the only thing that limits the blast radius of a leak nobody noticed.
  • Scope it down. A key that can only read cannot be used to write.

If you suspect a key has leaked, revoke it first and investigate afterwards. The key's last_used_at survives revocation, so the record of what it was doing is still there once the credential is dead.

On this page