Skip to content

API Tokens & REST API

URL: /tokens

The LEAF Portal exposes a REST API that allows external scripts, notebooks, and third-party tools to query sensor data programmatically. Access is authenticated with personal API tokens.

API interface

  1. Navigate to API Tokens in the sidebar.
  2. Click Generate token.
  3. Enter a descriptive name (e.g. grafana, notebook).
  4. Optionally set an expiry date.
  5. Click Create and copy the token — it is only shown once.

Tokens can be revoked at any time by clicking the revoke button on the token row. Revoked tokens are immediately rejected by the API.

Pass the token in the Authorization header — this is the only supported way to authenticate; there is no ?token= query-parameter fallback:

Terminal window
curl -H "Authorization: Bearer <token>" https://your-portal/api/managements

Interactive API documentation (Swagger UI) is available at /api/docs.

GET /api/managements

Returns the list of managements (access grants) the token owner has access to. Returns a plain JSON array (this endpoint is always small).

[
{
"id": "...",
"organisation": "WUR",
"department": "SSB",
"entity": null,
"time_start": null,
"time_end": null
}
]
GET /api/entities

Returns the distinct entities accessible to the token, across all departments. Streamed as NDJSON (see Response format):

{"organisation": "WUR", "department": "SSB", "entity": "R1"}
GET /api/data/tags?organisation=WUR&department=SSB&key=experiment_id

Returns the distinct values of a JSONB tag key across matching sensor rows, without scanning full rows. Useful for discovering e.g. which experiment_ids exist for an entity before fetching data. Streamed as NDJSON:

{"entity": "R1", "tag_value": "exp-042"}
ParameterDescription
organisationOrganisation name (required)
departmentDepartment name (required)
keyJSONB tag key to extract, e.g. experiment_id (required)
entityFilter to one or more entities, comma-separated (optional)
from / toISO 8601 time window (optional)
GET /api/data/recent?limit=50

Returns the most recent sensor readings across all accessible departments. limit defaults to 20, max 100000. Streamed as NDJSON, same row shape as /api/data.

GET /api/data?organisation=WUR&department=SSB&metric=temperature&from=2024-01-01&limit=500
ParameterDescription
organisationOrganisation name (required)
departmentDepartment name (required)
entityFilter to one or more entities, comma-separated (optional)
metricFilter to one or more metrics, comma-separated (optional)
fromStart time, inclusive — ISO 8601 (optional)
toEnd time, exclusive — ISO 8601 (optional)
limitMax rows returned (default 1000, max 5,000,000)
bucketReturn pre-aggregated rows instead of raw ones — see Bucketed data (optional)

Pass bucket=<1min\|5min\|10min\|1hour\|1day> to read from a server-side pre-aggregated rollup instead of raw rows. This is the recommended way to fetch data over long time ranges or for plotting/downsampling — it avoids transferring and locally averaging large volumes of raw readings, and the aggregates are computed exactly (mean, min, max, stddev; median is a close statistical approximation), not just decimated.

This is powered by TimescaleDB continuous aggregates — see Bucketed continuous aggregates for the underlying schema and why the aggregates are cascaded the way they are.

GET /api/data?organisation=WUR&department=SSB&entity=R1&metric=temperature&bucket=1hour

Bucketed rows have a different shape than raw rows — one row per (entity, metric, bucket):

{
"time": "2024-06-01T10:00:00Z",
"entity": "R1",
"metric": "temperature",
"value": 22.4,
"median_value": 22.3,
"min_value": 21.8,
"max_value": 23.9,
"stddev_value": 0.41,
"sample_count": 240,
"tags": {"unit": "celsius"},
"alternative_tags": null
}
  • value is the exact mean of every raw reading in the bucket.
  • median_value is a close statistical approximation (not exact) of the middle value — useful alongside value for spotting when a bucket’s average was skewed by a spike or a faulty reading.
  • tags is the tags from one raw reading in the bucket. If every raw reading in the bucket had identical tags, this is the complete, exact picture and alternative_tags is null. If readings in the bucket had differing tags (for example, an instrument that embeds a per-reading sequence number), tags is one real sample and alternative_tags is a second real sample that differed — a signal that the underlying series may need finer-grained filtering (e.g. by a tag value) rather than being treated as one uniform entity/metric.
  • Raw-row-only fields (department_id, organisation_id) are not present on bucketed rows.

/api/entities, /api/data/tags, /api/data/recent, and /api/data all stream their response as NDJSON (newline-delimited JSON, Content-Type: application/x-ndjson) — one JSON object per line, rather than a single JSON array. This lets the server stream arbitrarily large result sets (millions of rows) without buffering the whole response in memory, and lets clients start processing rows as they arrive instead of waiting for the full response.

{"time": "2024-06-01T10:00:00Z", "entity": "R1", "metric": "temperature", "value": 22.4, "tags": {"unit": "celsius"}, "department_id": "...", "organisation_id": "..."}
{"time": "2024-06-01T10:01:00Z", "entity": "R1", "metric": "temperature", "value": 22.5, "tags": {"unit": "celsius"}, "department_id": "...", "organisation_id": "..."}

This means you cannot call response.json() on these endpoints — parse it line by line instead (see the Python notebook example). /api/managements is the one exception: it always returns a plain JSON array, since the result set is inherently small.

For a full interactive example using requests and pandas, see the API notebook.