Skip to content

Pagination and filtering

Cursor pagination (almost everywhere)

List endpoints return a CursorPage. The simplest way to use it: iterate with foreach, and let the SDK fetch the following pages lazily, under the client throttle.

foreach ($client->customerInvoices->list(limit: 100) as $invoice) {
    // ...  walks EVERY page transparently
}

To control pages yourself:

$page = $client->customerInvoices->list(limit: 100);
$page->items;        // items of the current page only
$page->hasMore;      // whether a next page exists
$page->nextCursor;   // opaque cursor for the next page
$nextPage = $page->nextPage();        // or null on the last page

foreach ($page->iterPages() as $page) {      // page by page
    echo count($page->items) . PHP_EOL;
}

limit accepts 1 to 100 (the API default is 20). Fewer requests with limit: 100 means faster bulk reads under the rate limit.

Filters are not encoded in the cursor

When you paginate a filtered query, the API requires the same filter and sort on every page request. The SDK does this for you in nextPage() and during iteration.

Page-number pagination (a few Firm endpoints)

Three Firm API endpoints (companies, fiscalYears, trialBalance) paginate with page and perPage instead of a cursor. Same experience:

foreach ($firm->companies->list(perPage: 50) as $company) {
    // ...
}

These return a NumberedPage with totalPages and currentPage properties, and a hasMore() method.

Filtering

The filter parameter is a list of conditions. Build it with the typed static helpers on Filter:

use Pennylane\Sdk\Filter;

$client->customerInvoices->list(filter: [
    Filter::gte('date', '2026-01-01'),
    Filter::lt('date', '2026-07-01'),
    Filter::eq('status', 'upcoming'),
    Filter::in('customer_id', [42, 43]),
]);

Available helpers: eq, notEq, lt, lte, gt, gte, in, notIn, and where(field, operator, value) for anything else. Values can be strings, ints, booleans, \DateTimeInterface or null; amounts must be numeric strings, never floats.

Wire operators

lte() and gte() are convenience names: on the wire the API expects lteq and gteq. The SDK encodes them for you, so you never write the wire operator yourself.

Which fields and operators each endpoint supports is listed in the official reference per endpoint.

You can also pass a raw JSON string, or a plain array shaped like ['field' => ..., 'operator' => ..., 'value' => ...], if you already have one; the SDK sends it as-is.

Sorting

$client->customerInvoices->list(sort: '-date');   // descending by date
$client->customerInvoices->list(sort: 'id');      // ascending by id

Since the 2026 API changes, the default sort is -id (newest first) on all endpoints.

Side-loading with include

Some list endpoints support an experimental include parameter to embed related records in one call:

$page = $client->customerInvoices->list(include: 'invoice_lines');
$page->included;   // raw list of side-loaded records