Telemetry
POST /telemetry is the HTTP telemetry write endpoint. Gateways, simulators, and batch jobs push JSON arrays of records; the telemetry behind the Topology projection and prediction features flows through here.
POST /telemetry
Content-Type: application/json
Auth: x-api-key, scope telemetry:write. See Authentication.
Request body
The body is a bare JSON array of records, not a wrapper object:
[
{
"measurement": "satellite",
"time": "2026-07-09T14:31:42Z",
"tags": { "entity_id": "SAT-0012", "entity_type": "satellite" },
"fields": { "lat": 47.61, "lon": -122.33, "altitude_km": 550.2, "utilization": 0.42 }
},
{
"measurement": "link",
"time": "2026-07-09T14:31:42Z",
"tags": { "entity_id": "SAT-0012-GS-SEA-01" },
"fields": { "snr_db": 12.4, "utilization": 0.55 }
}
]
| Field | Type | Required | Description |
|---|---|---|---|
measurement | string, non-empty | yes | Series name. Any non-empty name is accepted on write, on both REST and gRPC. The six-name set only narrows the GET /topology?measurement= read filter; see Transports. |
time | ISO 8601 datetime | yes | Sample timestamp. A naive (no-offset) value is read as UTC. |
tags | object of string values | no, default empty | Indexed dimensions, at most 64 per record. Set entity_id on every record: Topology keys the entity on it, and predictions look up a model's inputs by it. |
fields | object of boolean, numeric, or string values | Include at least one field | Measured values. Records without fields are rejected. Numeric values are stored as floats; use strings for exact large-integer identifiers. |
Record handling:
- Request identity. Use the key and endpoint issued for your account or deployment. Client-supplied ownership tags do not establish an isolation boundary. See Tenant isolation.
- No write allowlist on either transport.
POST /telemetryand the gRPCTelemetryIngest.Writeboth accept anymeasurementname, and any tags and fields. The six-name set (link,satellite,ground_station,weather,attenuation,telemetry) constrains only themeasurementquery filter onGET /topology. An unfiltered topology read still projects any measurement, so a custom name written over either transport is stored and readable; you just cannot narrow a topology read to it by name. See Transports. - Entity keying.
GET /topologyprojects one row per entity keyed on the record'sentity_idtag. Over REST, a record with noentity_idtag is ingested but skipped by the topology projection. Over gRPC it is rejected outright. Setentity_idon anything you expect to read back as fleet state.
Response: 202 acknowledgment
A 202 response reports acceptance and rejection counts:
{
"write_id": "w-8c41f2ae",
"accepted_count": 118,
"rejected_count": 2,
"rejected_indices": [4, 87]
}
| Field | Description |
|---|---|
write_id | Server identifier for this write; log it for support and reconciliation. |
accepted_count | Records accepted by the configured ingest path. In asynchronous mode, acceptance precedes durable storage. |
rejected_count | Records rejected. |
rejected_indices | Zero-based indices into your submitted array, or null when nothing was rejected. |
Records with no fields or timestamps before 2000-01-01T00:00:00Z are rejected individually. HTTP responses identify their positions in rejected_indices. Future timestamps are clamped to the ingest time.
A synchronous acknowledgment follows the store write. Asynchronous ingestion acknowledges buffer acceptance and can lose buffered data if the process fails before flushing. Verify delivery through topology when persistence matters; a 202 alone is not a universal durability guarantee.
Handling partial rejection
A 202 with rejected_count > 0 means part of the batch was rejected. Avoid replaying accepted records:
- Map
rejected_indicesback to the records you sent. - Correct the rejected records, including missing fields or invalid timestamps.
- Resubmit only the corrected records.
Limits
| Limit | Value | Error |
|---|---|---|
| Records per batch | 1,000 | 413 {"error": "batch_too_large", "max": 1000, "received": N} |
| Tags per record | 64 | 400 {"error": "invalid_input", "field": "tags"} |
| Body size | 1 MiB | 413 {"error": "request_too_large", ...} |
Content-Length | required | 411 {"error": "length_required"} |
Enforce both the record count and encoded body-size limits. Inspect per-record rejections in successful responses. See Ingest telemetry.
Choose a sampling cadence for your equipment, then batch within the request-size and rate limits.
Transports
Telemetry can be published over HTTP or gRPC. Both require ingestion access. The fleet agent uses gRPC by default and also supports HTTP egress.
REST POST /telemetry | gRPC TelemetryIngest.Write | |
|---|---|---|
| Auth | x-api-key header | x-api-key request metadata |
| Records per call | 1,000 | 10,000 |
| Payload | JSON array | protobuf TelemetryRecord |
| Access | Ingestion enabled with the required scope | Ingestion enabled with the required scope |
measurement | any non-empty name | any non-empty name |
entity_id tag | optional | required, non-empty |
| Booleans | true / false preserved | true / false preserved |
| Integers | coerced to float | coerced to float |
| Missing fields or pre-2000 timestamp | Per-record rejection with indices | Per-record rejection count, without indices |
| Invalid field or tag content | Request rejected | Call rejected with INVALID_ARGUMENT |
| Designed shape | any client, any cadence | one warm channel per ground station, batched write about once a second |
gRPC uses a unary Write call. Its default batch cap is 10,000 records and can vary by environment. HTTP middleware lockout and rate-limit thresholds do not apply to the gRPC listener; use the RPC status codes below for recovery.
Cross-transport hazards
- Field types pin per field. The time-series store fixes one type per field name. Keep each field's type consistent across producers and transports.
entity_idis required on gRPC. REST ingests a record without it (topology just skips it); gRPC rejects the whole call. Setentity_idon every record.
Status codes
| Status | Meaning |
|---|---|
INVALID_ARGUMENT | invalid_input:<field>, where <field> is entity_id or tags. Not retryable — the whole call was rejected and the payload must be fixed |
UNAUTHENTICATED | Missing or invalid API key |
PERMISSION_DENIED | Missing telemetry:write scope or ingestion entitlement |
RESOURCE_EXHAUSTED | batch_too_large (split and resend) or ingest_buffer_full (back off, resend the same payload) |
UNAVAILABLE | Authentication infrastructure, entitlement lookup, or storage is unavailable. Back off before retrying. |
INTERNAL | A store-side deterministic failure. Inspect the record and contact support; do not replay in a tight loop. |
The two RESOURCE_EXHAUSTED cases carry distinct messages because the remedies differ: one needs a smaller batch, the other needs patience.
TelemetryIngest.Health is open and takes no API key, but do not branch on its ok field — it is always true. Liveness travels in the gRPC status: an unhealthy server aborts with UNAVAILABLE rather than answering ok=false. A client checking health must catch the RPC error, not read the body.
The endpoint is on port 443, TLS required, HTTP/2 only — there is no plaintext port: ingest.api.constellation.space:443. See Environments.
Idempotency warning
The API does not offer a request idempotency key or an exactly-once delivery guarantee. Treat a timed-out write as ambiguous and reconcile delivery before replaying it.
- For authentication failures, rate limits, schema errors, or body caps, resolve the cause and follow retry guidance. A store-side field conflict may involve a partial write; reconcile the batch before retrying.
- Ambiguous: timeouts and dropped connections after the request was sent. The batch may or may not have landed. Log the batch and its intended
write_idcontext for reconciliation instead of auto-replaying, or accept the double-write risk explicitly.
Example
Use Ingest telemetry to publish a current sample and read it back.
Errors
| Status | Body | Cause |
|---|---|---|
400 | {"error": "invalid_input", "field": "tags"} | More than 64 tags on a record; fails the batch. |
400 | {"error": "field_type_conflict", "field": "<field>", ...} | The field was written with a different type. Correct it and reconcile any partial write. |
403 | {"error": "live_ingest_not_entitled"} | Account ingestion access is not enabled. |
503 | {"error": "ingest_buffer_full", "retry_after_seconds": 1} | Buffer capacity exhausted. Honor Retry-After and retry. |
503 | {"error": "entitlement_unavailable"} | Ingestion access could not be checked. Back off. |
422 | detail list | A record failed schema validation (empty measurement, bad time, wrong tag types). |
413 | batch_too_large or request_too_large | Over the batch or body cap; split and resend. |
411 / 400 | length_required / invalid_content_length | Content-Length problems. |
401 / 403 | auth_failed / insufficient_scope | Key invalid, or the key lacks telemetry:write. See Authentication. |
429 | rate_limited or auth_lockout | Rate limits; honor Retry-After. |
503 | {"error": "telemetry_unavailable", "upstream": "influx", "retry_after_seconds": 5} | Backend transient; retrying is a write, so mind the idempotency warning. |
Related
- Topology: how ingested records surface as fleet state.
- Ingest telemetry example: write and verify a current sample.
- Fleet agent: collect and deliver telemetry from your host.
Next: Predictions — how ingested telemetry becomes model forecasts.