XpectraFlow docs
Ingestion

gRPC streaming

The sustained path — 10 kHz, validated to 20 kHz, authenticated with the same API key.

For anything continuous. Same dataset, same ch_N columns, same channel list as HTTP append — only the transport changes.

app.xpectraflow.com:50051

Confirm reachability before building against this. In the reference deployment the gRPC port is bound to loopback and is not exposed through the TLS front, so a self-hosted stack may not accept external connections on :50051 without additional configuration. If you are self-hosting, check your own network setup; if you are on our hosted service, ask us and we will confirm what your account can reach.

Authentication

The same x-api-key, as gRPC metadata:

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

Scopes: telemetry:write to ingest, telemetry:read to query.

Key changes lag by up to five minutes on gRPC. 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, and a widened one takes as long to take effect. If you are revoking because a key leaked, stop the stream as well.

The service

xpectra.telemetry.v1.TelemetryService:

RPCShapeScope
Submitunary — a batch of pointstelemetry:write
StreamSubmitclient-streaming — header, then pointstelemetry:write
Queryunary — downsampled read-backtelemetry:read
message TelemetryPoint {
  google.protobuf.Timestamp timestamp = 1;
  string channel_id = 2;
  oneof type { string string = 3; double double = 4; float float = 5; bool bool = 6;
               int32 int32 = 7; uint32 uint32 = 8; int64 int64 = 9; uint64 uint64 = 10;
               bytes bit_field = 11; uint32 enum = 12;
               google.protobuf.Empty empty = 13; bytes bytes = 14; }
}

channel_id is the channel index as a string — "0", "1" — not the channel UUID.

It comes from the ch_N column in GET /api/datasets/channels. Sending the UUID produces a stream that connects, authenticates, reports points accepted, and writes nothing you can read back. This is the most common mistake made against this API.

StreamSubmit requires the first message to be a StreamSessionHeader carrying experiment_id and dataset_id; every message after that is a point.

Reading back

Query takes a channel list and a time range and returns downsampled series, with total_raw_points telling you how much it summarised.

That is the point of it being gRPC rather than JSON over HTTP: a 20-minute flight at 10 kHz is 12 million rows per channel, and no reasonable response ships all of them.

message QueryRequest {
  string experiment_id = 1;
  string dataset_id = 2;
  repeated string channel_ids = 3;
  google.protobuf.Timestamp from = 4;
  google.protobuf.Timestamp to = 5;
  int32 max_points_per_channel = 6;
}

Getting the stubs

The definitions live in xpectra-proto. Go stubs are committed; other languages generate from the .proto with your own toolchain.

For Python, the SDK wraps all of this and is the shorter path.

Rate

Validated at 20 kHz across 8 channels sustained, with no dropped frames. Guidance for sizing a batch: aim for a few kilobytes to tens of kilobytes per message — at 10–20 kHz that is roughly 200–1000 rows, giving 10–100 messages per second per stream.

Smaller than that and per-message overhead dominates; much larger and a single retransmit costs more than it saves.

Next

On this page