Topology
GET /topology projects the newest observed sample per entity within a freshness window. If no observed telemetry is returned, it can provide labeled simulation geometry from the packaged catalog.
GET /topology
Auth: x-api-key, scope topology:read. See Authentication.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
as_of_utc | ISO 8601 datetime | now | End of the read window, exclusive. A sample exactly at this timestamp is outside the window. |
measurement | string | all measurements | Restrict to one measurement. When set, it must be one of link, satellite, ground_station, weather, attenuation, telemetry; anything else is 400 invalid_input. Omit it to return every measurement. |
freshness_seconds | float, 1 to 604800 | 300 | Look-back window. Only samples within freshness_seconds before as_of_utc are considered. |
Semantics
- Entity identity. The projection keys each row on the record's
entity_idtag. A record with noentity_idtag is skipped by topology (though it is still stored). Setentity_idwhen you ingest anything you want to read back here. See Telemetry. - Latest-per-entity. For each
(measurement, entity_id)with at least one sample inside the window, you get exactly one row: its newest sample. Entities silent for longer than the window simply disappear from the response; treat absence as "stale", not "deleted". - Read size. Keep freshness windows narrow for large fleets. The time-series query applies a configurable per-series row limit (50,000 by default), not a 50,000-row bound on the entire response.
- Simulation fallback. When no observed samples are returned, the API can generate approximate satellite, ground-station, and link geometry. Inspect
simulationandtags.data_sourcebefore treating a response as measured data. Other measurement filters can return empty results. - No pagination. There is no cursor or offset. The freshness window and the measurement filter are your only sizing tools.
Response
{
"as_of_utc": "2026-07-09T14:32:00Z",
"tenant_key": "acme-sat",
"entities": [
{
"measurement": "satellite",
"entity_id": "SAT-0012",
"observed_at": "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": "ground_station",
"entity_id": "GS-SEA-01",
"observed_at": "2026-07-09T14:31:55Z",
"tags": { "entity_id": "GS-SEA-01", "entity_type": "ground_station" },
"fields": { "lat": 47.44, "lon": -122.3, "utilization": 0.61 }
}
]
}
| Field | Description |
|---|---|
as_of_utc | The evaluation instant (echoes your parameter, or now). |
tenant_key | Request context resolved by the API. See Tenant isolation for plan boundaries. |
entities[].measurement | Which measurement the row came from. |
entities[].entity_id | Stable entity identifier, taken from the record's entity_id tag. |
entities[].observed_at | Timestamp of the newest sample for this entity. |
entities[].tags | String metadata. Observed samples can include producer_account_id; generated entities carry data_source=simulation. |
entities[].fields | Boolean, numeric, or string values. |
simulation | Present for generated geometry. Includes sampled, approximate, and per-entity issues. |
Example
curl -sS \
-H "x-api-key: $CONSTELLATION_API_TOKEN" \
"https://api.constellation.space/topology?measurement=satellite&freshness_seconds=900&as_of_utc=2026-07-09T14:32:00Z"
Replay: stepping as_of_utc
Because the projection is evaluated at an arbitrary instant, historical replay is just a loop: step as_of_utc through a time range at a fixed interval and issue one request per step. Each response is the fleet as it was known at that instant.
for t in 2026-07-09T12:00:00Z 2026-07-09T12:05:00Z 2026-07-09T12:10:00Z; do
curl -sS -H "x-api-key: $CONSTELLATION_API_TOKEN" \
"https://api.constellation.space/topology?measurement=satellite&freshness_seconds=300&as_of_utc=${t}"
done
Choose freshness_seconds for your sampling cadence and check each entity's observed_at. Budget replay loops against the rate limits.
For a handful of named entities, prefer GET /topology/series below: one request returns the whole window as channels, instead of one request per frame.
Series reads
GET /topology/series
Auth: x-api-key, scope topology:read.
GET /topology answers what exists now. This sibling answers how a named entity's measured channels moved over a window. Telemetry itself stays write-only — the raw evidence store is read through this projection, never re-exposed as its own verb.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
entity_id | string, repeatable | — | Required, 1 to 16 values. There is no unbounded tenant scan. Duplicates are collapsed. |
start | ISO 8601 datetime | — | Required. Range start, inclusive. |
end | ISO 8601 datetime | now | Range end, exclusive. |
measurement | string | telemetry | One of link, satellite, ground_station, weather, attenuation, telemetry. |
fields | comma-separated string | all numeric fields | Restrict the returned channels to these field names. |
interval | 15s, 30s, 1m, 5m, 15m, 1h | none | Aggregation window. Required beyond a one-hour raw span. |
agg | mean, min, max, last | mean when interval is set | Aggregation function. Setting it without interval is an error. |
Bounding rules
Bounding is explicit rather than silent. Three limits apply before the query runs:
- Raw reads cap at a one-hour span. Without
intervalthe server cannot know your sampling cadence, so span is the only available guard. A longer raw range returns422 interval_requiredcarryingmin_interval, the smallest interval that fits. - Aggregated reads are budget-checked up front. Entity count times window count must fit the point budget (10,000 by default). Over it,
422 interval_too_fine, again carryingmin_interval— ornullwhen no interval fits, meaning you must narrow the span or the entity list. - The series query horizon defaults to 30 days. This endpoint limit is not a data-retention or archival policy. An earlier
startreturns422 history_horizon_exceededwithhorizon_days. Use the environment's configured horizon when planning requests.
Response
{
"measurement": "ground_station",
"start": "2026-07-27T12:00:00Z",
"end": "2026-07-27T13:00:00Z",
"interval": "1m",
"agg": "mean",
"series": [
{
"entity_id": "GS-SEA-01",
"times": ["2026-07-27T12:00:00Z", "2026-07-27T12:01:00Z"],
"channels": { "utilization": [0.61, 0.58] },
"point_count": 2
}
],
"truncated": false,
"next_start": null
}
The shape is channel-form: one times array per entity, with each field's values aligned to it positionally. A field missing at a timestamp is null at that position, so you can plot or diff a signal without re-keying rows. Channels carry numeric values only.
One entry per requested entity, in request order. An entity with no data in range is present with empty arrays rather than absent — you can tell "no data" from "not asked for".
interval and agg echo the effective values, and both are null on a raw read.
Truncation and continuation
When the point budget cuts the response short, truncated is true and next_start is the timestamp to resume from. Reissue the same request with start set to next_start and everything else unchanged. There is no cursor and no opaque token; continuation is a parameter change you can inspect.
Aggregation windows are epoch-aligned and labeled by their start, which is what makes this exact: a next_start cutoff always lands on a window boundary, so the follow-up read re-aggregates the first excluded window in full instead of losing data behind a partial one.
Series errors
| Status | Body | Cause |
|---|---|---|
400 | {"error": "invalid_input", "field": "<field>"} | An entity id, measurement, or field name contains characters the store rejects. |
422 | {"error": "unknown_measurement", "allowed": [...]} | measurement outside the six canonical names. |
422 | {"error": "agg_requires_interval"} | agg set without interval. |
422 | {"error": "invalid_range", ...} | end is not after start. |
422 | {"error": "history_horizon_exceeded", "horizon_days": 30} | start is more than 30 days ago. |
422 | {"error": "interval_required", "min_interval": "1m"} | Raw span over one hour. Retry with the named interval. |
422 | {"error": "interval_too_fine", "min_interval": "5m"} | Requested interval would exceed the point budget. Retry with the named interval, or narrow the request. |
503 | {"error": "telemetry_unavailable", ...} | Time-series backend transient; retry after the delay. |
A requested entity with no matching samples returns empty series arrays.
Enforce the 1-to-16 entity bound in your client; the API rejects a request outside it.
Errors
| Status | Body | Cause |
|---|---|---|
400 | {"error": "invalid_input", "field": "measurement"} | measurement set but outside the allowlist. |
422 | detail list | Unparseable as_of_utc or freshness_seconds out of the 1 to 604800 range. |
401 / 403 | auth_failed / insufficient_scope | Key invalid, or the key lacks topology:read. See Authentication. |
429 | rate_limited or auth_lockout | Rate limits; honor Retry-After. |
503 | {"error": "telemetry_unavailable", "upstream": "influx", "retry_after_seconds": 5} | Time-series backend transient; retry after the delay. |
Related
- Ingest telemetry: what feeds this projection.
- Errors and limits: rate limits and retry rules.
Next: Predictions — forecasts computed from the same telemetry.