Pagination
List endpoints (GET /v1/passes, GET /v1/cards, etc.) are cursor-paginated. Cursor pagination is stable under concurrent inserts and deletes — offset pagination is not — and uses fewer resources at scale.
Shape
Every list response wraps the data:
{ "data": [ /* up to `limit` records */ ], "has_more": true, "next_cursor": "eyJpZCI6InBhc3NfMDFIWlgifQ"}To fetch the next page, pass the cursor:
curl "https://api.qairopay.com/v1/passes?starting_after=eyJpZCI6InBhc3NfMDFIWlgifQ&limit=100" \ -H "Authorization: Bearer $QAIROPAY_KEY"When has_more is false, you’ve reached the end of the dataset.
Parameters
| Parameter | Description |
|---|---|
limit | Page size. Default 25, max 100. |
starting_after | Cursor from a previous response’s next_cursor. Returns records after that cursor. |
ending_before | Cursor from a previous response. Returns records before it (for backward iteration). |
You cannot pass both starting_after and ending_before in the same request.
Default ordering
Records are returned in descending order by creation time (newest first). To order ascending, pass order=asc. Some list endpoints accept additional sort parameters — check the endpoint’s reference page.
Iterating safely
The TypeScript SDK exposes an async iterator that handles cursors for you. There’s one method per resource — list(...) — and it returns an AsyncListIterator that supports both for-await-of auto-pagination AND page-aware control through .page() / .next().
Auto-paginate with for-await-of
import { QairoPay } from "@qairopay/sdk";
const qp = new QairoPay({ apiKey: process.env.QAIROPAY_KEY! });
// Yields every pass across every page. Cursors are managed internally.for await (const pass of qp.passes.list({ status: "installed" })) { console.log(pass.id);}The iterator is lazy: breaking out of the loop stops further fetches. If you only need the first 50 records, the SDK only fetches the first page:
let count = 0;for await (const pass of qp.passes.list({ status: "installed" })) { if (++count >= 50) break; // exactly one API call total}Page-aware control with .page()
When you want to walk pages explicitly — for batching, checkpoint resumption, or backpressure — use .page():
const iter = qp.passes.list({ status: "installed" });const page1 = await iter.page(); // { data, has_more, next_cursor }console.log(`got ${page1.data.length} records, has_more=${page1.has_more}`);
if (page1.has_more) { const page2 = await iter.page(); // advances the cursor await checkpoint(page2.next_cursor); // persist progress}After the iterator is exhausted, .page() returns an empty terminal page ({ data: [], has_more: false, next_cursor: null }) without making a fetch.
Raw HTTP
If you’re not using the SDK, the manual pattern:
let cursor: string | undefined;do { const page = await fetch( `https://api.qairopay.com/v1/passes?limit=100${cursor ? `&starting_after=${cursor}` : ""}`, { headers: { Authorization: `Bearer ${KEY}` } }, ).then((r) => r.json());
for (const pass of page.data) handle(pass); cursor = page.has_more ? page.next_cursor : undefined;} while (cursor);Filters
Most list endpoints support filtering. Filters compose with AND and use simple query parameters:
GET /v1/passes?status=active&template_id=tpl_01HZX&created_after=2026-01-01Date filters use ISO 8601. Comparison operators (gte, lte, etc.) use square-bracket syntax: created[gte]=2026-01-01.
Counts
For performance reasons, list responses do not include a total count. Counting is a separate operation (GET /v1/passes/count?status=active) and is rate-limited more aggressively. If you find yourself fetching all pages just to count them, use the count endpoint instead.