Skip to content

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.

GET /api/v1/events?source_id=amr-west-07&fields=power.battery.state_of_charge_pct,location.local
{"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>.