# Pagination

> Page through list endpoints with cursor-based pagination.

Every VINR list endpoint — payments, invoices, settlements, points transactions, and more — returns results in pages. VINR uses cursor-based pagination rather than numeric page offsets, so results stay stable and fast even while new records are being created underneath you.

## Why cursors, not offsets

Offset pagination (`?page=3`) re-scans the table on every request and silently skips or duplicates rows when records are inserted or deleted between calls. A cursor is an opaque pointer to a specific position in a stable, descending-by-`created_at` ordering. Paging forward from a cursor always returns the next slice exactly once, regardless of writes happening concurrently.

> Cursors are opaque. Do not parse, decode, or construct them yourself — the format is internal and may change without notice. Treat each cursor as a string you received from VINR and hand back unmodified.

## Cursor parameters

List endpoints accept the following query parameters:

| Field            | Type     | Description                                                             | Default |
| ---------------- | -------- | ----------------------------------------------------------------------- | ------- |
| `limit`          | `number` | Page size, between 1 and 100                                            | `25`    |
| `starting_after` | `string` | Object ID to page forward from; returns records created before this one | `—`     |
| `ending_before`  | `string` | Object ID to page backward from; returns records created after this one | `—`     |

`starting_after` and `ending_before` take a resource ID (for example `pay_8sQ2...`), not a separate token. Use the ID of the last item on the current page to fetch the next page. The two are mutually exclusive — sending both returns a `400`.

## Response envelope

List responses share a consistent envelope. The `data` array holds the resources for the current page; `page` carries the metadata you need to iterate.

```json
{
  "object": "list",
  "data": [
    { "id": "pay_8sQ2hT1aZ", "object": "payment", "amount": 1000, "currency": "EUR" },
    { "id": "pay_7rP1gS0bY", "object": "payment", "amount": 2500, "currency": "EUR" }
  ],
  "page": {
    "has_more": true,
    "next_cursor": "pay_7rP1gS0bY",
    "total_estimate": 1842
  }
}
```

> `total_estimate` is a cached approximation for display ("about 1,800 payments"). Never derive a page count or loop bound from it — drive iteration off `has_more` and `next_cursor` only.

## Iterating fully

The SDK exposes an async iterator that handles cursors for you. Prefer it over manual paging.

##### SDK auto-paginate

```ts
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

// Streams every matching payment, fetching pages lazily as you consume them.
for await (const payment of vinr.payments.list({ limit: 100 })) {
  console.log(payment.id, payment.amount);
}
```

##### Manual loop

```ts
import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

let cursor: string | undefined;
do {
  const res = await vinr.payments.list({ limit: 100, startingAfter: cursor });
  for (const payment of res.data) {
    console.log(payment.id, payment.amount);
  }
  cursor = res.page.has_more ? res.page.next_cursor ?? undefined : undefined;
} while (cursor);
```

##### Raw REST

```bash
# First page
curl https://api.vinr.com/v1/payments?limit=100 \
  -H "X-Api-Key: $VINR_SECRET_KEY"

# Next page: pass the last ID from the previous response
curl "https://api.vinr.com/v1/payments?limit=100&starting_after=pay_7rP1gS0bY" \
  -H "X-Api-Key: $VINR_SECRET_KEY"
```

When iterating manually, stop when `has_more` is `false`. Do not loop on a fixed count, and never assume a page is empty just because it is smaller than `limit` — only the final page reliably contains fewer items.

## Combining with filters

Cursors are stable across most filters (date ranges, `status`, `customer`), so you can page through a filtered set the same way. Keep the filter parameters identical on every request in a sequence — changing a filter mid-iteration invalidates the cursor and returns a `400`.

```bash
curl "https://api.vinr.com/v1/invoices?status=open&created_after=2026-05-01T00:00:00Z&limit=50" \
  -H "X-Api-Key: $VINR_SECRET_KEY"
```

## Limits

- **Default page size** is 25; **maximum** is 100. Requesting more than 100 is clamped to 100, not rejected.
- **Cursor lifetime** is 24 hours. A `next_cursor` older than that returns `400 cursor_expired`; restart from the first page.
- **Deep iteration** is rate limited like any other call. For large exports (more than \~50,000 records), use the [Bulk export](/docs/operations) jobs instead of paging in a tight loop.

> For backfills and reconciliation, page **backward in time** with the default newest-first order and persist the last processed ID. On the next run, page forward from that ID with `starting_after` to pick up only what is new.

## Next steps

[API Reference](/docs/api-reference) — Endpoints, objects, and authentication for the VINR API.

[Rate limits](/docs/api-reference/rate-limits) — Throughput tiers and how to handle 429 responses while iterating.

[Webhooks](/docs/integration) — React to new records in real time instead of polling list endpoints.
