Reading UDM Data¶
GET /api/v1/events lists a tenant's events (scope events:read). Filter by source_id, source_type, event_type, session_id, start_time and end_time, and add include=event_data for the full UDM payload. Pages hold up to 1,000 events (page_size, default 100).
Paging through history¶
Use a cursor to read more than a page or two. Each cursor page costs the same, while a page number's offset gets slower the further it goes.
| Read | First request | Next page |
|---|---|---|
| Oldest first | order=asc (or after=0) | after=<next_after> |
| Newest first | default order | before=<next_before> |
The response carries next_after or next_before while has_more is true. Cursors are event sequence_numbers, so a read keeps its place while new events arrive.
Add count=none to skip total_count. Counting every match costs a full pass over the matches on each page. has_more doesn't need it.
# One robot's whole history, oldest first, 1000 events per request.
curl -s "$PHYCLOUD/api/v1/events?source_id=amr-west-07&order=asc&count=none&page_size=1000&include=event_data" \
-H "Authorization: Bearer $KEY"
# ...then repeat with &after=<next_after> until has_more is false.
page still works, with total_count, for small listings.
Selecting fields¶
fields returns only the listed parts of each event's UDM payload, as comma-separated dotted paths. It implies include=event_data. A robot-hour of full events is about 48 MB; one field of it is a small fraction of that.
{"event_data": {"power": {"battery": {"state_of_charge_pct": 81.5}},
"location": {"local": {"x_m": 12.4, "y_m": -3.1}}}}
Path segments may hold letters, digits and _. A path an event doesn't have is left out of that event.
Filtering on UDM values¶
where keeps events whose UDM field meets a condition. Repeat it to combine conditions (all must hold); up to 16 are allowed.
GET /api/v1/events?source_id=amr-west-07&where=power.battery.state_of_charge_pct<20&where=motion.speed_mps>1
| Part | Rules |
|---|---|
| Path | A dotted path into event_data, e.g. power.battery.state_of_charge_pct |
| Operator | = != < <= > >= |
| Value | A number, true/false, or text (optionally in double quotes). Booleans and text take only = and !=. |
A filter matches only fields of the value's type: speed_mps>1 skips events without a speed. Filters run in the database, after the indexed filters (source_id, time range), so narrow the range first for big tenants. URL-encode the operator characters (< is %3C).
Series over time¶
GET /api/v1/sources/{source_id}/series summarises one numeric UDM field per time bucket, in the database (scope events:read). A day of one robot's battery voltage is a few hundred points, not hundreds of thousands of events.
GET /api/v1/sources/amr-west-07/series?field=power.battery.voltage_v&bucket=300&start_time=2026-11-03T00:00:00Z&end_time=2026-11-04T00:00:00Z
{"source_id": "amr-west-07", "field": "power.battery.voltage_v", "bucket_sec": 300,
"points": [{"start": "2026-11-03T00:00:00Z", "count": 1500, "min": 16.1, "max": 16.4, "avg": 16.27}, ...]}
bucket is in seconds (default 60). The range defaults to the last 24 hours, and a request may cover at most 10,000 buckets. Buckets with no numeric value are left out.
Exporting¶
GET /api/v1/events/export streams every matching event as NDJSON, one event per line (scope events:read). It takes the same filters as the list, plus fields and where. Events come oldest first, or newest first with order=desc. PhyCloud reads the database a page at a time as you consume the stream, so a day of a robot's data is one request.
curl -sN "$PHYCLOUD/api/v1/events/export?source_id=amr-west-07&start_time=2026-11-03T00:00:00Z&end_time=2026-11-04T00:00:00Z&fields=power,location" \
-H "Authorization: Bearer $KEY" | gzip > amr-west-07-2026-11-03.ndjson.gz
Each line has the event's id, type, source, captured_at, ingested_at, sequence_number, content_hash and event_data. If the stream stops early, resume with after=<last sequence_number>.