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 Tokens
Section titled “API Tokens”Creating a token
Section titled “Creating a token”- Navigate to API Tokens in the sidebar.
- Click Generate token.
- Enter a descriptive name (e.g.
grafana,notebook). - Optionally set an expiry date.
- Click Create and copy the token — it is only shown once.
Managing tokens
Section titled “Managing tokens”Tokens can be revoked at any time by clicking the revoke button on the token row. Revoked tokens are immediately rejected by the API.
Using the API
Section titled “Using the API”Pass the token in the Authorization header — this is the only supported way to authenticate; there is no ?token= query-parameter fallback:
curl -H "Authorization: Bearer <token>" https://your-portal/api/managementsInteractive API documentation (Swagger UI) is available at /api/docs.
Endpoints
Section titled “Endpoints”List access grants
Section titled “List access grants”GET /api/managementsReturns 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 }]List accessible entities
Section titled “List accessible entities”GET /api/entitiesReturns the distinct entities accessible to the token, across all departments. Streamed as NDJSON (see Response format):
{"organisation": "WUR", "department": "SSB", "entity": "R1"}Distinct tag values
Section titled “Distinct tag values”GET /api/data/tags?organisation=WUR&department=SSB&key=experiment_idReturns 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"}| Parameter | Description |
|---|---|
organisation | Organisation name (required) |
department | Department name (required) |
key | JSONB tag key to extract, e.g. experiment_id (required) |
entity | Filter to one or more entities, comma-separated (optional) |
from / to | ISO 8601 time window (optional) |
Recent data
Section titled “Recent data”GET /api/data/recent?limit=50Returns the most recent sensor readings across all accessible departments. limit defaults to 20, max 100000. Streamed as NDJSON, same row shape as /api/data.
Data by scope
Section titled “Data by scope”GET /api/data?organisation=WUR&department=SSB&metric=temperature&from=2024-01-01&limit=500Query parameters
Section titled “Query parameters”| Parameter | Description |
|---|---|
organisation | Organisation name (required) |
department | Department name (required) |
entity | Filter to one or more entities, comma-separated (optional) |
metric | Filter to one or more metrics, comma-separated (optional) |
from | Start time, inclusive — ISO 8601 (optional) |
to | End time, exclusive — ISO 8601 (optional) |
limit | Max rows returned (default 1000, max 5,000,000) |
bucket | Return pre-aggregated rows instead of raw ones — see Bucketed data (optional) |
Bucketed data
Section titled “Bucketed data”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=1hourBucketed 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}valueis the exact mean of every raw reading in the bucket.median_valueis a close statistical approximation (not exact) of the middle value — useful alongsidevaluefor spotting when a bucket’s average was skewed by a spike or a faulty reading.tagsis 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 andalternative_tagsisnull. If readings in the bucket had differing tags (for example, an instrument that embeds a per-reading sequence number),tagsis one real sample andalternative_tagsis 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.
Response format
Section titled “Response format”/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.
Example: Python notebook
Section titled “Example: Python notebook”For a full interactive example using requests and pandas, see the API notebook.