Pagination

Page through list endpoints with cursor-based pagination.

View as Markdown

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 offsetsAsk

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 parametersAsk

List endpoints accept the following query parameters:

Prop

Type

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 envelopeAsk

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

{
  "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
  }
}

Prop

Type

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 fullyAsk

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

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);
}
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);
# 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 filtersAsk

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.

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"

LimitsAsk

  • 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 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 stepsAsk

Was this page helpful?