Pagination
Every list method on PlugipayClient returns a cursor-paginated page. This page covers the response shape, how to iterate manually, and a couple of patterns for consuming a full result set.
If you'd like to know how the underlying API does it, see API pagination.
The list response shape
Unlike single-resource methods (which return the resource directly), list methods return a small envelope:
{
data: T[];
cursor: string | null;
hasMore: boolean;
}
| Field | Type | Notes |
|---|---|---|
data |
T[] |
The page of results — up to limit items. |
cursor |
string | null |
Pass back to get the next page. null once you've reached the end. |
hasMore |
boolean |
true if more results exist past this page. |
Concretely, listing customers:
const page = await plugipay.customers.list({ limit: 50 });
page.data; // → Customer[]
page.cursor; // → "cur_01HX..." or null
page.hasMore; // → true or false
The shape is the same for every resource — customers, plans, checkoutSessions, invoices, subscriptions, payouts, refunds, receipts, ledger, events.
Why a different shape from the API envelope? The HTTP API puts cursor info in
meta.page. The SDK reshapes that to a flat{ data, cursor, hasMore }so you don't have to reach intometaon every page. Single-resource methods don't have this shape — they just return the resource.
Page parameters
Each list method takes a small parameter bag. The pagination-related options are consistent across resources:
| Param | Type | Default | Notes |
|---|---|---|---|
limit |
number |
50 |
Items per page. Maximum 100. |
cursor |
string |
none | Opaque token from a previous page.cursor. |
order |
'asc' | 'desc' |
resource-specific | Some lists (plans, ledger, events) support direction. |
Plus per-resource filters — see each resource's reference for what it accepts.
Example with a filter:
const page = await plugipay.invoices.list({
status: 'paid',
customerId: 'cus_01H...',
limit: 100,
});
The cursor encodes the filter set. Don't change filters between pages — mint a new request without a cursor instead.
Iterating manually
The simplest pattern: loop until hasMore is false.
async function listAllCustomers() {
const all = [];
let cursor: string | undefined = undefined;
do {
const page = await plugipay.customers.list({ limit: 100, cursor });
all.push(...page.data);
cursor = page.cursor ?? undefined;
} while (cursor);
return all;
}
A generic helper across any list-shaped resource:
type Lister<T> = (params: { limit?: number; cursor?: string }) => Promise<{
data: T[];
cursor: string | null;
hasMore: boolean;
}>;
async function listAll<T>(lister: Lister<T>, params: Record<string, unknown> = {}): Promise<T[]> {
const all: T[] = [];
let cursor: string | undefined = undefined;
while (true) {
const page = await lister({ ...params, limit: 100, cursor });
all.push(...page.data);
if (!page.hasMore) break;
cursor = page.cursor ?? undefined;
}
return all;
}
// Usage:
const allCustomers = await listAll((p) => plugipay.customers.list(p));
const allPaidInvoices = await listAll((p) => plugipay.invoices.list(p), { status: 'paid' });
Async iterator pattern
If you'd rather stream the pages (so you can process items as they arrive without holding them all in memory), wrap the loop in an async generator:
async function* iterateCustomers(params: Parameters<typeof plugipay.customers.list>[0] = {}) {
let cursor: string | undefined = undefined;
while (true) {
const page = await plugipay.customers.list({ ...params, limit: 100, cursor });
for (const item of page.data) {
yield item;
}
if (!page.hasMore) break;
cursor = page.cursor ?? undefined;
}
}
// Usage:
for await (const customer of iterateCustomers()) {
await processCustomer(customer);
}
This is the right pattern for big exports — processing tens of thousands of invoices, building a CSV, exporting to a data warehouse. You never load more than 100 items in memory at once.
Use
limit: 100for iteration, lower for UI. The maximum page size keeps the round-trip count low. For an interactive dashboard list,limit: 25orlimit: 50matches how many rows you'd render anyway.
A note on listAll helpers
The SDK does not currently ship a built-in customers.listAll() helper — only the list() method that returns a single page. Roll your own with the snippet above (it's ~10 lines) or use the generic helper.
We're considering adding a built-in across all resources in a future minor version. If you'd find it useful, let us know on GitHub.
Stable ordering
By default, list responses come back newest first (createdAt descending). Cursor pagination preserves order across pages — you won't see duplicates or skip items even if new records are created mid-iteration.
A few endpoints accept order: 'asc' | 'desc':
plans.listledger.listevents.list
The rest are fixed-newest-first. To backfill chronologically (warehouse export), asc is the right choice on supported endpoints; for the rest, iterate descending and reverse at the end.
Cursor lifetime
Cursors are valid for 24 hours after issuance. A stale cursor returns invalid_cursor (which the SDK surfaces as a PlugipayError). Restart the iteration without a cursor.
For long-running export jobs:
- Don't pause for hours between pages. Process the page, persist what you've got, then immediately request the next page.
- If you need to pause longer, save the last-seen
createdAtand restart with asincefilter (where supported) rather than holding a cursor.
Filtering and pagination together
Filters and cursors compose. Once you've passed a filter on the first call, the cursor includes that filter — you don't need to re-pass it:
let cursor: string | undefined;
const filter = { status: 'paid', customerId: 'cus_01H...' };
const firstPage = await plugipay.invoices.list({ ...filter, limit: 100 });
const secondPage = await plugipay.invoices.list({ ...filter, limit: 100, cursor: firstPage.cursor ?? undefined });
// Equivalent: the cursor encodes the same filter
If you do pass mismatched filters with a cursor, you'll get invalid_cursor — the server cross-checks.
Total counts
The SDK doesn't expose total counts. Counting is expensive on large tables, and the count changes between pages anyway. To estimate or audit a count:
- Iterate with
limit: 100and tally as you go. - Use the Reports endpoints (
plugipay.reports.pnl,plugipay.reports.cashFlow) for aggregated totals. - For an exact count, the API has CSV
/exportendpoints on some resources — not currently wrapped by the SDK, but reachable via the low-levelrequest()if needed.
Common pitfalls
Forgetting ?? undefined on cursor
The SDK returns cursor: string | null (matching what the API surfaces). Most loops want string | undefined (because list takes an optional cursor in its params). Convert with cursor ?? undefined — passing null will encode literally as ?cursor=null, which the server rejects.
Changing filters mid-iteration
The cursor encodes the filter set. Don't change status, customerId, etc. between pages. Mint a new request from scratch.
Holding a cursor across hours
24h limit. Treat cursors as session-local, not as a durable bookmark.
Next
- Webhooks — verifying inbound events.
- Reference — every
listmethod onPlugipayClient. - API pagination — the underlying HTTP contract.