---
title: "Pagination"
description: "`list` for one page, `list_all` for every page, and `iterate` for one item at a time."
url: "https://openemail.uk/docs/python/pagination"
area: "Python"
category: "Getting started"
---

# Pagination

`list` for one page, `list_all` for every page, and `iterate` for one item at a time.

## One page: list

**list_page.py**

```
cursor: str | None = None

while True:
    page = client.emails.list(status='failed', limit=50, cursor=cursor)

    for email in page['items']:
        print(email['id'], email['lastError'])

    if not page['hasMore'] or page['nextCursor'] is None:
        break

    cursor = page['nextCursor']
```

A page is `{'items': [...], 'hasMore': ..., 'nextCursor': ...}`. Pass `nextCursor` back as `cursor=`, with the same filters, for the page after it.

`nextCursor` is `None` on the last page, and `hasMore` says whether more rows match beyond this one. In `openemail.types` a page is `Page[T]`, so `emails.list` returns a `Page[EmailResource]` and a type checker knows what each item holds.

> The API pages threads and drafts with a `pageToken`. The client hands it to you as `nextCursor` and takes it back as `cursor=`, like every other list, and `list_all` and `iterate` follow it for you. It is opaque: pass back what you were given and never build one.

## Every page: list_all

**list_all.py**

```
complaints = client.suppressions.list_all(reason='complaint')
print(len(complaints), [row['email'] for row in complaints])

book = client.addresses.list_all()
print(book['unrestricted'], len(book['addresses']), len(book['domains']))
```

`list_all` follows `nextCursor` to the last page and returns one `list`. Every page is fetched before it returns, so give it a filter that ends.

`addresses.list_all` is the one that returns something else: the whole address book, `{'unrestricted': ..., 'addresses': [...], 'domains': [...]}`, with the addresses of every page in it.

## One at a time: iterate

**iterate.py**

```
from itertools import islice

for email in client.emails.iterate(status='bounced'):
    if email['createdAt'] < '2026-09-01':
        break

    print(email['id'], email['lastError'])

first_ten = list(islice(client.threads.iterate(folder='inbox'), 10))
print([thread['id'] for thread in first_ten])
```

`iterate` returns a generator. Calling it sends nothing: the first page is fetched when the loop asks for its first item, and each page after it only when the loop reaches the end of the one before. Breaking out of the loop, or taking a slice with `itertools.islice`, stops the requests.

> `limit=` on `list_all` and `iterate` is the size of each page they fetch, not a cap on how many items you get, so a larger `limit` means fewer requests. `cursor=` starts the walk from a page you already hold. Both stop when `hasMore` is false or `nextCursor` is `None`, and also when a page names the cursor that fetched it, so a misbehaving page cannot loop for ever.

## Lists inside a namespace

`list_all` and `iterate` page through the same endpoint as the `list` beside them, and so do the other pairs named after the list they walk. Each pair takes the same filters as its list.

| Namespace | One page | Every page | One at a time |
| --- | --- | --- | --- |
| `emails` | `list_events` | `list_all_events` | `iterate_events` |
| `calendar` | `list_events` | `list_all_events` | `iterate_events` |
| `templates` | `list_versions` | `list_all_versions` | `iterate_versions` |
| `templates` | `list_images` | `list_all_images` | `iterate_images` |
| `tracking` | `list_opens` | `list_all_opens` | `iterate_opens` |
| `tracking` | `list_clicks` | `list_all_clicks` | `iterate_clicks` |
| `rules` | `list_runs` | `list_all_runs` | `iterate_runs` |
| `audiences` | `list_contacts` | `list_all_contacts` | `iterate_contacts` |
| `broadcasts` | `list_recipients` | `list_all_recipients` | `iterate_recipients` |
| `contacts` | `list_people` | `list_all_people` | `iterate_people` |
| `contacts` | `list_threads` | `list_all_threads` | `iterate_threads` |
| `domains` | `list_addresses` | `list_all_addresses` | `iterate_addresses` |
| `forms` | `list_submissions` | `list_all_submissions` | `iterate_submissions` |
| `members` | `list_invitations` | `list_all_invitations` | `iterate_invitations` |
| `files` | `list_links` | `list_all_links` | `iterate_links` |
| `temp_mail` | `list_messages` | `list_all_messages` | `iterate_messages` |
| `webhooks` | `list_deliveries` | `list_all_deliveries` | `iterate_deliveries` |
| `webhooks` | `list_workspace_deliveries` | `list_all_workspace_deliveries` | `iterate_workspace_deliveries` |
| `webhooks` | `list_activity` | `list_all_activity` | `iterate_activity` |
| `webhooks` | `list_workspace_activity` | `list_all_workspace_activity` | `iterate_workspace_activity` |
| `keys` | `list_requests` | `list_all_requests` | `iterate_requests` |
| `keys` | `list_activity` | `list_all_activity` | `iterate_activity` |
| `keys` | `list_workspace_requests` | `list_all_workspace_requests` | `iterate_workspace_requests` |
| `keys` | `list_workspace_activity` | `list_all_workspace_activity` | `iterate_workspace_activity` |

## Pages that carry more

**other_pages.py**

```
people = client.contacts.list_people(sort='recent', limit=50)
print(len(people['items']), people['seen'], people['nextCursor'])

sends = client.templates.list_sends('order-shipped', page=2, page_size=50)
print(sends['total'], sends['page'], sends['pageSize'], len(sends['items']))
```

A few lists carry more than a page. `contacts.list_people` adds `seen` beside `items`, `hasMore` and `nextCursor`, `temp_mail.list_messages` adds `expiresAt`, and `addresses.list` returns `addresses` in place of `items`, beside `unrestricted`, `domains`, `hasMore` and `nextCursor`. Each still continues with `cursor=` and walks with its `list_all` and `iterate`.

Two lists page another way. `templates.list_sends` counts pages: pass `page=` and `page_size=`, and read `total`, `page` and `pageSize` beside `items`. `imports.list_failures` is left as the API sends it, `{'object': 'list', 'data': [...], 'nextCursor': ...}`, and continues with `after=`, the number in `nextCursor`. Neither has a `list_all` or an `iterate`.

## Page sizes

Most lists take a `limit=` from 1 to 100 and default to 25, the two numbers in `PAGE_LIMITS` (`PAGE_LIMITS.MAX_LIMIT` and `PAGE_LIMITS.DEFAULT_LIMIT`). `contacts.list`, `audiences.list_contacts`, `broadcasts.list_recipients` and the `tracking` lists take up to 200 with a default of 50, and `temp_mail.list_messages` takes up to 50. A value outside the range is refused with a 422 rather than clamped.

## On AsyncOpenEmail

The async client pages the same way. `list` and `list_all` are awaited, and `iterate` returns an async iterator that you loop over with `async for`, with no `await` in front of the call.

**async_pages.py**

```
import asyncio

from openemail import AsyncOpenEmail

async def main() -> None:
    async with AsyncOpenEmail() as client:
        page = await client.emails.list(status='failed', limit=50)
        print(len(page['items']), page['nextCursor'])

        everyone = await client.members.list_all()
        print(len(everyone))

        async for delivery in client.webhooks.iterate_deliveries('whe_…', status='failed'):
            print(delivery['eventType'], delivery['responseCode'])

asyncio.run(main())
```

- [Async](https://openemail.uk/docs/python/async.md): The same methods, awaited, on asyncio or trio.
