# Пагинация

Списки возвращаются страницами. Переход к следующей странице выполняется по курсору, без номеров страниц и смещений.

## Ответ со списком

Все списки имеют одинаковую оболочку:

```json
{
  "object": "list",
  "data": [ { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31" } ],
  "has_more": true,
  "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU"
}
```

| Поле | Описание |
|---|---|
| `object` | Всегда `list`. |
| `data` | Объекты текущей страницы. |
| `has_more` | `true`, если после этой страницы есть еще данные. |
| `next_cursor` | Курсор следующей страницы. `null`, если страниц больше нет. |
| `meta` | Дополнительные сведения. Есть у переписки и ленты событий. |

Общего числа записей (`total`) и смещения (`offset`) нет.

## Параметры

- `limit`: сколько объектов вернуть. По умолчанию 50, от 1 до 100. У ленты событий до 200.
- `cursor`: значение `next_cursor` из предыдущего ответа.

Порядок записей фиксирован: от старых к новым по времени создания, при равном времени по `id`. Параметра сортировки нет. Новые записи попадают в конец списка.

## Как листать

1. Отправьте первый запрос с нужными фильтрами.
2. Обработайте `data`.
3. Если `has_more` равен `true`, повторите запрос с теми же фильтрами и `cursor=<next_cursor>`.
4. Остановитесь, когда `has_more` равен `false`.

```js
async function listAll(path, params = {}) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(`https://api.melioralab.io/v1${path}`);
    for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
    if (cursor) url.searchParams.set('cursor', cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` },
    });
    if (!res.ok) throw new Error(`Meliora API: ${res.status} ${res.headers.get('Request-Id')}`);
    const page = await res.json();
    items.push(...page.data);
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
  return items;
}

const leads = await listAll('/leads', { campaign_id: '4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14', limit: 100 });
```
```python
import os
import requests

def list_all(path, params=None):
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {os.environ['MELIORA_API_KEY']}"
    params = dict(params or {})
    items = []
    while True:
        resp = session.get(f"https://api.melioralab.io/v1{path}", params=params, timeout=15)
        resp.raise_for_status()
        page = resp.json()
        items.extend(page["data"])
        if not page["has_more"]:
            return items
        params["cursor"] = page["next_cursor"]

leads = list_all("/leads", {"campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "limit": 100})
```

## Правила курсора

- **Курсор непрозрачен.** Не разбирайте и не собирайте его сами, только передавайте обратно.
- **Курсор привязан к запросу.** Фильтры следующего запроса должны совпадать с первым. Если фильтры другие, ключ получил другие права или охват, ответ будет 400 `cursor_mismatch`.
- **`limit` можно менять** между страницами, это не ошибка.
- **Курсор списка живет 15 минут.** Просроченный курсор дает 410 `cursor_expired`: начните обход заново с первой страницы. Курсор ленты событий живет 30 суток, см. [Опрос событий](https://docs.melioralab.io/events-polling/).
- **Страница может быть короче `limit`.** Ответ не превышает 1 МиБ, поэтому длинная страница сокращается, а невыданные записи придут на следующей. Ориентируйтесь на `has_more`, а не на число записей.

## Списки не снимок

Список читается постранично, и между запросами данные могут меняться: лиды добавляются, статусы обновляются. Записи не пропадают и не повторяются из-за смещений, но статус лида на первой странице мог измениться к моменту чтения последней. Если нужно согласованное состояние, сочетайте обход списка с [лентой событий](https://docs.melioralab.io/events-polling/): события после якоря покажут, что изменилось.
