data, links, and meta fields, page-based navigation via ?page=N, and HATEOAS-style links the API returns for navigating between pages. This page covers the envelope, the navigation pattern, the edge cases, and the practical patterns for reading paginated data.
The pagination envelope
Every list endpoint returns a JSON object with three top-level fields:
This is the Laravel API resource pagination shape, common across Laravel-based APIs.
The links object
The URLs in
links are complete — they include the full host, version segment, path, and query parameters. Use them as-is rather than constructing URLs by hand.
The meta object
Page size
Volunteer’s pagination differs from CRM+ and Raise in an important way: the page size is not partner-controlled.
The Volunteer spec documents
page as a query parameter on list endpoints but does not document per_page as a query parameter. Partner integrations cannot request larger or smaller pages.
This has practical implications:
- A customer with many records produces many pages. 120 users with
per_page: 15= 8 pages. 12,000 users = 800 pages. - Bulk reads make many requests. Compared to a 1000-per-page API, bulk reads against Volunteer make ~67× more requests for the same data.
- Rate limits matter more. With many requests required, rate-limit awareness becomes critical earlier. See Rate Limits.
The recommended pattern: follow links.next
The cleanest pagination pattern uses the API’s own links.next URL rather than constructing page URLs manually:
JavaScript
Combining filters with pagination
When the original request has filter parameters (likename_like=wayne), the links.next URL preserves them:
JavaScript
links.next URL. The integration doesn’t need to re-attach it manually.
Stopping early
For workflows that don’t need every record — finding a specific user, taking the first N matches, etc. — break out of the loop when you have what you need:JavaScript
Progress tracking for large reads
For long-running bulk reads, surface progress to the user or to logs:JavaScript
Edge cases
Empty result set
A query that matches no records returns an emptydata array with links.next === null:
links.next is null. The empty data array is correctly handled.
Note that meta.from and meta.to are null when there are no records on the page — handle this if you display the range to users:
JavaScript
Single page of results
When all records fit on one page,links.prev and links.next are both null:
Modification during iteration
If records are added or modified while you’re paging through results, the page boundaries may shift. A record that was on page 3 when you started might move to page 4 by the time you read it; a new record added at the start might cause your next page to repeat records you’ve already seen. For most analytics workloads, this is acceptable — the snapshot is “as of approximately when the read started.” For workloads that need strict consistency, two options:
The
created_before filter is the cleaner pattern for read-once snapshots.
Page navigation beyond last
If your code (incorrectly) requests ?page=999 against an endpoint with only 8 pages, the response is typically still a valid envelope:
data array is empty, next is null. The integration’s loop terminates correctly even if a page number is somehow out of range.
When to manually construct page URLs
For most workflows, followlinks.next. But two specific cases call for manual page URL construction:
Random-access page jumps
For UI workflows like “go to page 5” or “show the last page”:JavaScript
Resumable iterators
For workflows that may be interrupted (long backfills, crashable workers):JavaScript
Performance considerations
A few patterns that keep paginated reads efficient:Don’t re-fetch pages you’ve already processed
JavaScript
Filter aggressively
JavaScript
Process pages as they arrive
For very large reads, don’t accumulate everything in memory:JavaScript
Pagination across the three Virtuous APIs
Quick reference for partners building against multiple APIs:
Three different pagination styles across three APIs. The mental model for Volunteer is the most distinct — follow the API’s own navigation links rather than calculating page offsets.
A pagination checklist
When implementing pagination against the Volunteer API:- Use
links.nextto navigate, not manual page number construction (where possible) - Check
links.nextwith simple null check, not string comparison - Handle the empty-result-set case (
data: [], allmeta.from/meta.tonull) - Push filters into the request, not client-side after the fact
- Process pages as they arrive for large reads (don’t accumulate everything)
- For interrupted bulk reads, save page-number checkpoints
- For UI workflows needing random-access pages, construct URLs manually
- Be aware page size isn’t partner-controllable
- Pair pagination with rate-limit-aware throttling (see Rate Limits)
Where to go next
Rate Limits
The throttling patterns that pair with paginated reads.
The Volunteer Data Model
What resources are available to paginate through.
Error Handling
Error handling specific to multi-page reads.
API Performance Tips
The broader performance patterns including pagination-aware caching.