Pagination
One page, every page, or one item at a time, on every list that pages.
list, listAll and iterate
Every list that pages has three methods. list fetches one page and returns an OpenEmail\Result\Page. listAll follows the cursor through every page and returns one array. iterate walks the same pages one item at a time and returns a Generator, which fetches the next page only when you get there. All three take the list’s filters, limit:, cursor: and apiKey:.
$page = $client->emails->list(status: 'failed', limit: 50); foreach ($page as $email) { echo $email['id'], ' ', $email['lastError'] ?? '', PHP_EOL;} $failures = $client->emails->listAll(status: 'failed'); foreach ($client->emails->iterate(status: 'failed') as $email) { echo $email['id'], PHP_EOL;} echo count($failures), ' ', $page->hasMore ? 'more to come' : 'that is all', PHP_EOL;The same three names repeat wherever a namespace has more than one list, named after the list they walk: listEvents, listAllEvents and iterateEvents on emails, listDeliveries, listAllDeliveries and iterateDeliveries on webhooks, and so on.
OpenEmail\Result\Page
itemsarray- The rows of this page, lifted out of the API’s `data` envelope, each an associative array. Empty when the page holds nothing.
hasMorebool- Whether another page follows. When the API sends no `hasMore`, it is true exactly when there is a `nextCursor`.
nextCursorstring or null- What to pass back as `cursor:` for the next page, and null on the last one.
A page is immutable: its properties are readonly. It is IteratorAggregate and Countable too, so foreach ($page as $item) walks its rows and count($page) counts them.
A Generator
iterate asks for nothing until you start to walk it, and it asks for the next page only once every item of the current one has been yielded, so anything that stops early stops the requests too: break ends the walk, and so does returning from the function that holds the loop.
$latest = []; foreach ($client->emails->iterate(status: 'failed', limit: 100) as $email) { $latest[] = $email; if (count($latest) === 10) { break; }} $invoice = null; foreach ($client->emails->iterate(status: 'failed') as $email) { if (($email['tags']['invoice'] ?? null) === 'inv_2026_09_4192') { $invoice = $email; break; }} echo count($latest), ' ', $invoice['id'] ?? 'not found', PHP_EOL;Anything that needs every item, such as iterator_to_array() called on the Generator, reads every page before it returns, the way listAll does.
A Generator can be walked only once. Walking it a second time throws, so call iterate again when you need the items again, or keep the items rather than the Generator.
Resuming from a cursor
A cursor is opaque. Keep the nextCursor of the last page you read and pass it back as cursor: to carry on from there, in a later request or in another process. listAll and iterate take cursor: too, and start their walk after it.
$firstPage = $client->emails->list(status: 'failed', limit: 25);$saved = $firstPage->nextCursor; if ($saved !== null) { $rest = $client->emails->listAll(status: 'failed', cursor: $saved); echo count($rest), PHP_EOL;}A cursor belongs to the list and the filters it came from, so send the same filters with it. One the list cannot place is refused with invalid_cursor, and the answer then is to start again without one.
limit:
limit: is the size of each page, not a total. On list it is how many rows come back. On listAll and iterate it is how many each request asks for, so a larger value means fewer round trips for the same rows. Each list has its own range and default, most often 1 to 100 with 25 when you send none, and a value outside the range is refused rather than clamped. The reference for each list gives its range.
When a walk stops
- When a page says
hasMoreis false. - When a page carries no
nextCursor, since a page that claims more while naming no cursor would loop for ever. - When the API hands back a cursor the walk has already followed, for the same reason.
Each page is a GET, so it is retried on its own like any read before anything throws. A failure that survives the retries throws out of listAll, and the items already fetched are discarded. With iterate the items of the earlier pages have already been yielded by then, so make what the loop does safe to run twice, or page with list and keep each nextCursor so a second attempt can start where the first one stopped.
Threads and drafts
threads->list and drafts->list, with their listAll and iterate, page with the API’s pageToken and nextPageToken rather than a cursor. The client hides the difference: pass the token as cursor: and read it from nextCursor.
$page = $client->threads->list(folder: 'inbox', limit: 50);$later = $page->hasMore ? $client->threads->list(folder: 'inbox', limit: 50, cursor: $page->nextCursor) : null; echo count($page), ' ', $later === null ? 0 : count($later), PHP_EOL;The server offers a token whenever a page comes back full, so hasMore can be true on what turns out to be the last page, and the next call then returns no items.
Pages that carry more
A few lists answer with more than rows, and return an object of their own from OpenEmail\Result in place of Page. Each is immutable, IteratorAggregate over its rows and Countable.
| Method | Returns | What it adds |
|---|---|---|
| addresses->list | AddressBookPage | addresses in place of items, plus unrestricted and domains, with hasMore and nextCursor. |
| addresses->listAll | AddressBook | Every address in addresses, with unrestricted and domains as the last page reported them. It is the one listAll that returns the whole address book rather than an array. addresses->iterate yields the addresses alone. |
| contacts->listPeople | PeoplePage | seen, false when the key cannot read the addresses seen in mail. listAllPeople and iteratePeople return the people alone. |
| tempMail->listMessages | TempMessagesPage | expiresAt, when the inbox runs out. listAllMessages and iterateMessages return the messages alone. |
| templates->listSends | TemplateSends | Paged by number rather than by cursor: items, total, page and pageSize. Ask for the next page with page:. |
| emails->sendBatch | BatchResult | Not a page: items, one for each message you sent, with the sent and failed counts. |
Lists that return the API’s array
Some lists page by offset, by page number or by a numeric cursor of their own, and return the decoded body as it came, an array with data, rather than a Page. They have no listAll or iterate, so you page them yourself.
| Method | Pages with | What comes back |
|---|---|---|
| exports->list | limit: and offset: | data, total and hasMore. |
| imports->listFailures | after: and limit: | data, and nextCursor, an integer to pass back as after: that is null on the last page. |
| subscriptions->list and subscriptions->listDomains | limit: and offset: | data, total, counts and hasMore. |
| billing->listInvoices | page: and limit: | data, total, page, limit, hasMore and metered. |
$offset = 0; do { $batch = $client->subscriptions->list(status: 'active', limit: 50, offset: $offset); foreach ($batch['data'] as $row) { echo $row['senderEmail'], ' ', $row['total'], PHP_EOL; } $offset += count($batch['data']);} while ($batch['hasMore'] && $batch['data'] !== []);A list that is not paged at all, such as languages->list, labels->listColors or roles->listPermissions, returns its rows as a plain list straight away.