Pagination
Every endpoint that returns a list is cursor-paginated.
{ "data": [ { "id": "rec_…" }, { "id": "rec_…" } ], "pagination": { "next_cursor": "eyJvIjoxMDAsInMiOiJ…", "has_more": true }}The loop
Section titled “The loop”Follow next_cursor until has_more is false:
cursor, out = None, []while True: r = requests.get( "https://api.ahoy.ai/rest/v1/objects/ahoy_contact/records/", headers={"Authorization": f"Bearer {key}"}, params={"cursor": cursor} if cursor else {}, timeout=30, ) r.raise_for_status() body = r.json() out.extend(body["data"]) if not body["pagination"]["has_more"]: break cursor = body["pagination"]["next_cursor"]Cursors are opaque. Pass back exactly the string you were given. Don’t
decode, construct, or modify one — a cursor the server doesn’t recognise returns
request_invalid with
reason: "cursor_invalid".
Trust has_more, not the page size. A page may contain fewer items than you
asked for and still not be the last one. has_more is the only reliable signal.
Cursors encode their query. Don’t change the filter, sort, or properties
selection midway through a pagination run — start again with a fresh request.
Selecting columns
Section titled “Selecting columns”?properties= limits which properties come back, which meaningfully reduces
payload size on wide object types:
GET /rest/v1/objects/ahoy_contact/records/?properties=ahoy_email,ahoy_first_name,ahoy_last_nameAggregations paginate differently
Section titled “Aggregations paginate differently”POST …/records/aggregate/ pages over groups with limit and offset
rather than a cursor, and reports has_more. It is the one exception to the
cursor rule.