Pagination
List endpoints return paginated results to keep response sizes manageable. The REST API uses cursor-based pagination: pass the opaque cursor value from the previous response to fetch the next page.
Resource endpoints
Resource list endpoints (Users, Courses, Course Groups, Clients, and so on) return an unwrapped response whose top level contains a pageInfo object plus an array named after the resource.
Response: 200 List response
{
"courseGroups": [
{ "id": "abc123", "title": "Onboarding", "slug": "onboarding" },
{ "id": "def456", "title": "Compliance", "slug": "compliance" }
],
"pageInfo": {
"perPage": 50,
"total": 142,
"hasMore": true,
"cursor": "eyJwYWdlIjoyfQ"
}
}Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
cursor | string | No | — | Opaque cursor returned by the previous response. Omit for the first page. |
perPage | integer | No | 50 | Number of records per page. |
pageInfo fields
| Field | Type | Description |
|---|---|---|
perPage | integer | Records returned in this page. |
total | integer | Total records matching the query. |
hasMore | boolean | Whether additional pages are available. |
cursor | string | Opaque cursor to pass on the next request. Present only when hasMore is true. |
Events endpoints
Event and notification endpoints (GET /v2/events/:kind) use a lighter pagination model. Responses include a pageInfo object with only cursor and hasMore, plus an events array.
Response: 200 Events response
{
"pageInfo": {
"cursor": "MTUxODM2NTg1Mzk5OQ",
"hasMore": true
},
"events": [ /* ... */ ]
}Events endpoints accept cursor as the only pagination query parameter; perPage is not supported.
Iterating through pages
Pass the cursor from each response until hasMore is false.
# First page
curl -s -H "Authorization: Bearer $TI_API_KEY" \
"https://api.thoughtindustries.com/incoming/v2/courseGroups?perPage=50"
# Subsequent pages
curl -s -H "Authorization: Bearer $TI_API_KEY" \
"https://api.thoughtindustries.com/incoming/v2/courseGroups?perPage=50&cursor=eyJwYWdlIjoyfQ"async function fetchAllCourseGroups(apiKey) {
const groups = [];
let cursor = null;
do {
const url = new URL("https://api.thoughtindustries.com/incoming/v2/courseGroups");
url.searchParams.set("perPage", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { "Authorization": `Bearer ${apiKey}` }
});
const body = await response.json();
groups.push(...body.courseGroups);
cursor = body.pageInfo.hasMore ? body.pageInfo.cursor : null;
} while (cursor);
return groups;
}import requests
def fetch_all_course_groups(api_key):
groups = []
cursor = None
base_url = "https://api.thoughtindustries.com/incoming/v2/courseGroups"
while True:
params = {"perPage": 50}
if cursor:
params["cursor"] = cursor
response = requests.get(
base_url,
headers={"Authorization": f"Bearer {api_key}"},
params=params
)
body = response.json()
groups.extend(body["courseGroups"])
if not body["pageInfo"]["hasMore"]:
break
cursor = body["pageInfo"]["cursor"]
return groupsfunction fetchAllCourseGroups(string $apiKey): array {
$groups = [];
$cursor = null;
$baseUrl = "https://api.thoughtindustries.com/incoming/v2/courseGroups";
do {
$params = ["perPage" => 50];
if ($cursor) $params["cursor"] = $cursor;
$url = $baseUrl . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $apiKey"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
$groups = array_merge($groups, $body["courseGroups"]);
$cursor = ($body["pageInfo"]["hasMore"] ?? false) ? $body["pageInfo"]["cursor"] : null;
} while ($cursor);
return $groups;
}Best practices
- Prefer
perPage=50or higher for bulk exports to minimize round-trips. - Treat
cursoras opaque. Do not decode, mutate, or persist it beyond the immediately following request. - Stop iterating as soon as
hasMoreisfalse; the nextcursorfield will be absent. - Use
totalfor progress indicators, but do not rely on it to detect the last page —hasMoreis authoritative.
Related
- Rate limits — request quotas apply per page request
- Authentication — include credentials on every page request