# Pagination

Offset-based pagination, response envelope, and how to iterate through pages.

List endpoints use offset-based pagination. Two query parameters drive it, and every paginated response carries the same envelope so you can iterate without endpoint-specific logic.

## Query parameters

| Parameter | Type    | Default | Bounds    |
| --------- | ------- | ------- | --------- |
| `limit`   | integer | `20`    | `1`–`100` |
| `offset`  | integer | `0`     | `≥ 0`     |

> **Note:** A small number of endpoints raise these defaults — most notably `GET /v2/usage` (default `100`, max `500`) and `GET /v2/billing` (default `50`, max `100`). Always check the per-operation schema in the OpenAPI spec for the authoritative bounds.

## Response envelope

```json
{
  "data": [ /* items */ ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "has_more": true,
    "next_offset": 20,
    "prev_offset": null
  }
}
```

| Field         | Type            | Description                                                        |
| ------------- | --------------- | ------------------------------------------------------------------ |
| `limit`       | integer         | Page size used for this response.                                  |
| `offset`      | integer         | Current offset.                                                    |
| `has_more`    | boolean         | Whether more results exist beyond this page.                       |
| `next_offset` | integer \| null | Offset to fetch the next page, or `null` if this is the last page. |
| `prev_offset` | integer \| null | Offset for the previous page, or `null` if this is the first page. |

A few endpoints extend the envelope with additional fields — for example, `GET /v2/invoices` adds a `total` count of matching invoices across all pages. Per-operation schemas in the OpenAPI spec are authoritative.

## Example: first page

```http
GET https://api.benjipays.com/v2/partners?limit=20&offset=0
Authorization: Bearer YOUR_ACCESS_TOKEN
```

```json
{
  "data": [
    { "partnerId": "6022f98854f2c41d7ef2b1fe", "name": "Acme MSP", "externalId": "acme-001" }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "has_more": true,
    "next_offset": 20,
    "prev_offset": null
  }
}
```

## Example: next page

```http
GET https://api.benjipays.com/v2/partners?limit=20&offset=20
Authorization: Bearer YOUR_ACCESS_TOKEN
```

```json
{
  "data": [ /* ... 20 items starting from offset 20 */ ],
  "pagination": {
    "limit": 20,
    "offset": 20,
    "has_more": true,
    "next_offset": 40,
    "prev_offset": 0
  }
}
```

## Example: last page

```json
{
  "data": [ /* ... remaining items (less than `limit`) */ ],
  "pagination": {
    "limit": 20,
    "offset": 60,
    "has_more": false,
    "next_offset": null,
    "prev_offset": 40
  }
}
```

## Iterating through all pages

```javascript
async function fetchAllPartners(accessToken) {
  const all = [];
  let offset = 0;
  const limit = 20;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(
      `https://api.benjipays.com/v2/partners?limit=${limit}&offset=${offset}`,
      { headers: { Authorization: `Bearer ${accessToken}` } }
    );
    const body = await response.json();
    all.push(...body.data);
    hasMore = body.pagination.has_more;
    offset = body.pagination.next_offset ?? offset + limit;
  }

  return all;
}
```

## Best practices

* Use `has_more` to decide whether to fetch another page; don't infer it from `data.length`.
* Use `next_offset` from the response rather than recomputing it.
* Pick a `limit` close to your actual rendering / processing batch size.
* Respect [rate limits](./rate-limits.md) when iterating — pace your requests so you don't burst through your quota.
* An empty `data` array with `has_more: false` is the end of the list, not an error.