Build an integration
Difficulty: Advanced. Time: about 45 minutes. Needs: a provisioned tenant API key and a language runtime of your choice.
You will build the canonical Constellation OS integration shape: a poller that reads fleet state from GET /topology, computes a derived metric your operators care about, and writes the result back through POST /telemetry so it shows up in the console like any other signal. This read-compute-writeback loop is the pattern behind most real integrations; once it runs, swapping in your own metric is the easy part.
The platform has no webhooks, streaming, or pagination, so polling is not a workaround here; it is the contract. See Integration patterns.
1. Design the loop
every 60s:
GET /topology?measurement=link&freshness_seconds=900
for each link entity:
margin_flag = snr_db - required_snr_db
POST /telemetry (measurement "telemetry", one record per link)
Two contract details shape the design:
- Reads only accept the five canonical measurement families (
link,satellite,ground_station,weather,telemetry). Derived metrics therefore write undertelemetrywith tags identifying them; that keeps them readable back through the same topology projection. - Budget requests, not records. The per-tenant limit is 60 requests per minute (burst 10 per second); one topology read plus one batched telemetry write per minute uses two.
2. Poll topology
curl -sS -H "x-api-key: $CONSTELLATION_API_TOKEN" \
"https://api.constellation.space/topology?measurement=link&freshness_seconds=900"
Parse entities, keeping each entity's entity_id, observed_at, and the fields you need. Treat an empty array as "no fresh links", not an error; widen freshness_seconds if your pipeline reports sparsely. A runnable four-language poller is in Poll topology.
3. Compute the derived metric
Keep the first metric trivially checkable, for example link margin against a required threshold:
def derive(link):
snr = link["fields"].get("snr_db")
if snr is None:
return None # skip honestly, never invent
margin = snr - REQUIRED_SNR_DB
return {
"measurement": "telemetry",
"time": link["observed_at"], # derived value inherits its source instant
"tags": {
"link_id": link["entity_id"],
"metric": "link_margin_db",
"source": "margin-poller",
},
"fields": {"link_margin_db": margin, "margin_ok": 1.0 if margin > 0 else 0.0},
}
Stamping time from observed_at rather than wall clock keeps the derived series aligned with its source, which matters when someone replays the window later.
4. Write back
Batch every derived record into one POST /telemetry call (JSON array, up to 1,000 records, 1 MB body). Expect 202 with an ack; log rejected_indices when nonzero, because partial rejection is precise and tells you exactly which records to inspect. See Ingest telemetry for client code.
Expected outcome: after two poll cycles, reading GET /topology?measurement=telemetry&freshness_seconds=900 returns one entity per link tagged metric=link_margin_db, and your derived signal is queryable in the console against the same clock as everything else.
5. Handle failure like production code
| Response | Handling |
|---|---|
401 auth_failed | Fatal. Stop, alert, fix the key. Never loop, or the lockout will take your IP out for 15 minutes. |
429 rate_limited / 429 auth_lockout | Sleep exactly Retry-After seconds, then resume. |
503 telemetry_unavailable | Transient data plane outage; honor Retry-After (typically 5 seconds), cap retries, keep the last good state. |
413 (either code) | A bug in your batching; split, do not retry as-is. |
422 | A malformed record; the detail list locates it by index and field. |
Add the platform's health endpoints (GET /health/*, no credentials) as a pre-flight and a monitoring probe; Monitor the platform shows the cron shape.
6. Harden and ship
- Read the key from the environment or a secrets manager; support rotation without redeploy.
- Emit one structured log line per cycle: entities read, records written, rejects, latency.
- Alert on consecutive cycle failures and on
margin_ok = 0transitions, not on single blips. - Run it server-side; browsers are blocked by the CORS allowlist by design. See API overview.
- Derived entities never appear: confirm you wrote
measurement: "telemetry"and are reading back the same family; a custom measurement name writes fine but is not readable through topology. - One entity instead of one per link: your identity tags collapse; make sure
link_id(or another distinguishing tag) differs per record. - Sporadic
429s under load: you are hitting the 10 per second burst; smooth the writes or batch harder. - Values look stale: check
observed_atversus yourfreshness_seconds; the projection only returns entities with samples inside the window.
Where next
- Examples for complete clients in TypeScript, Python, Go, and cURL.
- Tooling for the developer tooling around the platform.
- Errors and limits for the full error catalog this loop must survive.