API Notebook
This notebook shows how to query the LEAF Portal REST API using Python.
Prerequisites
Section titled “Prerequisites”pip install requests pandasAuthentication
Section titled “Authentication”All endpoints require a token passed as a Bearer header:
Authorization: Bearer <token>Tokens can be generated on the API Tokens page in the portal (/tokens).
Set your token as an environment variable before running this notebook:
export API_TOKEN=your_token_hereThe base URL for all endpoints is https://leaf-portal.containers.wur.nl.
import ioimport osimport requestsimport pandas as pd
BASE_URL = "https://leaf-portal.containers.wur.nl"
# Load API token from environment variable# Set it in your terminal: export API_TOKEN=your_token_hereTOKEN = os.getenv("API_TOKEN", "")
if not TOKEN: print("Warning: API_TOKEN is not set. Requests will return 401.")
HEADERS = {"Authorization": f"Bearer {TOKEN}"}GET /api/managements
Section titled “GET /api/managements”Lists all data managements accessible to your token. Each management represents a scoped slice of data (organisation → department → entity → time window) that your token has been granted access to.
Use the returned id values to query GET /api/data by management UUID (if supported), or use the organisation and department names as parameters.
response = requests.get(f"{BASE_URL}/api/managements", headers=HEADERS)
if response.status_code != 200: raise RuntimeError(f"Error {response.status_code}: {response.text}")
df = pd.DataFrame(response.json())df.head()| id | organisation | department | entity | time_start | time_end | |
|---|---|---|---|---|---|---|
| 0 | 37881edb-6925-48c8-b2eb-4e1c980201ef | WUR | SSB | None | None | None |
GET /api/data/recent
Section titled “GET /api/data/recent”Returns the most recent sensor readings across all departments accessible to your token.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 20 | Number of rows to return (max 100000) |
Streamed as NDJSON (one JSON object per line) rather than a JSON array — see the parsing note below.
response = requests.get(f"{BASE_URL}/api/data/recent", headers=HEADERS, params={"limit": 20})
if response.status_code != 200: raise RuntimeError(f"Error {response.status_code}: {response.text}")
# The response is NDJSON (one JSON object per line), not a JSON array, so# response.json() will not work here. pandas reads this format natively.if not response.text.strip(): print("No data returned.") df = pd.DataFrame()else: df = pd.read_json(io.StringIO(response.text), lines=True) df["time"] = pd.to_datetime(df["time"])df.head()| time | entity | metric | value | tags | department_id | organisation_id | |
|---|---|---|---|---|---|---|---|
| 0 | 2026-07-01 23:59:59+00:00 | D0167289 | Stirrer.process-value | 299.8000 | {'unit': 'rpm', 'topic': 'ssb/BCS/D0167289/Sar... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 1 | 2026-07-01 23:59:59+00:00 | D0167289 | Gas_flow.process-value | 1.4979 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 2 | 2026-07-01 23:59:59+00:00 | D0167289 | pO2.process-value | 48.3600 | {'unit': '%sat', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 3 | 2026-07-01 23:59:59+00:00 | D0167289 | pH.process-value | 6.9790 | {'unit': 'pH', 'topic': 'ssb/BCS/D0167289/Sart... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 4 | 2026-07-01 23:59:58+00:00 | D0167289 | pH.process-value | 6.4110 | {'unit': 'pH', 'topic': 'ssb/BCS/D0167289/Sart... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
GET /api/data
Section titled “GET /api/data”Returns sensor data for a specific organisation and department, with optional filters.
| Parameter | Type | Required | Description |
|---|---|---|---|
organisation | str | yes | Organisation name (e.g. WUR) |
department | str | yes | Department name (e.g. SSB) |
entity | str | no | Filter to one or more entities, comma-separated |
metric | str | no | Filter to one or more metrics, comma-separated |
from | str | no | Start time, ISO 8601 (inclusive) |
to | str | no | End time, ISO 8601 (exclusive) |
limit | int | no | Max rows to return (default 1000, max 5,000,000) |
bucket | str | no | 1min, 5min, 10min, 1hour, or 1day — return pre-aggregated rows instead of raw ones (see next section) |
Like /api/data/recent, this is streamed as NDJSON.
response = requests.get( f"{BASE_URL}/api/data", headers=HEADERS, params={ "organisation": "WUR", "department": "SSB", "entity": "D0167289", "metric": "Gas_flow.process-value", "from": "2026-07-01T00:00:00Z", "to": "2026-07-01T05:00:00Z", "limit": 1000, },)
if response.status_code != 200: raise RuntimeError(f"Error {response.status_code}: {response.text}")
if not response.text.strip(): print("No data returned.") df = pd.DataFrame()else: df = pd.read_json(io.StringIO(response.text), lines=True) df["time"] = pd.to_datetime(df["time"])df.head()| time | entity | metric | value | tags | department_id | organisation_id | |
|---|---|---|---|---|---|---|---|
| 0 | 2026-07-01 04:59:58+00:00 | D0167289 | Gas_flow.process-value | 0.99750 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 1 | 2026-07-01 04:59:57+00:00 | D0167289 | Gas_flow.process-value | 0.99735 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 2 | 2026-07-01 04:59:51+00:00 | D0167289 | Gas_flow.process-value | 0.00000 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 3 | 2026-07-01 04:59:49+00:00 | D0167289 | Gas_flow.process-value | 1.49835 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
| 4 | 2026-07-01 04:59:48+00:00 | D0167289 | Gas_flow.process-value | 1.49775 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | 74c70fdf-12d7-4982-bb2c-2e0d46fa31f0 | c9ebd03f-c9c5-412a-99c9-5b05b3fe24aa |
GET /api/data with bucket=
Section titled “GET /api/data with bucket=”For long time ranges, pass bucket= to get server-side pre-aggregated rows
instead of fetching and locally downsampling raw data. Each bucketed row
covers one (entity, metric, time bucket) and includes value (the exact
mean), median_value (an approximation, useful for spotting spikes that
skew the mean), min_value, max_value, stddev_value, sample_count,
and tags (see the docs page for what alternative_tags means when a
bucket mixes readings with differing tags).
response = requests.get( f"{BASE_URL}/api/data", headers=HEADERS, params={ "organisation": "WUR", "department": "SSB", "entity": "D0167289", "metric": "Gas_flow.process-value", "from": "2026-07-01T00:00:00Z", "to": "2026-07-02T00:00:00Z", "bucket": "1hour", },)
if response.status_code != 200: raise RuntimeError(f"Error {response.status_code}: {response.text}")
if not response.text.strip(): print("No data returned.") df = pd.DataFrame()else: df = pd.read_json(io.StringIO(response.text), lines=True) df["time"] = pd.to_datetime(df["time"])df.head()| time | entity | metric | value | median_value | min_value | max_value | stddev_value | sample_count | tags | alternative_tags | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2026-07-01 23:00:00+00:00 | D0167289 | Gas_flow.process-value | 0.951935 | 0.997004 | 0 | 1.49895 | 0.624141 | 3301 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... |
| 1 | 2026-07-01 22:00:00+00:00 | D0167289 | Gas_flow.process-value | 1.218351 | 1.496306 | 0 | 1.50000 | 0.516277 | 3304 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... |
| 2 | 2026-07-01 21:00:00+00:00 | D0167289 | Gas_flow.process-value | 1.013776 | 1.496306 | 0 | 1.49910 | 0.643779 | 3306 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... |
| 3 | 2026-07-01 20:00:00+00:00 | D0167289 | Gas_flow.process-value | 1.069052 | 1.496306 | 0 | 1.50000 | 0.599781 | 3309 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... |
| 4 | 2026-07-01 19:00:00+00:00 | D0167289 | Gas_flow.process-value | 1.059117 | 1.496306 | 0 | 1.50000 | 0.608999 | 3310 | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... | {'unit': 'slpm', 'topic': 'ssb/BCS/D0167289/Sa... |
Download the raw notebook: api-notebook.ipynb