Documentation
On this page

Timeseries

Write points to the series of a timeseries type, read them raw, in buckets, or downsampled, get each series' newest point, list series, and read the ingest log.

Timeseries explains series, fields, retention, and who may read and write them. Timeseries types are created and changed with the ontology schema endpoints and addressed here by their api_name. These endpoints answer 400 for a type that is not a timeseries type, 404 for one you cannot read, and 403 without view_objects (or, to write, edit_objects). Series about objects follow their objects’ access; see How access works. Times are RFC 3339; responses give them to the microsecond.

Selecting series

Reads select series by any of:

Field
seriesa list of series ids
objectthe id of the series’ object
tagstag values the series must have, such as {"sensor": "spindle"}; other tags may have any value

Leaving all of them out selects every series you may read. A read covers at most 100 series: naming more ids is 400, and so is a selection that matches more (“name the series, an object, or tags”). An unknown tag is 422 at tags.

Writing

POST /v1/ontology/timeseries/{type}/points

AuthfullPermissionview_objects + edit_objects, and edit access to the timeseries type or a write_attached listing on each object

JSON
{
  "series": [
    { "object": "o4b8j2e6c0t1q5w9r3y7u2i6", "tags": { "sensor": "spindle" },
      "points": [
        { "at": "2026-10-26T09:00:00Z", "values": { "temperature": 41.5, "rpm": 1200, "running": true } },
        { "at": "2026-10-26T09:00:10Z", "values": { "temperature": 41.7, "rpm": 1210, "running": true } }
      ] }
  ],
  "idempotency_key": "saw-2026-10-26T09:00"
}

object is required when the type has an object_type and refused when it has none; tags may name only the type’s tags. values maps fields to values. idempotency_key is optional, 1-200 characters.

201:

JSON
{
  "batch_id": "b3a7t9c1h5i2d8x4q6w0e2r4",
  "type": "telemetry",
  "series": [
    { "id": "s1e3r5i7e9s0i2d4x6c8v0b2", "object": "o4b8j2e6c0t1q5w9r3y7u2i6",
      "tags": { "sensor": "spindle" }, "points": 2 }
  ],
  "points": 2,
  "inserted": 2,
  "replaced": 0,
  "unchanged": 0,
  "replayed": false
}

A point at a time its series already has replaces the stored one (replaced), or changes nothing if its values are the same (unchanged).

StatuscodeWhen
201written
200a retry with the same idempotency_key and request: the first result, with replayed: true; nothing is written
400bad_requestmore than 1,000 series or 10,000 points, or a malformed body
403forbiddenno write access to the type
409idempotency_conflictthe key was used for a different request
422validation_failedone detail per problem, with paths such as series[0].points[3].at or series[0].points[3].values.temperature: a point twice in the batch, a point with no values, a value that fails its field, a time before the retention cutoff or more than a day ahead, an object you cannot see, an unknown tag
413a body over 8 MiB
429rate_limitedtoo many batches; see Rate limits

The whole batch is refused if any point is. With live on, the batch emits one series.points event per series it changed.

Reading

POST /v1/ontology/timeseries/{type}/query

AuthfullPermissionview_objects + a view of the series

Field
series, object, tagsthe selection
fieldsthe fields to return (default all); an unknown one is 400
from, tothe time range [from, to); by default from the retention cutoff to a day ahead
bucketaggregate per bucket: seconds, or "30s", "5m", "1h", "1d" (1 second to 366 days), aligned to the Unix epoch
aggregateswith bucket or max_points: any of avg, min, max, sum (numbers), count, first, last (any field)
max_points2-10,000: downsample [from, to) to at most that many buckets per series; needs from and to
limitraw reads: 1-10,000, default 1,000
cursorraw reads: next_cursor of the previous page
groupwith bucket or max_points: totals across series
objects_where, per_serieswith group: see totals across series

bucket and max_points exclude each other. Without either, the read returns raw points, by series and time:

JSON
{
  "series": [
    { "id": "s1e3r5i7e9s0i2d4x6c8v0b2", "object": "o4b8j2e6c0t1q5w9r3y7u2i6",
      "tags": { "sensor": "spindle" }, "created_at": "2026-10-26T09:00:01.482113Z" }
  ],
  "points": [
    { "series": "s1e3r5i7e9s0i2d4x6c8v0b2", "at": "2026-10-26T09:00:00.000000Z",
      "values": { "temperature": 41.5, "rpm": 1200, "running": true } }
  ],
  "next_cursor": null
}

With bucket or max_points, it returns buckets, one per series and bucket that has points:

JSON
{
  "series": [ { "id": "s1e3r5i7e9s0i2d4x6c8v0b2", "...": "..." } ],
  "bucket_seconds": 60,
  "buckets": [
    { "series": "s1e3r5i7e9s0i2d4x6c8v0b2",
      "start": "2026-10-26T09:00:00.000000Z", "end": "2026-10-26T09:01:00.000000Z", "points": 6,
      "values": {
        "temperature": { "avg": 42.1, "min": 41.5, "max": 42.9 },
        "running": { "last": true } } }
  ]
}

Without aggregates, numbers get avg, min, and max, and other fields last; with them, each field gets the ones that apply to it (400 if none applies to any). A read over more than 10,000 buckets in all, counting every series, is 400: use wider buckets, a shorter range, or fewer series.

Totals across series

With group, the read combines many series into one value per group and bucket, such as the total power drawn at each site per hour:

JSON
{ "fields": ["watts"], "from": "2026-10-26T00:00:00Z", "to": "2026-10-27T00:00:00Z",
  "bucket": "1h", "group": { "by": "property", "property": "site" },
  "objects_where": { "op": "eq", "property": "kind", "value": "meter" } }

It works in two steps. First each series becomes one value per field and bucket (per_series), then a group’s series are combined (aggregates). Total power is the sum of each meter’s average in the hour, not the sum of its raw points, so a meter that reports every second counts as much as one that reports every minute.

Field
group{"by": "total"} (one group), {"by": "tags", "tags": [...]} (one or more distinct tags), {"by": "object"}, or {"by": "property", "property": "<name>"}: a top-level scalar property of the series’ objects, such as a string, number, date, or reference
objects_wherea query filter on the series’ objects: only their series count
per_serieseach series’ value per bucket: avg, min, max, sum (numbers), count, first, last; default avg for numbers, last for other fields
aggregateshow a group combines its series’ values: sum, avg, min, max (numbers), count; default all five for numbers, count for other fields

To count points, use "per_series": "count" with "aggregates": ["sum"]. The selection (series, object, tags) still applies, and from, to, bucket and max_points work as above. Grouping by a property and objects_where need a type whose series belong to objects (400 otherwise).

JSON
{
  "bucket_seconds": 3600,
  "groups": [
    { "key": { "site": "HQ" }, "series": 412,
      "buckets": [
        { "start": "2026-10-26T09:00:00.000000Z", "end": "2026-10-26T10:00:00.000000Z", "series": 400,
          "values": { "watts": { "sum": 1234.5, "avg": 3.0, "min": 0.4, "max": 9.8, "count": 400 } } } ] }
  ],
  "truncated": false
}

A group’s key holds the tag values, {"object": "<id>"}, the property’s value, or nothing for total. Its series counts the series in the group, and a bucket’s series those with a point in that bucket. Groups are ordered by key.

Totals cover only the series you may read: a series of an object hidden from you counts nowhere, not even in series. A restricted property hidden from you is unknown to group and objects_where (400), as in queries.

There is no 100-series limit, but a read returns at most 1,000 groups and 10,000 groups × buckets. Past that, whole groups are left out and truncated is true. Naming more than 100 series by id is still 400. Totals read every point they cover, so a long range of busy series takes longer.

POST /v1/ontology/timeseries/{type}/latest

AuthfullPermissionview_objects + a view of the series

JSON
{ "object": "o4b8j2e6c0t1q5w9r3y7u2i6", "fields": ["temperature"] }

Takes the selection and fields, nothing else. Each selected series with its newest point, or null if it has none left:

JSON
{
  "series": [
    { "id": "s1e3r5i7e9s0i2d4x6c8v0b2", "object": "o4b8j2e6c0t1q5w9r3y7u2i6",
      "tags": { "sensor": "spindle" }, "created_at": "2026-10-26T09:00:01.482113Z",
      "latest": { "at": "2026-10-26T09:05:50.000000Z", "values": { "temperature": 43.2 } } }
  ]
}

GET /v1/ontology/timeseries/{type}/series

AuthfullPermissionview_objects + a view of the series

The series you may read, by id, a page at a time.

Query
objectonly the series of this object
tag.<name>only series with this tag value, such as tag.sensor=spindle
limit1-500, default 100
cursornext_cursor of the previous page
JSON
{ "data": [ { "id": "s1e3r5i7e9s0i2d4x6c8v0b2", "object": "o4b8j2e6c0t1q5w9r3y7u2i6",
              "tags": { "sensor": "spindle" }, "created_at": "2026-10-26T09:00:01.482113Z" } ],
  "next_cursor": null }

The ingest log

GET /v1/ontology/timeseries/{type}/batches

AuthfullPermissionmanage_ontology and a view of the whole type

Every batch written to the type, newest first.

Query
beforeonly batches written before this time: pass the last created_at of the previous page
limit1-500, default 100
JSON
{
  "data": [
    { "id": "b3a7t9c1h5i2d8x4q6w0e2r4",
      "written_by": { "id": "a9g3e7n1t5b2o8t4q6w0e2r4", "kind": "agent", "method": "api_token", "via": "rest" },
      "series": 1, "points": 2, "inserted": 2, "replaced": 0,
      "first_at": "2026-10-26T09:00:00.000000Z", "last_at": "2026-10-26T09:00:10.000000Z",
      "created_at": "2026-10-26T09:00:01.482113Z" }
  ]
}

method is session, oauth, external_jwt, or api_token; via is rest or mcp. Replays are not batches and are not listed.

Esc
↑ ↓ navigate↵ open