Skip to main content

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"
}
FieldMeaning
itemsThe records on this page.
next_cursorOpaque 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 — first page
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 — next page
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)

FieldDefaultMaximum
limit (in the JSON body)1000check 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

Python — walk every page
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)
JavaScript — same idea
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_cursor back as cursor in the JSON body.
  • ✅ Cursors are opaque — never decode or modify.
  • next_cursor: null means no more results.
  • ✅ Prefer bigger limit values.
  • ✅ Cursors stay valid for ~24 hours; restart on 400.