> ## Documentation Index
> Fetch the complete documentation index at: https://docs.horizonpay.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Invoice AI API Pagination: Cursor-Based List Pages

> How cursor-based pagination works on Invoice AI list endpoints, the cursor and limit parameters, and iterating all pages safely in JavaScript.

# Pagination

All list endpoints in the Invoice AI API use **cursor-based pagination**. Cursors are more reliable than offset-based pagination (`?page=2`) for live data — a new invoice created while you are walking a list does not shift rows across page boundaries, so your integration never silently skips a record.

## Why Cursors Instead of Offsets

With offset pagination, fetching page 2 re-runs the full query and skips the first N rows. If an invoice is created between your first and second requests, every subsequent invoice shifts down one position. The invoice that was at position N is now at position N+1, and it falls in the gap between pages — your integration never sees it. For a sync job, that is a missing invoice discovered months later during a reconciliation.

A cursor names a **position** in the ordered result set. New invoices appear at the front of the list, where a client walking backwards never was. The page you already fetched stays exactly where it was.

## Query Parameters

<ParamField query="cursor" type="string">
  An opaque string returned as `next_cursor` in the previous response. Omit
  this parameter (or pass an empty value) to start from the beginning of the
  list — the most recently created resources first.
</ParamField>

<ParamField query="limit" type="integer" default="25">
  Number of items to return per page. Minimum `1`, maximum `100`. Requests
  above the maximum are clamped to `100`.
</ParamField>

## Response Fields

Every list response includes these fields at the top level alongside `data`:

<ResponseField name="data" type="array" required>
  The page of results. May be an empty array if there are no resources or no
  resources match your filters. An empty array does **not** mean an error.
</ResponseField>

<ResponseField name="next_cursor" type="string | null" required>
  Opaque cursor pointing to the position after the last item in this page.
  Pass it as the `cursor` query parameter in your next request to fetch the
  following page. `null` means you have reached the last page — there are no
  more items.
</ResponseField>

## Treating the Cursor as Opaque

<Warning>
  Never parse, construct, or store the cursor as anything other than a string.
  Its internal encoding is an implementation detail that **may change without
  notice**. Code that reverse-engineers the cursor format will break silently
  when the encoding changes.
</Warning>

The cursor is intentionally opaque — treat it as a black-box token. Pass it back as-is in the `cursor` query parameter; never attempt to construct or modify one. If you hand-craft cursors, your integration will break whenever the cursor format evolves.

## Fetching a Single Page

```bash theme={null}
curl "https://invoice.horizonpay.co/api/v1/invoices?limit=10" \
  -H "Authorization: Bearer inv_live_..."
```

**Response**

```json theme={null}
{
  "data": [
    { "id": "in_01HXYZ", "status": "open", "amount_due": 250000, ... },
    { "id": "in_01HWVU", "status": "draft", "amount_due": 99900, ... }
  ],
  "next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTA2LTAxVDA5OjAwOjAwLjAwMFoiLCJpZCI6ImluXzAxSFdWVSJ9"
}
```

To fetch the next page, pass `next_cursor` back as `cursor`:

```bash theme={null}
curl "https://invoice.horizonpay.co/api/v1/invoices?limit=10&cursor=eyJjcmVhdGVkQXQiOiIyMDI0LTA2LTAxVDA5OjAwOjAwLjAwMFoiLCJpZCI6ImluXzAxSFdWVSJ9" \
  -H "Authorization: Bearer inv_live_..."
```

When `next_cursor` is `null` in the response, you have consumed the entire list.

## Iterating All Pages

The following JavaScript async generator transparently walks every page and yields individual invoice objects. Use it to build data exports, reconciliation jobs, or initial syncs.

```javascript theme={null}
async function* listAllInvoices(apiKey) {
  let cursor = null;
  do {
    const url = new URL('https://invoice.horizonpay.co/api/v1/invoices');
    if (cursor) url.searchParams.set('cursor', cursor);
    url.searchParams.set('limit', '100');
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` }
    });
    const body = await res.json();
    yield* body.data;
    cursor = body.next_cursor;
  } while (cursor);
}
```

**Usage:**

```javascript theme={null}
for await (const invoice of listAllInvoices('inv_live_...')) {
  console.log(invoice.id, invoice.status, invoice.amount_due);
}
```

<Tip>
  Use `limit=100` (the maximum) when you intend to iterate all pages. Fewer,
  larger pages means fewer round-trips and lower total latency for a full sync.
  Use a smaller `limit` only when you want to display incremental results to a
  user as each page arrives.
</Tip>

## Combining Pagination with Filters

All list endpoints accept filter parameters alongside `cursor` and `limit`. Filters are applied server-side before the cursor position is evaluated — they are stable across pages as long as the filter values stay the same.

```bash theme={null}
# First page of open invoices, newest first
curl "https://invoice.horizonpay.co/api/v1/invoices?status=open&limit=20" \
  -H "Authorization: Bearer inv_live_..."

# Next page of the same filtered list
curl "https://invoice.horizonpay.co/api/v1/invoices?status=open&limit=20&cursor=eyJ..." \
  -H "Authorization: Bearer inv_live_..."
```

<Warning>
  Do not change filter parameters between pages of the same list traversal.
  Changing `status` or `customer` mid-walk is equivalent to starting a new
  query from the beginning — you may see duplicates or miss records.
</Warning>

### Invoice list filters

<ParamField query="status" type="string">
  Filter by invoice status. Accepted values: `draft`, `open`, `paid`,
  `overdue`, `void`. `overdue` is a derived status — invoices are not stored
  as overdue, but the API identifies open invoices past their due date and
  returns them when you filter by `overdue`.
</ParamField>

<ParamField query="customer" type="string">
  Filter by customer. Accepts a `cus_…` prefixed ID or a raw UUID.
</ParamField>

<ParamField query="from" type="string (ISO 8601 date)">
  Return only invoices created on or after this date. Example: `2024-01-01`.
</ParamField>

<ParamField query="to" type="string (ISO 8601 date)">
  Return only invoices created on or before this date. Example: `2024-12-31`.
</ParamField>

## Ordering

All list endpoints return results in **reverse chronological order** — the most recently created resource is always first. There is no `sort` parameter; the ordering is fixed to ensure cursors remain stable and unambiguous.
