# Pagination

Every list endpoint is newest first and cursor paginated. Pass meta.next_cursor back as cursor to walk through the pages.

List endpoints such as `GET /calls`, `GET /leads` and `GET /chats` return the newest records first and use **cursor pagination**: each page tells you where the next one starts, so records created while you page through never shift or repeat.

## Parameters

| Parameter | Description |
| --- | --- |
| `per_page` | How many records per page, from 1 to 100. Defaults to 25. |
| `cursor` | The `meta.next_cursor` (or `meta.prev_cursor`) of the page you just read. Leave it out for the first page. |

Most lists also take filters, such as `since` and `until` (ISO 8601 times) or a `status`. Keep the same filters on every page. The [API reference](https://vocenya.com/docs/reference) lists the filters of each endpoint.

## Response shape

```json
{
  "data": [
    { "id": 311, "name": "Jordan Smith", "source": "chat", "created_at": "2026-10-02T14:05:40+00:00" }
  ],
  "links": {
    "first": null,
    "last": null,
    "prev": null,
    "next": "https://vocenya.com/api/public/v1/leads?cursor=eyJpZCI6MzExLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9"
  },
  "meta": {
    "path": "https://vocenya.com/api/public/v1/leads",
    "per_page": 25,
    "next_cursor": "eyJpZCI6MzExLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
    "prev_cursor": null
  }
}
```

`meta.next_cursor` is `null` on the last page. Treat cursors as opaque strings: pass them back exactly as you received them.

## Fetching every page

```javascript
async function fetchAllLeads() {
  const leads = [];
  let cursor = null;

  do {
    const url = new URL('https://vocenya.com/api/public/v1/leads');
    url.searchParams.set('per_page', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.VOCENYA_API_KEY}` },
    });
    const page = await response.json();

    leads.push(...page.data);
    cursor = page.meta.next_cursor;
  } while (cursor);

  return leads;
}
```

```php
$client = new \GuzzleHttp\Client();
$leads = [];
$cursor = null;

do {
    $response = $client->request('GET', 'https://vocenya.com/api/public/v1/leads', [
        'headers' => ['Authorization' => 'Bearer '.getenv('VOCENYA_API_KEY')],
        'query' => array_filter(['per_page' => 100, 'cursor' => $cursor]),
    ]);
    $page = json_decode((string) $response->getBody(), true);

    array_push($leads, ...$page['data']);
    $cursor = $page['meta']['next_cursor'];
} while ($cursor !== null);
```

```python
import os

import requests

leads = []
cursor = None

while True:
    params = {"per_page": 100}
    if cursor:
        params["cursor"] = cursor

    page = requests.get(
        "https://vocenya.com/api/public/v1/leads",
        headers={"Authorization": f"Bearer {os.environ['VOCENYA_API_KEY']}"},
        params=params,
    ).json()

    leads.extend(page["data"])
    cursor = page["meta"]["next_cursor"]
    if not cursor:
        break
```

## Syncing new records

To keep another system up to date, prefer [webhooks](https://vocenya.com/docs/webhooks): they tell you about new leads, calls, bookings and chats as they happen. If you poll instead, pass `since` with the time of your last sync and page until `next_cursor` is `null`, staying within the [rate limit](https://vocenya.com/docs/rate-limits).
