Every paginated collection in the SDK is exposed three ways. They hit the same endpoint and differ only in how much they buffer.
| Method | Returns | Requests | Use when |
|---|---|---|---|
list | Page[T] | Exactly one | You are rendering a page, or you only need a count |
list_all | list[T] | As many as it takes | You need the whole set in memory anyway |
iterate | Iterator[T], or AsyncIterator[T] on AsyncEnvless | As many as it takes, lazily | The set is large, or you can stop early |
from envless import envless
page = envless.products.list(limit=50)
print(page['items'])
print(page['pagination']['limit'], page['pagination']['offset'])
print(page['pagination']['totalCount'], page['pagination']['hasMore'])from envless import envless
everything = envless.products.list_all()from envless import envless
for product in envless.products.iterate():
if product['slug'] == 'billing':
breakiterate is the only one that can stop early. Breaking out of the loop stops making requests, so scanning for one item in a large workspace costs one page, not all of them. On AsyncEnvless it is the same loop written async for.
Page is a TypedDict generic over the item type, so page['items'] is a list[ProductResource] to your type checker.
Options
List methods take these as keyword arguments, after their positional slugs. Each list accepts only the options its route reads, so an option a route ignores is a TypeError:
| Option | Type | Notes |
|---|---|---|
limit | int | 1 to 100. Defaults to 25 on the API. iterate and list_all default to 100 to minimise round trips, and clamp anything higher. |
offset | int | Rows to skip. iterate and list_all use it as a starting point and then advance it themselves. |
search | str | Products, product projects, projects, environments, invites, members, roles, keys and webhooks. Each method page says what it matches. |
order | str | name, asc or desc, on products, product projects, projects, environments, keys and webhooks. |
sort_by | str | Roles only: name (the default), updated or created. |
role_id | str | Members only: keeps the members holding that role. |
status | str | Webhook deliveries only: pending, processing, succeeded or failed. |
product | str | Projects, environments, variables, variable history and versions: disambiguates a project slug that exists in more than one product. |
timeout | float | Bounds each request of the walk, not the whole walk. |
A limit outside 1 to 100, or a non-integer, is refused with 400 request.invalid_pagination rather than being clamped server-side. error.field names the offending parameter.
How the walk terminates
list_all and iterate stop on either of two conditions, which matters because it makes them safe against a server that keeps saying hasMore:
hasMore is False
The normal case.
A page comes back empty
Even if hasMore is still True. Without this a bad cursor would loop forever.
The offset advances by the number of items actually received, not by limit, so a short page does not skip rows. A failure on any page fails the whole list_all call, and the rows it had already collected are lost; iterate has already handed you the earlier items, so it raises at the page that failed.
Secondary collections
A namespace can paginate more than one collection. Those get their own trio, named after what they read.
| Namespace | Primary | Secondary |
|---|---|---|
products | list, list_all, iterate | list_projects, list_all_projects, iterate_projects and access.list, access.list_all, access.iterate |
variables | list, list_all, iterate | list_history, list_all_history, iterate_history |
versions | list, list_all, iterate | list_entries, list_all_entries, iterate_entries |
webhooks | list, list_all, iterate | list_deliveries, list_all_deliveries, iterate_deliveries |
Two endpoints look like lists but are not paginated, because the whole set is small and fixed. Both return a plain dict rather than a Page:
| Method | Returns |
|---|---|
roles.list_permissions() | PermissionsResource with the full catalogue, a byResource index, and the system role sets |
webhooks.list_event_types() | EventTypesResource with eventTypes and totalCount |
One collection carries an extra field
products.access.list() returns a ProductAccessPage, which is a normal page plus isPrivate. That flag tells you whether the grants matter at all: on a public product every member has access and the list is informational.
from envless import envless
access = envless.products.access.list('internal-tools')
if not access['isPrivate']:
print('Public product, every member already has access')
print([entry['id'] for entry in access['items']])access.list_all and access.iterate only stream items, so they drop isPrivate. If you need the flag, use access.list.
Counting without fetching
totalCount is the size of the full filtered set, not the page, so a single one-row request is enough to count.
from envless import envless
pagination = envless.members.list(limit=1)['pagination']
print(f"{pagination['totalCount']} members")