Pagination
Endpoints that return collections use cursor-based pagination. You ask for a page of results; if more exist, the response carries an opaque cursor you pass back to fetch the next page.
Why cursors (and not page numbers)?
Page numbers break when underlying data shifts (a new record inserted between requests would shuffle what's on each page). Cursors anchor to a stable position in the result set so you never miss or duplicate records — even when data is being written in parallel.
Response shape
{
"items": [
{ "...": "..." },
{ "...": "..." }
],
"next_cursor": "eyJ0IjoiMjAyNi0wMS0xNVQxMDoxNTowMCIsImlkIjo5OTk5fQ"
}
| Field | Meaning |
|---|---|
items | The records on this page. |
next_cursor | Opaque string. Pass back as cursor in the JSON request body for the next page. null when there are no more results. |
The cursor value is opaque — don't try to decode or modify it. Its format is internal and may change.
Consuming a paginated endpoint
The traffic data search endpoint is POST (not GET) — the time range, filters, page size and cursor all travel in the JSON body:
curl -X POST \
-H "Authorization: Token ..." \
-H "Content-Type: application/json" \
-d '{
"time": {"from_dt": "2026-01-01T00:00:00Z", "to_dt": "2026-01-31T00:00:00Z"},
"limit": 1000
}' \
https://mytrafficdata.com/api/v1/traffic/data/search
The response includes next_cursor if there's more. Pass it back in the body:
curl -X POST \
-H "Authorization: Token ..." \
-H "Content-Type: application/json" \
-d '{
"time": {"from_dt": "2026-01-01T00:00:00Z", "to_dt": "2026-01-31T00:00:00Z"},
"limit": 1000,
"cursor": "eyJ0Ij…"
}' \
https://mytrafficdata.com/api/v1/traffic/data/search
Keep passing the latest next_cursor back until you receive next_cursor: null. That's the end.
:::warning 30-day time-range cap
On /api/v1/traffic/data/search, the time window is capped at 30 days per query. For longer ranges, use the export endpoint (POST /api/v1/traffic/exports) which produces an async file you can download.
:::
Page size (limit)
| Field | Default | Maximum |
|---|---|---|
limit (in the JSON body) | 1000 | check the per-endpoint reference |
Prefer bigger pages. One request of 1,000 records is roughly ten times cheaper than ten requests of 100 — fewer network round-trips, less rate-limit pressure, less server work per record.
Full walk-through pattern
import requests
TOKEN = "..."
URL = "https://mytrafficdata.com/api/v1/traffic/data/search"
def walk(initial_body):
body = dict(initial_body)
while True:
r = requests.post(
URL,
headers={"Authorization": f"Token {TOKEN}"},
json=body,
timeout=(5, 30),
)
r.raise_for_status()
page = r.json()
yield from page["items"]
if not page.get("next_cursor"):
break
body["cursor"] = page["next_cursor"]
for record in walk({
"time": {"from_dt": "2026-01-01T00:00:00Z", "to_dt": "2026-01-31T00:00:00Z"},
"limit": 1000,
}):
process(record)
async function* walk(url, initialBody) {
let body = {...initialBody};
while (true) {
const r = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Token ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(await r.text());
const page = await r.json();
yield* page.items;
if (!page.next_cursor) break;
body.cursor = page.next_cursor;
}
}
for await (const record of walk(URL, {
time: {from_dt: "2026-01-01T00:00:00Z", to_dt: "2026-01-31T00:00:00Z"},
limit: 1000,
})) {
process(record);
}
Stable ordering
Records within items are returned in a stable, deterministic order (typically (timestamp, id)). The cursor encodes that position, so requesting the next page is always exactly "the next records after the last one I saw" — even if new records were inserted in the meantime.
Cursor lifetime
Cursors are valid for at least 24 hours. If you pause a long pagination job (e.g., overnight) and resume from a stored cursor the next day, that's fine.
If you receive a 400 Validation Error mentioning the cursor, it's expired or malformed — start over from the first page (omit cursor from the body).
Summary checklist
- ✅ Cursor-based — pass
next_cursorback ascursorin the JSON body. - ✅ Cursors are opaque — never decode or modify.
- ✅
next_cursor: nullmeans no more results. - ✅ Prefer bigger
limitvalues. - ✅ Cursors stay valid for ~24 hours; restart on 400.