---
title: Pagination
description: Page through list responses from the Reloop API with cursors or page numbers.
---
> For the complete documentation index, see [llms-docs.txt](/llms-docs.txt) or the site index [llms.txt](/llms.txt). Full docs corpus: [llms-full-docs.txt](/llms-full-docs.txt). Prefer markdown URLs (append `.md`) for agent consumption. Product skill: [skill.md](/skill.md).


List endpoints support two ways to page through results:

- **Cursor pagination (recommended).** Pass the `nextCursor` from one response as `cursor` in the next request. Pages stay stable while new records are created, so you never skip or repeat a record. Use this for syncing, exports and agents.
- **Page numbers.** Pass `page` and `limit`. Useful for numbered pages in a UI.

Every list response includes the fields for both.

## Cursor Pagination

Request the first page without a cursor, then follow `nextCursor` until `hasMore` is `false`:

```bash
curl "https://reloop.sh/api/contacts/v1/list?limit=100" \
  -H "x-api-key: rl_prod_YOUR_API_KEY"

# Response includes: "hasMore": true, "nextCursor": "eyJ0IjoiMjAyNi0xMC0yNVQx..."

curl "https://reloop.sh/api/contacts/v1/list?limit=100&cursor=eyJ0IjoiMjAyNi0xMC0yNVQx..." \
  -H "x-api-key: rl_prod_YOUR_API_KEY"
```

```javascript
let cursor = null;
do {
  const url = new URL("https://reloop.sh/api/contacts/v1/list");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { "x-api-key": process.env.RELOOP_API_KEY } });
  const page = await res.json();
  for (const contact of page.contacts) handle(contact);
  cursor = page.nextCursor;
} while (cursor);
```

| Field | Type | Description |
|-------|------|-------------|
| `hasMore` | `boolean` | Whether more results exist after this page. |
| `nextCursor` | `string \| null` | Pass as `cursor` to get the next page. `null` on the last page. |

Rules:

- Treat cursors as opaque strings. Pass them back unchanged; do not build or edit them. An invalid cursor returns `400`.
- Keep the same filters (and `limit`, if you like) on every request in a walk. A cursor only marks a position.
- When `cursor` is set, `page` is ignored. You can start in page mode and switch: every page-mode response also returns a `nextCursor`.
- Results are ordered newest first unless an endpoint says otherwise.
- A few endpoints offer alternative sort orders. Cursors only work with the default order; combining `cursor` with another sort returns `400`, and `nextCursor` is `null` for those sorts:
  - List contacts with `suppressed=true` (sorted by suppression date).
  - List API keys with a `sort` other than `createdAt:desc`.
  - Campaign recipients with `category=clicked` (sorted by clicks).

### Exceptions

- **Automations and custom events** use page numbers only. They are sorted by last update, which changes when a record is edited, so a cursor would not be stable. These lists are small; fetch them with `limit=100`.
- **Agent inbox messages and threads** use `limit` and `offset` and return a plain array.
- **Group members** (`/api/contacts/v1/groups/{group_id}/contacts`) are ordered oldest first.

## Page Numbers

### Query Parameters

| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `page` | `number` | `1` | - | The page number to retrieve (1-indexed). |
| `limit` | `number` | `20` | `100` | The number of records to return per page. |

**Example request:**

```bash
curl "https://reloop.sh/api/contacts/v1/list?page=2&limit=50" \
  -H "x-api-key: rl_prod_YOUR_API_KEY"
```

### Response Structure

Pagination metadata is returned at the **root** of the JSON response alongside the data array. The exact field names vary by endpoint, but every paginated response includes `total`, `page`, and `limit`.

#### Contacts

The contacts list endpoint includes additional aggregate counts:

```json
{
  "object": "contact",
  "contacts": [...],
  "total": 120,
  "page": 1,
  "limit": 20,
  "hasMore": true,
  "nextCursor": "eyJ0IjoiMjAyNi0xMC0yNVQx...",
  "totalContacts": 120,
  "subscribedContacts": 100,
  "unsubscribedContacts": 20,
  "event": "contact.list"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `contacts` | `array` | The list of contact objects for the current page. |
| `total` | `number` | Total number of contacts matching the query. |
| `page` | `number` | The current page number. |
| `limit` | `number` | The page size used for this request. |
| `totalContacts` | `number` | Total contacts in the organization. |
| `subscribedContacts` | `number` | Number of contacts with an active subscription. |
| `unsubscribedContacts` | `number` | Number of contacts who have unsubscribed. |

#### Templates

```json
{
  "templates": [...],
  "total": 45,
  "page": 1,
  "limit": 20,
  "hasMore": true,
  "nextCursor": "eyJ0IjoiMjAyNi0xMC0yNVQx...",
  "event": "template.list"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `templates` | `array` | The list of template objects for the current page. |
| `total` | `number` | Total number of templates matching the query. |
| `page` | `number` | The current page number. |
| `limit` | `number` | The page size used for this request. |

### Calculating Total Pages

Use the `total` and `limit` fields to calculate the number of pages:

```javascript
const totalPages = Math.ceil(response.total / response.limit);
const hasNextPage = response.page < totalPages;
```


