Reading resources

Filters, field masks, ordering, and pagination.

Read requests across SPEKTRA Edge share the same four controls, whichever service and whichever protocol you use:

Control Question it answers
filter Which resources?
view and fieldMask Which fields of each?
orderBy In what order?
pageSize and pageToken How many at a time?

Learn them once and every collection in the platform is queryable. Getting them right is also the difference between an integration that stays fast at ten thousand devices and one that does not.

Filtering

A filter is a SQL-flavored expression evaluated server-side.

status.connectionStatus="CONNECTED" AND metadata.labels CONTAINS "site:berlin"

Field paths are the resource’s fields in lowerCamelCase, dotted to reach nested messages, exactly as they appear in the JSON representation. String values must be double-quoted. Keywords are case-insensitive, so AND and and are equivalent.

AND is also implicit — adjacent conditions are combined without it, and + works as an alias. These three are the same filter:

spec.enabled=true AND status.state="READY"
spec.enabled=true status.state="READY"
spec.enabled=true + status.state="READY"

You can annotate long filters: -- begins a comment that runs to the end of the line, and newlines are insignificant.

-- tills that have stopped reporting
status.connectionStatus="DISCONNECTED"
metadata.labels CONTAINS "role:till"

Operators

These are implemented and safe to rely on:

Operator Example
= != < > <= >= spec.osVersion!="1.0.2"
IN status.state IN ["READY","DEGRADED"]
CONTAINS (alias HAS) metadata.labels CONTAINS "site:berlin"
CONTAINS ANY metadata.labels CONTAINS ANY ["site:berlin","site:hamburg"]
CONTAINS ALL metadata.labels CONTAINS ALL ["role:till","env:prod"]
IS NULL, IS NOT NULL status.lastSeenTime IS NOT NULL
AND (implicit, or +) see above

List arguments accept either brackets or parentheses — IN ["a","b"] and IN ("a","b") are the same.

There is one further restriction. When comparing a name or reference field against a value containing a - wildcard, only = is available; anything else is rejected.

Values

Type Form
String "berlin" — always double-quoted
Number 42, -1.5, 1e3
Boolean true, false
Null NULL
Map {"key":"value"}

Timestamps are strings in RFC 3339:

metadata.createTime>"2026-01-01T00:00:00Z"

Labels

Labels are a map, and the whole map is one field path. Match an entry with CONTAINS and a key:value string, not by indexing into it:

metadata.labels CONTAINS "site:berlin"

This is why labels are the practical way to segment a fleet: one indexed field supports the queries, the alert groupings, and the deployment targeting.

If a label key itself contains a dot, escape it with a backslash so the parser does not read it as a path separator.

Unions and negation

Because OR and NOT do not execute, express them client-side:

  • Union — run one query per branch and merge the results, de-duplicating on name. Often a single IN or CONTAINS ANY collapses the branches anyway, which is cheaper; check for that first.
  • Negation — invert the operator where you can. NOT (state="READY") becomes state!="READY"; NOT IN ["a","b"] usually has to become a client-side filter over a broader query.

Prefer collapsing to IN/CONTAINS ANY over multiple round trips whenever the branches differ only in a value.

Choosing which fields you get

By default a read returns a standard set of fields. Two controls change that, and they combine; they are not alternatives.

view picks a baseline:

View Returns
NAME Only the resource name.
BASIC A commonly useful subset. The usual default.
DETAIL More, including heavier fields.
FULL Everything.

fieldMask is additive on top of the view — a list of field paths to include as well. This trips people up: setting a field mask does not restrict you to those fields, it adds them to whatever the view already returns. For a minimal response, combine view=NAME with a field mask of exactly what you need:

curl -s -H "Authorization: Bearer $API_KEY" --get \
  --data-urlencode 'view=NAME' \
  --data-urlencode 'fieldMask=status.connectionStatus,metadata.labels' \
  "https://devices.apis.edgelq.com/v1/projects/$PROJECT/regions/$REGION/devices"

The saving is real. Device resources carry substantial hardware inventory and status; asking for two fields instead of all of them changes both response size and latency materially across a large fleet.

Ordering

orderBy takes field paths with an optional direction, asc (the default) or desc:

orderBy=metadata.createTime desc

Ordering must be stable for pagination to be correct, so prefer a field that is unique or append a tie-breaker.

Pagination

Set pageSize, then follow the tokens the server returns. Never assume the server honoured your page size — it may return fewer, and a short page does not mean the last page. The only reliable end-of-results signal is an empty nextPageToken.

TOKEN=""
while : ; do
  RESP=$(curl -s -H "Authorization: Bearer $API_KEY" --get \
    --data-urlencode "pageSize=100" \
    --data-urlencode "pageToken=$TOKEN" \
    "https://devices.apis.edgelq.com/v1/projects/$PROJECT/regions/$REGION/devices")

  echo "$RESP" | jq -r '.devices[]?.name'

  TOKEN=$(echo "$RESP" | jq -r '.nextPageToken // empty')
  [ -z "$TOKEN" ] && break
done

Responses also carry prevPageToken, so a cursor can be walked in either direction.

If you need to render “page 3 of 47”, set includePagingInfo=true and the response adds currentOffset and totalResultsCount. It costs the server extra work to count, so leave it off when you are just iterating.

Keep pageSize modest — a few hundred at most. Large pages increase the chance of a timeout on a slow query, and a retry then repeats the whole page.

Some resources also expose Search, which takes a free-text phrase in addition to the controls above:

GET /v1/projects/{project}/regions/{region}/devices:search?phrase=till

Use Search for a human typing into a box, and List with a filter for anything programmatic. Search trades exactness for recall, which is the wrong trade for automation.

Reading across regions

Substitute - for the region to query every region a project uses:

GET /v1/projects/{project}/regions/-/devices

BatchGet is the other cross-region read — it takes names instead of a parent, so one call can fetch resources from several regions at once.

Nested request fields over REST

The four controls above are flat, but some reads take structured request fields. Over gRPC you set them as nested messages; over REST, each leaf becomes a dotted query parameter. Reading time series is the case you are most likely to hit:

curl -s -H "Authorization: Bearer $API_KEY" --get \
  --data-urlencode 'filter=metric.type="devices.edgelq.com/device/cpu/usage"' \
  --data-urlencode 'interval.startTime=2026-09-01T00:00:00Z' \
  --data-urlencode 'interval.endTime=2026-09-02T00:00:00Z' \
  --data-urlencode 'aggregation.alignmentPeriod=60s' \
  --data-urlencode 'aggregation.perSeriesAligner=ALIGN_MEAN' \
  --data-urlencode 'aggregation.crossSeriesReducer=REDUCE_NONE' \
  "https://monitoring.apis.edgelq.com/v4/projects/$PROJECT/timeSeries"

Three things to note. The field names are the JSON ones, so alignment_period in the .proto is aggregation.alignmentPeriod here. interval.endTime is required while interval.startTime is optional and defaults to the end time, so omitting the start gives you an empty window, not everything. And durations are strings with a unit suffix, such as 60s.

For what the aligners and reducers actually compute, see the Monitoring API reference. For the metric types you can filter on, see the device metrics reference.

Trying it interactively

Every control here has a cuttle flag, which makes the CLI the fastest way to iterate on a filter before committing it to code:

cuttle devices list devices \
  --parent projects/$PROJECT/regions/- \
  --filter 'metadata.labels CONTAINS "site:berlin"' \
  --field-mask status.connectionStatus \
  --order-by 'metadata.createTime desc' \
  --page-size 50 \
  -o json

See Operating with the cuttle CLI for the CLI-side detail.

Next steps