# Pagination

> Page through lists of messages, conversations, events and logs with cursors, without missing or repeating items.

Source: https://docs.vlayers.ai/developers/pagination

Kind: How-to guide

## What you'll build

A loop that reads a whole list one page at a time, and a poller that only fetches what is new.

## Prerequisites

- A secret API key with the scope of the list you read (`messages:read`, `events:read`, `webhooks:manage` or `logs:read`). See [Authentication and API keys](https://docs.vlayers.ai/get-started/authentication).
- curl, Node.js 18 or later with `@voicelayer/api`, or Python 3.9 or later with `voicelayer`.

## How cursors work

Lists return a page and a cursor. Pass the cursor back to get the next page. Cursors are opaque: store them as they are, never build or change one. A cursor the API did not issue is refused with `400`.

| List | Send | Read from the answer | Page size | Order |
|---|---|---|---|---|
| `GET /v1/messages`, `GET /v1/conversations/{id}/messages` | `after` (newer), `before` (older, or `end` for the newest page) | `nextCursor`, `prevCursor`, `hasMore` | default 50, max 100 | oldest first |
| `GET /v1/conversations` | `cursor` | `nextCursor` (`null` at the end) | default 50, max 100 | most recently active first |
| `GET /v1/events` | `after` | `cursor`, `hasMore` | default 100, max 500 | oldest first |
| `GET /v1/request-logs` | `after` (older), `since` (newer) | `nextCursor`, `latestCursor` | default 50, max 200 | newest first |
| `GET /v1/webhooks/deliveries` | `since`, `until` (times) | — | default 50, max 200 | newest first |

Set the page size with `limit`.

## Step 1: Read the first page

cURL:

```bash
curl "https://api.vlayers.ai/v1/conversations?controller=human&limit=20" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listConversations } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listConversations({
  client,
  query: {
    controller: 'human',
    limit: 20
  }
});
console.log(data.conversations);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.list_conversations(
        controller="human",
        limit=20,
    )
    print(result.conversations)
```

Response `200`:

```json
{
  "conversations": [
    {
      "id": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21",
      "channel": "sms",
      "lineId": "0192f5a8-1a2b-7c3d-8e4f-5a6b7c8d9e01",
      "bindingId": null,
      "peer": {
        "address": "+15550123"
      },
      "controller": "api",
      "status": "open",
      "openedAt": "2026-10-20T15:04:05.000Z",
      "lastActivityAt": "2026-10-20T15:04:05.000Z",
      "test": true
    }
  ],
  "nextCursor": null
}
```

## Step 2: Follow the cursor

Pass `nextCursor` back as `cursor` until it is `null`:

```bash
export NEXT_CURSOR=<the nextCursor you got>
```

cURL:

```bash
curl "https://api.vlayers.ai/v1/conversations?cursor=$NEXT_CURSOR&limit=50" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listConversations } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listConversations({
  client,
  query: {
    cursor: process.env.NEXT_CURSOR!,
    limit: 50
  }
});
console.log(data.nextCursor);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.list_conversations(
        cursor=os.environ["NEXT_CURSOR"],
        limit=50,
    )
    print(result.next_cursor)
```

## Step 3: Show a chat history, newest first

For a conversation's messages, start with `before=end` for the newest page. Each page is still in time order (oldest first). Pass its `prevCursor` as `before` for the page before it; `prevCursor` is `null` when nothing older is left.

```bash
export CONVERSATION_ID=<the conversation's id>
```

cURL:

```bash
curl "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID/messages?before=end&limit=20" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listConversationMessages } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listConversationMessages({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  query: {
    before: 'end',
    limit: 20
  }
});
console.log(data.messages);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.list_conversation_messages(
        id=os.environ["CONVERSATION_ID"],
        before="end",
        limit=20,
    )
    print(result.messages)
```

## Step 4: Poll for what is new

Keep the last `nextCursor` (messages) or `cursor` (events) and pass it as `after` next time. On an empty page the cursor stays where it was, so a poller never loses its place. For events, add `waitSec` to wait for the next one instead of polling in a tight loop:

cURL:

```bash
curl "https://api.vlayers.ai/v1/events?after=$CURSOR&waitSec=20" \
  -H "Authorization: Bearer $VOICELAYER_API_KEY"
```

TypeScript:

```ts
import { createVoiceLayerClient, listEvents } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await listEvents({
  client,
  query: {
    after: process.env.CURSOR!,
    waitSec: 20
  }
});
console.log(data.events);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.EventsApi(client)
    result = api.list_events(
        after=os.environ["CURSOR"],
        wait_sec=20,
    )
    print(result.events)
```

## Test it

With a test key, simulate a few messages with `POST /v1/test/inbound` (see [Test mode](https://docs.vlayers.ai/developers/test-mode)), then page through `GET /v1/messages` with `limit=1`.

## Common errors

| Error | Status | How to fix |
|---|---|---|
| [`invalid_request`](https://docs.vlayers.ai/errors/invalid_request) | 400 | Read `message` and `details.issues` (the validation issues, by path), fix the request and send it again. |
| [`invalid_cursor`](https://docs.vlayers.ai/errors/invalid_cursor) | 400 | Pass back a `cursor` (pull) or an event `id:` line (stream) exactly as received, or leave it out to start afresh. |
| [`cursor_expired`](https://docs.vlayers.ai/errors/cursor_expired) | 410 | Page through `GET /v1/events` from the same cursor (the pull reads the whole retention), then reconnect the stream with its last cursor. |

## Next steps

- [Stream events](https://docs.vlayers.ai/developers/receive/event-stream): no polling at all.
- [Rate limits](https://docs.vlayers.ai/developers/rate-limits).
- [API reference](https://docs.vlayers.ai/reference).
