Skip to content

API Notebook

This notebook shows how to query the LEAF Portal REST API using Python.

Terminal window
pip install requests pandas

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:

Terminal window
export API_TOKEN=your_token_here

The base URL for all endpoints is https://leaf-portal.containers.wur.nl.

import io
import os
import requests
import 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_here
TOKEN = os.getenv("API_TOKEN", "")
if not TOKEN:
print("Warning: API_TOKEN is not set. Requests will return 401.")
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

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

Returns the most recent sensor readings across all departments accessible to your token.

ParameterTypeDefaultDescription
limitint20Number 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

Returns sensor data for a specific organisation and department, with optional filters.

ParameterTypeRequiredDescription
organisationstryesOrganisation name (e.g. WUR)
departmentstryesDepartment name (e.g. SSB)
entitystrnoFilter to one or more entities, comma-separated
metricstrnoFilter to one or more metrics, comma-separated
fromstrnoStart time, ISO 8601 (inclusive)
tostrnoEnd time, ISO 8601 (exclusive)
limitintnoMax rows to return (default 1000, max 5,000,000)
bucketstrno1min, 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

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