Skip to main content

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​

ParameterTypeDefaultDescription
as_of_utcISO 8601 datetimenowEnd of the read window, exclusive. A sample exactly at this timestamp is outside the window.
measurementstringall measurementsRestrict 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_secondsfloat, 1 to 604800300Look-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_id tag. A record with no entity_id tag is skipped by topology (though it is still stored). Set entity_id when 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 simulation and tags.data_source before 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 }
}
]
}
FieldDescription
as_of_utcThe evaluation instant (echoes your parameter, or now).
tenant_keyRequest context resolved by the API. See Tenant isolation for plan boundaries.
entities[].measurementWhich measurement the row came from.
entities[].entity_idStable entity identifier, taken from the record's entity_id tag.
entities[].observed_atTimestamp of the newest sample for this entity.
entities[].tagsString metadata. Observed samples can include producer_account_id; generated entities carry data_source=simulation.
entities[].fieldsBoolean, numeric, or string values.
simulationPresent 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​

ParameterTypeDefaultDescription
entity_idstring, repeatable—Required, 1 to 16 values. There is no unbounded tenant scan. Duplicates are collapsed.
startISO 8601 datetime—Required. Range start, inclusive.
endISO 8601 datetimenowRange end, exclusive.
measurementstringtelemetryOne of link, satellite, ground_station, weather, attenuation, telemetry.
fieldscomma-separated stringall numeric fieldsRestrict the returned channels to these field names.
interval15s, 30s, 1m, 5m, 15m, 1hnoneAggregation window. Required beyond a one-hour raw span.
aggmean, min, max, lastmean when interval is setAggregation 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 interval the server cannot know your sampling cadence, so span is the only available guard. A longer raw range returns 422 interval_required carrying min_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 carrying min_interval — or null when 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 start returns 422 history_horizon_exceeded with horizon_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​

StatusBodyCause
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​

StatusBodyCause
400{"error": "invalid_input", "field": "measurement"}measurement set but outside the allowlist.
422detail listUnparseable as_of_utc or freshness_seconds out of the 1 to 604800 range.
401 / 403auth_failed / insufficient_scopeKey invalid, or the key lacks topology:read. See Authentication.
429rate_limited or auth_lockoutRate limits; honor Retry-After.
503{"error": "telemetry_unavailable", "upstream": "influx", "retry_after_seconds": 5}Time-series backend transient; retry after the delay.

Next: Predictions — forecasts computed from the same telemetry.