Постраничность
Как читать большие списки ActionPulse API — непрозрачный курсор, параметры cursor и limit, признак последней страницы, отличия реестра и рабочий цикл обхода.
На этой странице
Списковые методы ActionPulse отдают данные страницами по курсору. Номеров страниц и смещения в API нет: вы получаете страницу и вместе с ней значение, которое нужно передать, чтобы получить следующую. Эта страница нужна, когда список не помещается в один ответ — сырые события, участники, сессии, записи реестра, журнал аудита. Какие именно параметры принимает конкретный метод, указано на его странице в справочнике API.
Параметры
| Параметр | Где | Тип | Границы | По умолчанию |
|---|---|---|---|---|
cursor |
строка запроса | строка | до 4096 символов | нет — отсутствие означает «первая страница» |
limit |
строка запроса | целое | от 1 до 100 | 50 |
Это общие для API значения. У части методов границы другие — см. таблицу
исключений ниже. limit — верхняя граница размера страницы, а не гарантия:
страница может оказаться короче.
В ответе значение для следующего шага приходит в поле next_cursor.
{
"events": [{ "event_id": "8b0d3f2e-1c4a-4f0e-9b1d-2a3c4d5e6f70" }],
"next_cursor": "b3BhcXVlLWN1cnNvci12YWx1ZQ"
}
Имя массива с данными у каждого метода своё — events, sessions, imports,
violations, actors, records и так далее, — а поле с курсором во всём API
называется одинаково: next_cursor. Поля элемента в примере сокращены.
Курсор непрозрачен
Курсор — не идентификатор последнего элемента и не номер строки. Это закодированное значение, в которое сервер укладывает позицию обхода и отпечаток всей области запроса: проект, фильтры, часовой пояс, вычисленные границы периода. Разбирать, собирать и править его вручную нельзя — формат не является частью контракта и может измениться.
Практическое следствие одно, и оно важное: между страницами нельзя менять условия запроса. Курсор действителен только для того набора параметров, с которым он был выдан.
| Что изменили между страницами | Результат |
|---|---|
| Фильтр, период, часовой пояс | Курсор больше не подходит: 422 validation_failed, details.field=cursor |
| Проект в адресе | То же: курсор привязан к проекту и в другом проекте не принимается |
| Взяли курсор из другого списка | То же: курсоры разных методов не взаимозаменяемы |
Только limit |
Допустимо: размер следующей страницы можно менять |
Слишком длинное значение отклоняется до попытки его раскодировать — так метод не тратит работу на заведомо некорректный ввод.
Как понять, что страницы закончились
Единственный признак — поле next_cursor:
- поля нет, оно пустое или
null— данных больше нет, обход закончен; - поле непустое — есть ещё как минимум одна страница.
Останавливайтесь именно по нему, а не по числу элементов: контракт обещает
next_cursor только как признак наличия следующих данных, а длина страницы
признаком конца не является. Страница короче limit с непустым next_cursor —
нормальный ответ.
Чего в ответе нет:
- признака
has_more— его роль полностью выполняетnext_cursor; - общего количества элементов как универсального поля. У отдельных методов
в ответе есть
total— например, у выгрузки участников когорты retention и у списка тех, кто отвалился на шаге воронки, — но это часть их предметного результата, а не счётчик страниц. Рассчитывать на него в общем клиенте нельзя; - возможности перейти на страницу N. Обход только последовательный, с начала.
Порядок задаёт сам метод — у большинства списков от новых к старым; точный порядок указан в описании метода.
Курсор реестра
У реестра событий свой параметр курсора. Отличий два, и оба стоит учесть в общем клиенте:
- длина — до 2048 символов вместо 4096. Если ваш код валидирует длину сам, он должен знать про меньшую границу;
- область — курсор привязан к проекту и полному набору условий запроса
реестра. У двух методов реестра — журнала нарушений и сводки состояния —
границы
fromиtoобязательны, задают непустой период не длиннее 31 дня (fromвключительно,toисключительно) и входят в область курсора. Сдвинули окно — начали новый обход.
limit у реестра общий: от 1 до 100, по умолчанию 50.
Где границы другие
Проверено по спецификации. Всё, чего в таблице нет, использует общие значения из первого раздела.
| Метод | cursor |
limit |
|---|---|---|
| Список сырых событий | до 4096 | 1…100, значения по умолчанию нет — задайте его сами |
| Фрагменты записи сессии | до 4096 | 1…16, по умолчанию 8 |
| Маркеры релизов, токены релизов | до 512 | 1…200, по умолчанию 100 |
| События правила оповещения | до 512 | 1…100, по умолчанию 100 |
| Запуски еженедельной сводки | до 1024 | 1…100, по умолчанию 20 |
| Журнал аудита | до 1024 | 1…200, по умолчанию 50 |
| Доставки защищённой ссылки | до 1024 | 1…100, по умолчанию 50 |
| Методы реестра событий | до 2048 | 1…100, по умолчанию 50 |
| Сохранённые сегменты | ограничение длины в схеме не объявлено | 1…100, по умолчанию 50 |
| Список задач импорта | ограничение длины в схеме не объявлено | 1…100, по умолчанию 50 — значения заданы только в описании параметра, не в схеме |
| Список задач удаления данных субъекта | курсора нет: список ограниченный, а не постраничный | 1…100, по умолчанию 50 |
Отдельный случай — методы, где курсор передаётся в теле POST-запроса, а не
в строке запроса: выгрузка участников когорты retention, отчёт атрибуции,
исторический прогон сезонного оповещения и список тех, кто отвалился на шаге
воронки (там поля называются dropped_cursor и dropped_limit). Правила те
же, отличается только место параметра; у сезонного прогона курсор короче — до
256 символов, а страница — от 1 до 50 элементов, по умолчанию 30.
Типичная ошибка: сохранённый курсор
Курсор выглядит как закладка, и его хочется положить в базу, чтобы «в следующий раз продолжить с того места». Так он не работает.
- Списки упорядочены от новых к старым. Новые строки появляются перед сохранённой позицией, а не после неё, поэтому старый курсор продолжит уводить вас в глубь старых данных и никогда не покажет то, что пришло позже.
- В курсор вложен отпечаток области запроса и версия его собственного формата.
Изменилось хоть что-то — вы получите
422, а не «пустую страницу».
Правильный приём для регулярного дочитывания — не курсор, а явные границы периода. Запоминайте не курсор, а до какого момента вы уже обработали данные, и следующий раз запрашивайте новый период с начала. Курсор живёт внутри одного обхода и там же умирает.
Рабочий пример
Обход списка сырых событий за закрытый период. Останов — по отсутствию
next_cursor, отказ по курсору обрабатывается отдельно.
curl
test -n "$YOUR_ACCESS_TOKEN" || { echo "YOUR_ACCESS_TOKEN is required" >&2; exit 1; }
base="https://YOUR_ACTIONPULSE_HOST/api/v1/projects/YOUR_PROJECT_ID/events"
window="from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&limit=100"
cursor=""
while : ; do
url="$base?$window"
if [ -n "$cursor" ]; then
url="$url&cursor=$(printf '%s' "$cursor" | jq -sRr @uri)"
fi
page=$(curl -sS -w '\n%{http_code}' \
-H "Authorization: Bearer $YOUR_ACCESS_TOKEN" "$url")
code=$(printf '%s\n' "$page" | tail -n 1)
body=$(printf '%s\n' "$page" | sed '$d')
if [ "$code" != "200" ]; then
echo "page failed: $code $(printf '%s' "$body" | jq -r '.error.code')" >&2
exit 1
fi
printf '%s' "$body" | jq -c '.events[]'
cursor=$(printf '%s' "$body" | jq -r '.next_cursor // empty')
[ -n "$cursor" ] || break
doneNode.js
const accessToken = process.env.YOUR_ACCESS_TOKEN;
if (!accessToken) throw new Error('YOUR_ACCESS_TOKEN is required');
const base = 'https://YOUR_ACTIONPULSE_HOST/api/v1/projects/YOUR_PROJECT_ID/events';
// Условия запроса фиксированы на весь обход: менять их между страницами нельзя.
const window = { from: '2026-07-01T00:00:00Z', to: '2026-07-08T00:00:00Z', limit: '100' };
let cursor;
let guard = 0;
do {
const url = new URL(base);
for (const [key, value] of Object.entries(window)) url.searchParams.set(key, value);
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
const { error } = await response.json();
// 422 с details.field === 'cursor' — курсор больше не подходит к области
// запроса; продолжать обход бессмысленно, нужен новый заход с первой страницы.
throw new Error(`page failed: ${response.status} ${error?.code} ${error?.details?.field ?? ''}`);
}
const page = await response.json();
for (const event of page.events) handle(event);
// Единственный признак конца — пустой или отсутствующий next_cursor.
cursor = page.next_cursor || undefined;
guard += 1;
} while (cursor && guard < 1000);Ограничение числа итераций в примере — не требование API, а страховка от бесконечного цикла из-за ошибки в собственном коде.
Что проверить в своём клиенте
- Обход останавливается по отсутствию
next_cursor, а не по короткой странице и не по счётчику. - Условия запроса собираются один раз до начала обхода и не меняются внутри.
422сdetails.field = "cursor"обрабатывается как «начать заново», а не как повторяемая ошибка.limitзадан явно там, где у метода нет значения по умолчанию.- Между страницами есть обработка
429: обход большого списка — это серия запросов, и на неё действуют лимиты частоты. - Для регулярного дочитывания сохраняется граница обработанного периода, а не курсор.
- Если нужен воспроизводимый результат — период закрыт, либо задача решается выгрузкой, а не обходом.