Списки в PayBot листаются курсором, а не номерами страниц. Каждый ответ — это конверт {data: [...], next_cursor: string|null, has_more: bool}: берите next_cursor из ответа и передавайте его в следующий запрос, пока has_more не станет false. Курсор — непрозрачная (opaque) строка, её нельзя парсить или собирать вручную.
Такой подход даёт стабильный обход даже когда данные постоянно добавляются, поэтому в PayBot нет нумерованных страниц и общего счётчика записей — только «Загрузить ещё» или бесконечная прокрутка.
Как устроен курсорный конверт
Любой курсорный endpoint возвращает три поля.
| Поле | Тип | Смысл | |
|---|---|---|---|
data | массив | элементы текущей страницы | |
next_cursor | string \ | null | курсор для следующего запроса |
has_more | bool | есть ли ещё страницы |
{
"data": [
{ "id": "op_7f3c9a12", "amount": 5000, "status": "paid" },
{ "id": "op_6e2b8d01", "amount": 12000, "status": "paid" }
],
"next_cursor": "eyJpZCI6ICJvcF82ZTJiOGQwMSJ9",
"has_more": true
}Признак «есть ещё» — именно has_more, а не next_cursor != null. На практике они согласованы, но опирайтесь на has_more. Сортировка во всех списках фиксирована: created_at DESC, id DESC (сначала новые).
Курсорную пагинацию используют, среди прочих: /v2/payments, /v2/events, /v2/payment-links, /me/webhooks/{id}/deliveries, /partner/clients, /partner/commissions, /partner/balance/transactions, /partner/withdrawals, /partner/v1/clients, /partner/v1/payments.
Как обойти все страницы циклом
Запрашивайте страницы в цикле, подставляя next_cursor, пока has_more == true. Передавайте курсор дословно — не декодируйте и не изменяйте его.
import requests
def fetch_all(url: str, api_key: str, params: dict | None = None) -> list:
items = []
cursor = None
base_params = dict(params or {})
base_params["limit"] = 100
while True:
page_params = dict(base_params)
if cursor:
page_params["cursor"] = cursor
page = requests.get(url, headers={"X-API-Key": api_key}, params=page_params).json()
items.extend(page["data"])
if not page["has_more"]:
break
cursor = page["next_cursor"]
return items
payments = fetch_all(
"https://api.paybot.kz/v2/payments",
"kp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
{"status": "paid"},
)
print(len(payments))Параметр limit — от 1 до 100 (по умолчанию 20 для /v2/payments и /v2/events, 25 для /v2/payment-links). Значение больше 100 сервер не примет.
Почему не offset-пагинация
Смещение (offset/page) ломается на живых данных: пока пользователь листает, в начало списка добавляются новые записи, всё сдвигается — и вы либо видите дубли, либо пропускаете элементы. Курсор указывает на конкретную позицию в фиксированной сортировке, поэтому обход остаётся консистентным независимо от новых записей.
Практические следствия для UI:
- •Не рисуйте пагинатор с номерами страниц и не показывайте «стр. 3 из 12» — общего числа страниц нет.
- •Не показывайте «Найдено N записей» — в курсорном конверте нет
total. - •Стройте интерфейс вокруг кнопки «Загрузить ещё» или бесконечного скролла по
has_more.
Часть списков возвращается без курсора, с обычным limit: например /me/payments (≤100), /me/logs (по умолчанию 50, потолок 200), /me/notifications (≤100), /me/team, /me/kaspi/accounts, /me/webhooks. Полная карта endpoint'ов — в справочнике API.
FAQ
Как понять, что страницы закончились?
Смотрите на has_more. Когда он false — вы дошли до конца, next_cursor при этом будет null. Не завязывайтесь на проверку next_cursor != null как на основной сигнал, хотя обычно она совпадает с has_more.
Можно ли перескочить на произвольную страницу?
Нет. Курсор описывает последовательный обход от новых записей к старым; произвольный переход «на страницу 5» не поддерживается по устройству модели. Для навигации используйте фильтры (status, type), чтобы сузить выборку.
Что делать с курсором — декодировать?
Ничего. Курсор — непрозрачная base64-строка, её формат может меняться. Передавайте next_cursor в следующий запрос ровно как получили, без парсинга.
Смежные разделы
- •Журнал событий — пример курсорной ленты.
- •Справочник API — какие списки курсорные, а какие — с обычным limit.