Pagination
Cursor-based, by created_at, descending.
Every list endpoint in the Pivotal API uses cursor pagination. The pattern is the same shape across customers, contacts, and onboardings:
Response:
Pass response.next_cursor straight back as the next request’s cursor. Stop when has_more is false.
Parameters
cursor must validate as an ISO 8601 datetime. Garbage in returns a 400:
Walking a list end to end
Python:
Filters and pagination interact
Filters like ?status=active apply on the server, then pagination cuts the filtered result. The cursor still walks created_at descending — you don’t get drift from cursoring through a moving filter as long as the filter is stable for the duration of the walk.
If you mutate records mid-walk (e.g. flipping status from onboarding to active while paginating ?status=onboarding), expect to either skip or duplicate the affected rows depending on the timing. Snapshot in your client if that’s a problem.
What we deliberately don’t do
- No total count. Counting at scale gets expensive. If you need a UI element that says “245 customers”, fetch the count separately (a future
/customers/countendpoint will land — for now, walk the list once and cache). - No offset/limit. Offsets get slower as the offset grows and behave badly with concurrent inserts. Cursors are stable and constant-cost.
- No
beforecursor. Lists return newest first; if you need older-first, reverse the page in your client.
Edge cases
- An empty page comes back as
{ object: "list", data: [], has_more: false, next_cursor: null }. - The last page has
has_more: falseeven if it returnedlimititems — trust the flag, not the count. - Soft-deleted rows never appear in lists. They’re still gettable by id if you know it (
GET /customers/{id}will 404 — soft-deleted is treated as gone). If you need to read deletes, surface them through the customer’s timeline.