Skip to main content
https://.thoughtindustries.com

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

ParameterTypeRequiredDefaultDescription
cursorstringNoOpaque cursor returned by the previous response. Omit for the first page.
perPageintegerNo50Number of records per page.

pageInfo fields

FieldTypeDescription
perPageintegerRecords returned in this page.
totalintegerTotal records matching the query.
hasMorebooleanWhether additional pages are available.
cursorstringOpaque 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 groups
function 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=50 or higher for bulk exports to minimize round-trips.
  • Treat cursor as opaque. Do not decode, mutate, or persist it beyond the immediately following request.
  • Stop iterating as soon as hasMore is false; the next cursor field will be absent.
  • Use total for progress indicators, but do not rely on it to detect the last page — hasMore is authoritative.