Pagination
Endpoints that return time-series samples — such as Segment samples history — can return more data than fits in a single response. When that happens, the API splits the result into pages and returns a cursor you use to fetch the next one.
Instead of asking for "page 2", you send back the cursor the previous response gave you, and the API continues exactly where it left off.
Parameters
| Parameter | Where | Type | Description |
|---|---|---|---|
pageSize | query | number | Upper bound on the number of samples in each page, from 1 to 5000. Defaults to 1000. A page may contain fewer — read the actual array length rather than assuming pageSize. |
cursor | query | string | Marks where the next page begins. Copy it from the previous response — don't build it yourself. Omit it on the first request. |
A 200 response carries a cursor field when the API may have more data for your query. On the last page the field is left out of the response entirely — treat a missing key as "no more pages" rather than expecting an explicit null.
A cursor is not a guarantee that more data exists — a page can come back with a cursor even when nothing follows it, in which case the next request returns an empty data and no cursor. Keep looping until the cursor is gone, and treat an empty final page as a normal ending.
How to page through a result
- Make your first request with the query you want (
start,end,resolution, units, optionallypageSize). Leavecursorout. - Check for the
cursorfield in the response. If it's missing, you're done; if it's a string, request the next page. - Repeat the same request with
cursor=<value>from the previous response. - Keep going until a response comes back without a
cursor.
Onlycursorshould change between pages. Keepstart,end,resolution,pageSize, and unit parameters identical across the loop. Changing the time range mid-loop is not just inconsistent — a cursor that falls outside the newstart/endis rejected with400 The cursor is invalidon the mold, virus risk, occupancy, ventilation, and hourly radon endpoints, and is silently clamped to the new range on/samples.
If you omit start and end altogether, the query covers the whole measuring segment — from the segment's start to its end, or to now if the segment is still running — which is often more data than one page holds.
Example
# First request — no cursor
curl -X GET \
"https://ext-api.airthings.com/v1/segments/{segmentId}/samples?start=2024-01-01T00:00:00Z&end=2024-03-01T00:00:00Z&resolution=HOUR" \
-H "Authorization: Bearer <ACCESS_TOKEN>"The response carries a cursor, so there may be more data:
{
"start": "2024-01-01T00:00:00Z",
"end": "2024-03-01T00:00:00Z",
"measurementSystem": "METRIC",
"cursor": "MTcwNzY2MDAwMA==",
"data": {
"time": [1704067200, 1704070800, "..."],
"temp": [21.4, 21.6, "..."],
"co2": [612, 640, "..."]
}
}Send the same query again with the returned cursor appended, and repeat until a response arrives with no cursor field:
curl -X GET \
"https://ext-api.airthings.com/v1/segments/{segmentId}/samples?start=2024-01-01T00:00:00Z&end=2024-03-01T00:00:00Z&resolution=HOUR&cursor=MTcwNzY2MDAwMA==" \
-H "Authorization: Bearer <ACCESS_TOKEN>"Looping in code
import requests
BASE_URL = "https://ext-api.airthings.com/v1/segments/{segment_id}/samples"
params = {
"start": "2024-01-01T00:00:00Z",
"end": "2024-03-01T00:00:00Z",
"resolution": "HOUR",
}
headers = {"Authorization": f"Bearer {access_token}"}
pages = []
while True:
resp = requests.get(
BASE_URL.format(segment_id=segment_id),
params=params,
headers=headers,
)
resp.raise_for_status()
body = resp.json()
pages.append(body["data"])
cursor = body.get("cursor")
if not cursor: # field absent -> last page reached
break
params["cursor"] = cursorThe last iteration may append an empty page, since a cursor can be returned even when no data follows it. Filter empty pages out before merging if that matters to you.
Combining pages
Sample data is columnar: data holds parallel arrays (time, temp, co2, …) aligned by index. To merge pages into one continuous series, concatenate each array by key, in the order the pages were returned:
def merge(pages):
merged = {}
for page in pages:
for key, values in page.items():
merged.setdefault(key, []).extend(values)
return mergedThe keys differ per endpoint: /samples returns time as numbers (Unix seconds), while the mold, virus risk, occupancy, ventilation, and hourly radon endpoints return time as strings holding the same Unix seconds, alongside their own sensor keys. Concatenating by key works either way; normalise the timestamp type yourself if you mix endpoints.
Notes
pageSizeis an upper bound, not an exact page length. A page can be shorter — on the last page, and depending on theresolutionyou asked for. Drive your loop off thecursorand the array lengths you actually receive, never off an assumed row count.pageSizeandcursorwork the same way across all sample-history endpoints (segment and device samples, including the mold, virus risk, occupancy, ventilation, and hourly radon variants): same bounds, same default, same cursor loop. The response body shape differs as described above, and the exact number of samples per page can vary between them.pageSizeoutside1–5000is rejected with400.- Since
pageSizedefaults to1000, a large date range may be paginated even when you don't set it. Always check for a responsecursorrather than assuming one request returned everything.
Updated 2 days ago