Nine routes of the API follow something as it happens. They are GET requests that upgrade to a
WebSocket; each text message the server sends is one JSON document. The client sends nothing
(anything it sends is ignored); closing the socket stops the follow-up. Identity is not required
to follow.
| Route | Messages | Replays the past | Ends |
|---|---|---|---|
GET /v1/tc/{target}/{id}/events | TcEvent | yes, every event of the telecommand | on a final state |
GET /v1/tc/watch?target=… | TcWatched: {target, event}, the TcEvent with its target | no, events from now | never |
GET /v1/runs/{id}/events | RunEvent | yes, the whole log of the run | after finished |
GET /v1/targets/{target}/values/watch?measures=… | CurrentValue | the current values first | never |
GET /v1/alarms/watch?target=… | AlarmEvent | no, transitions from now | never |
GET /v1/instances/gateway/{instance}/throughput | ThroughputView | no, reports from now | never |
GET /v1/passes/watch?target=… | PassView | every pass first | never |
GET /v1/schedules/watch?target=… | Schedule | every schedule first | never |
GET /v1/transfers/watch?target=… | Transfer | every transfer first | never |
GET /v1/reconciler/events/watch?target=… | journal Entry | no, changes from now | never |
When the stream ends, the server sends a WebSocket close frame. Errors found before the upgrade
(an invalid target or identifier, the bus unavailable) answer a plain HTTP error with the
error body: 404 api::invalid-target, 404 api::invalid-id,
400 api::invalid-measure, 503 api::unavailable.
Telecommand events#
GET /v1/tc/{target}/{id}/events replays the events of the telecommand from the TC_EVENTS
stream, follows the new ones, and closes after a final state: REJECTED, ENCODE_FAILED,
SEND_FAILED, VERIFIED, VERIFY_FAILED, VERIFY_TIMEOUT or COMPLETE. Connecting after the
end still gives the whole chain.
{"tc": "01JA2Y8S5V5E6F7G8H9J0K1M2N", "state": "PENDING", "at": "2026-10-02T10:15:01.120Z",
"telecommand": {"component": "tcu", "instance": "TCU1", "telecommand": "set_mode"}}
{"tc": "01JA2Y8S5V5E6F7G8H9J0K1M2N", "state": "ENCODED", "at": "2026-10-02T10:15:01.131Z"}
{"tc": "01JA2Y8S5V5E6F7G8H9J0K1M2N", "state": "SENT", "at": "2026-10-02T10:15:01.140Z"}
{"tc": "01JA2Y8S5V5E6F7G8H9J0K1M2N", "state": "ECHO", "at": "2026-10-02T10:15:01.402Z", "conforming": true}
{"tc": "01JA2Y8S5V5E6F7G8H9J0K1M2N", "state": "VERIFIED", "at": "2026-10-02T10:15:03.010Z"}detail explains a failure or a rejection; ECHO is not a state and carries conforming. See
Telecommand Lifecycle.
Run events#
GET /v1/runs/{id}/events replays the log of the run from the RUNS stream, follows it, and
closes after its finished event. Each message has run, seq (position in the log, from 1),
at, and event with the fields of that event:
event | Fields |
|---|---|
started | procedure, library, environment, targets, ir, by, inputs |
procedure_started, procedure_finished | path; ok when finished |
step_started | path, attempt, inputs |
telecommand_sent | path, tc, target, telecommand, changes_state, retry_of |
telecommand_finished | path, tc, state, detail, acks (the chain of events) |
checked | path, statement (expect, check, wait_until), condition, ok, reason, samples |
logged | path, message, samples |
asked | path, prompt, role (for a hazardous confirmation) |
answered | path, accepted, by, value, role |
answer_refused | path, by, reason |
decision_required | path, reason |
suspended, continued | path, by; choice when continued |
taken_over | by (executor instance) |
step_finished | path, attempt, ok, reason |
finished | ok, reason |
{"run": "01JA2Z…", "seq": 1, "at": "…", "event": "started", "procedure": "Hot standby test",
"library": "platform-v3-steps@1.4.2", "environment": "AIT",
"targets": {"sat": "sim-1", "psu": "psu-sim-1"}, "ir": "9f3c…",
"inputs": {"tcu": "TCU2", "bus_voltage": 28.0}}
{"run": "01JA2Z…", "seq": 2, "at": "…", "event": "procedure_started", "path": "Hot standby test"}
{"run": "01JA2Z…", "seq": 3, "at": "…", "event": "step_started",
"path": "Hot standby test / Power the PPU", "attempt": 1}A path locates a procedure or a step: names separated by /, #<n> for an unnamed inline
step. When an asked or decision_required event arrives, answer with
POST /v1/runs/{id}/answers and its path. See Evidence and Reports.
Current values#
GET /v1/targets/{target}/values/watch sends the current value of each measure of the target,
then every update of the current value table. measures (comma-separated) selects measures:
| Selector | Selects |
|---|---|
tcu[TCU1].responding | one instance |
tcu.responding | every instance of the component |
responding | the measure in any component |
Without measures, every measure of the target. Each message is a CurrentValue:
{"component": "tcu", "instance": "TCU1", "measure": "anode_voltage",
"sample": {"value": 42.5, "raw": 9284, "time": "…", "ground_time": "…",
"link": "nominal", "delivery": "realtime"}}instance is absent for a single-instance component. See
Measures and Current Values.
Alarm transitions#
GET /v1/alarms/watch sends every alarm transition from now on, in order; target keeps those
of one target. Each message is an AlarmEvent:
{"target": "sat1-fm", "component": "tcu", "instance": "TCU1", "alarm": "anode_voltage",
"previous": "NORMAL", "state": "ACTIVE_UNACK", "severity": "warning", "value": 285.2,
"at": "…", "seq": 7, "cause": "condition"}Read the current states with GET /v1/alarms first, then follow: the transitions carry seq,
increasing per alarm. See Alarm Handling.
Passes, schedules and transfers#
GET /v1/passes/watch, GET /v1/schedules/watch and GET /v1/transfers/watch send every pass
(with the check of its gateway), schedule or transfer first, then each one again when it changes:
a pass refined by a feeder or whose gateway check changes, a schedule at risk, fired or missed, a
transfer that progresses. target keeps those of one target (for schedules, those on its
passes). Each message is the whole object, as GET /v1/passes, GET /v1/schedules/{id} and
GET /v1/transfers/{target}/{file_id}/{generation} return it: keep the last one of each
identifier.
Journal of the reconciler#
GET /v1/reconciler/events/watch sends each change the reconciler makes from now on: a link bound,
unbound or rebound, a target ready or not ready, a new revision of its configuration.
GET /v1/reconciler/events?since=…&limit=… reads the past ones, newest first:
{"at": "…", "target": "sim-1", "link": "nominal", "change": "rebound",
"driver": "platform-v3-sim-2", "gateway": "sim-gw-1", "reasons": []}Gateway throughput#
GET /v1/instances/gateway/{instance}/throughput relays the throughput reports of a gateway, for
each target it serves, as it publishes them (every metrics_period, 5 s by default):
{"target": "sat1-fm", "at": "…", "uplink_bps": 2048.0, "downlink_bps": 1843200.0,
"uplink_bytes": 51200, "downlink_bytes": 94371840, "window_ms": 5000}Examples#
With websocat:
websocat ws://localhost:8080/v1/runs/01JA2Z…/events
websocat 'ws://localhost:8080/v1/targets/sim-1/values/watch?measures=tcu%5BTCU1%5D.anode_voltage'
websocat 'ws://localhost:8080/v1/alarms/watch?target=sat1-fm'With the Python websockets package:
import asyncio, json
import websockets
async def follow(run: str) -> bool:
async with websockets.connect(f"ws://localhost:8080/v1/runs/{run}/events") as log:
async for text in log:
event = json.loads(text)
print(event["seq"], event["event"], event.get("path", ""))
if event["event"] == "finished":
return event["ok"]
return False
asyncio.run(follow("01JA2Z…"))The API client of the Python SDK does this for runs, with typed
events and reconnection: async for event in run.events().