Pagination
List endpoints never return everything. Each returns one page wrapped in the same envelope, plus opaque cursors for moving through the rest.
Parameters
limit is clamped, not rejected. Values outside 1–100 are silently coerced into range, so limit=5000 returns 100 rather than an error. Do not rely on the number you sent being the number you get.
starting_after and ending_before are mutually exclusive. If you send both, starting_after wins.
Paging forward
Read next_cursor from the response and send it back as starting_after. Stop when has_more is false.
Asking for the total
Lists do not count their records unless you ask. Add include=total_count and the response carries total_count, the number of records that match your request across every page, not just this one. Leave it out and the field is not in the response at all.
The count is taken at the moment of the request. It is worked out fresh each time you ask, so if records are added or removed while you page, a later count can differ from the first. Ask for it on the first page, where it describes the list you are about to walk, rather than on every page.
include is not a filter. Asking for the total on one page and not the next never makes a cursor invalid. Any value other than total_count returns 400 validation_failed.
Send the same filters on every page
This is the part that surprises people. A cursor remembers the filters that produced it. Send q, donor_id, type and similar on the first request, then send them again, with the same values, alongside the cursor on every later page. A later page whose filters differ from the cursor’s, including one that leaves a filter out, returns 400 invalid_cursor rather than quietly paging through a different list.
Filters that take an ID, such as donor_id and award_cycle_id, take the record’s 9-character ID exactly as the API returned it. On the scholarship reporting endpoints award_cycle_id is required on every request, first page or not. A cursor taken while a filter held an older whole-number ID returns 400 invalid_cursor; start again from the first page.
Cursors are opaque and encrypted
A cursor is an encrypted token bound to your institution and to the filters that produced it. It is not a page number, an offset, or a record id, and it cannot be read: decoding one shows nothing about your data.
Asking for the same page twice returns a different cursor each time. Both work; never compare cursors to decide whether you have seen a page before.
Never build, edit, truncate, or store one for later reuse. A cursor that has been tampered with, has expired, belongs to another institution, or disagrees with the filters on the request returns 400 invalid_cursor.
If you need to resume work later, re-run the first request rather than persisting a cursor.