ApiPay API v2 Documentation — ApiPay.kz

Полная документация ApiPay REST API v2 — Интернет-эквайринг через Kaspi Pay (Phone Payments)

REST API для приёма платежей по номеру телефона через Kaspi Pay. Без договора с банком, без комиссий. Поддержка каталога, подписок, возвратов и webhooks.

Быстрый старт

Песочница доступна сразу! Вы можете создавать тестовые счета (is_sandbox=true) до подключения Kaspi Business.
  1. Войдите в личный кабинет apipay.kz/login через WhatsApp
  2. Получите API ключ в Настройки → Подключение
  3. Создавайте счета: POST /api/v1/invoices
  4. Для работы с реальными платежами — напишите в WhatsApp поддержки, мы подключим ваш Kaspi Business с правами Кассира
  5. Настройте webhook в личном кабинете для получения уведомлений об оплате

Базовая конфигурация

ПараметрЗначение
Base URLhttps://bpapi.bazarbay.site/api/v1
АутентификацияHeader X-API-Key: ваш_api_ключ
Rate Limit60 запросов/минуту
Content-Typeapplication/json

Обзор эндпоинтов (23 шт.)

#МетодПутьОписание
Health
1GET/statusПроверка доступности (без auth)
Счета (Invoices)
2POST/invoicesСоздать счёт (с корзиной или без)
3GET/invoicesСписок счетов
4GET/invoices/{id}Получить счёт
5POST/invoices/{id}/cancelОтменить счёт
6POST/invoices/{id}/refundВозврат по счёту
7GET/invoices/{id}/refundsВозвраты по счёту
8POST/invoices/status/checkМассовая проверка статусов
Возвраты (Refunds)
9GET/refundsСписок всех возвратов
Каталог (Catalog)
10GET/catalog/unitsЕдиницы измерения
11GET/catalogСписок товаров каталога
12POST/catalog/upload-imageЗагрузить изображение
13POST/catalogСоздать товары (batch)
14PATCH/catalog/{id}Обновить товар
15DELETE/catalog/{id}Удалить товар
Подписки (Subscriptions)
16POST/subscriptionsСоздать подписку
17GET/subscriptionsСписок подписок
18GET/subscriptions/{id}Получить подписку
19PUT/subscriptions/{id}Обновить подписку
20POST/subscriptions/{id}/pauseПоставить на паузу
21POST/subscriptions/{id}/resumeВозобновить
22POST/subscriptions/{id}/cancelОтменить подписку
23GET/subscriptions/{id}/invoicesСчета подписки

Health Check

GET /status

Проверка доступности API. Не требует аутентификации.

Ответ (200 OK)

{
  "status": "ok",
  "timestamp": "2026-02-12T10:00:00+05:00"
}

Счета (Invoices)

POST /invoices

Создать счёт на оплату. Два режима: без корзины (передать amount) или с корзиной (передать cart_items, сумма рассчитывается автоматически).

Запрос (без корзины)

{
  "amount": 10000,
  "phone_number": "87001234567",
  "description": "Оплата заказа #123",
  "external_order_id": "order_123"
}

Запрос (с корзиной — для организаций с каталогом)

{
  "phone_number": "87001234567",
  "description": "Оплата заказа #123",
  "cart_items": [
    { "catalog_item_id": 1, "count": 2, "price": 4500.00 },
    { "catalog_item_id": 5, "count": 1, "discount": 500 }
  ],
  "discount_percentage": 10
}

Параметры

ПолеТипОбязательноОписание
amountnumberДа (без корзины)Сумма 0.01 - 99999999.99. Игнорируется при cart_items.
phone_numberstringДаТелефон клиента, формат: 8XXXXXXXXXX
descriptionstringНетМакс 500 символов
external_order_idstringНетВаш ID заказа, макс 255 символов
cart_itemsarrayНетТовары корзины (только для организаций с каталогом)
discount_percentagenumberНетГлобальный % скидки (0-100). Применяется к позициям без явного discount. Per-item discount имеет приоритет.

Поля cart_items

ПолеТипОбязательноОписание
catalog_item_idintegerДаID товара из каталога (поле id из GET /catalog)
countintegerДаКоличество, мин 1
pricenumberНетКастомная цена (0.01 - 99999999.99). Заменяет каталожную цену для этой позиции.
discountnumberНетСкидка на строку товара (мин 0). Применяется к итогу строки (price × count).
Рекомендация: Перед созданием счёта с корзиной получите актуальный список товаров через GET /catalog, чтобы использовать корректные catalog_item_id.

Ответ (201 Created)

{
  "id": 124,
  "amount": "4550.00",
  "status": "pending",
  "subtotal": "5000.00",
  "discount_sum": "450.00",
  "discount_percentage": "10",
  "created_at": "2026-02-12T10:35:00+05:00"
}
Примечание: Поля subtotal, discount_sum и discount_percentage появляются только при наличии скидки (обратная совместимость).

GET /invoices

Список счетов с фильтрацией, сортировкой и пагинацией.

Query параметры

ПараметрТипПо умолчаниюОписание
pageinteger1Номер страницы
per_pageinteger10Записей на странице (1-100)
searchstringПоиск по ID или описанию (макс 100)
status[]arrayФильтр: pending, paid, cancelled, expired
date_fromstringДата от (YYYY-MM-DD)
date_tostringДата до (YYYY-MM-DD)
sort_bystringcreated_atПоле: id, amount, client_name, status, created_at
sort_orderstringdescasc или desc

Ответ (200 OK)

{
  "current_page": 1,
  "data": [
    {
      "id": 124,
      "user_id": 5,
      "organization_id": 3,
      "api_key_id": 2,
      "amount": "5000.00",
      "phone_number": "87001234567",
      "description": "Оплата заказа #123",
      "external_order_id": "order-123",
      "status": "paid",
      "paid_at": "2026-02-12T10:36:00+05:00",
      "created_at": "2026-02-12T10:35:00+05:00"
    }
  ],
  "total": 100
}
Примечание: Список использует плоскую пагинацию (current_page, total на верхнем уровне — без обёртки meta). Полные данные счёта (в т.ч. client_name, items, поля возвратов) доступны через GET /invoices/{id}.

GET /invoices/{id}

Получить счёт по ID.

Ответ

{
  "id": 124,
  "amount": "5000.00",
  "phone_number": "87001234567",
  "description": "Payment for order #123",
  "external_order_id": "order-123",
  "status": "paid",
  "client_name": "Ivan I.",
  "client_comment": null,
  "is_sandbox": false,
  "total_refunded": "0.00",
  "is_fully_refunded": false,
  "kaspi_invoice_id": "13234689513",
  "items": [
    {
      "id": 1,
      "invoice_id": 124,
      "catalog_item_id": 42,
      "name": "Americano",
      "price": "800.00",
      "original_price": "900.00",
      "count": 2,
      "discount": null,
      "unit_id": 1
    },
    {
      "id": 2,
      "invoice_id": 124,
      "catalog_item_id": 43,
      "name": "Latte",
      "price": "1200.50",
      "original_price": null,
      "count": 1,
      "discount": "500.00",
      "unit_id": 1
    }
  ],
  "paid_at": "2026-02-12T10:36:00+05:00",
  "created_at": "2026-02-12T10:35:00+05:00"
}

Поля items

ПолеТипОписание
idintegerID позиции
invoice_idintegerID счёта
catalog_item_idinteger|nullID товара из каталога. null если товар удалён
namestringНазвание товара (snapshot)
pricestringЦена за единицу (snapshot)
countintegerКоличество
unit_idintegerID единицы измерения
original_pricestring|nullКаталожная цена, если переопределена кастомной price. null если использована каталожная цена.
discountstring|nullСкидка на строку. null если скидки нет.
Примечание: Поле items содержит snapshot товаров на момент создания счёта. Пустой массив [] — организация без каталога или старый счёт. Если catalog_item_id: null — товар был удалён из каталога, но данные (name, price, count, unit_id) сохранены.

POST /invoices/{id}/cancel

Отменить счёт. Только для статуса pending. Может вернуть 202 Accepted — в этом случае счёт переходит в статус cancelling, используйте GET /invoices/{id} для проверки.

Ответ (200 OK / 202 Accepted)

{
  "message": "Invoice cancelled successfully",
  "invoice": {
    "id": 124,
    "status": "cancelled"
  }
}
Асинхронная отмена: Если сервер вернул 202 Accepted, счёт переходит в статус cancelling. Используйте GET /invoices/{id} для поллинга до финального статуса cancelled.

POST /invoices/{id}/refund

Создать полный или частичный возврат. Без amount — полный возврат. Для счетов с товарами используйте return_items для возврата конкретных позиций.

Запрос

{
  "amount": 2000.00,
  "reason": "Запрос клиента",
  "return_items": [
    { "catalog_item_id": 1, "count": 1 }
  ]
}

Параметры

ПолеТипОбязательноОписание
amountnumberНетСумма возврата (0.01 - 99999999.99). Без параметра — полный возврат.
reasonstringНетПричина возврата, макс 500 символов
return_itemsarrayНетКонкретные позиции для возврата. Формат: { catalog_item_id, count }

Ответ (201 Created)

{
  "message": "Refund created and queued for processing",
  "refund": {
    "id": 456,
    "invoice_id": 124,
    "amount": "2000.00",
    "reason": "Запрос клиента",
    "status": "pending",
    "created_at": "2026-02-12T11:00:00+05:00"
  },
  "invoice": {
    "id": 124,
    "amount": "5000.00",
    "total_refunded": "0.00",
    "available_for_refund": 5000,
    "pending_refund_amount": 2000
  }
}

GET /invoices/{id}/refunds

Список возвратов по конкретному счёту с информацией о счёте.

Ответ (200 OK)

{
  "invoice": {
    "id": 124,
    "amount": "5000.00",
    "total_refunded": "2000.00",
    "available_for_refund": 3000,
    "is_fully_refunded": false
  },
  "refunds": [
    {
      "id": 456,
      "invoice_id": 124,
      "amount": "2000.00",
      "status": "completed",
      "reason": "Запрос клиента",
      "items": null,
      "created_at": "2026-02-12T11:00:00+05:00"
    }
  ],
  "total": 1
}

POST /invoices/status/check

Массовая проверка статусов нескольких счетов за один запрос. Полезно для синхронизации состояния в вашей системе.

Запрос

{
  "invoice_ids": [124, 125, 126]
}

Параметры

ПолеТипОбязательноОписание
invoice_idsarray of integersДаID счетов для проверки (макс 100)

Ответ (200 OK)

{
  "message": "Status check jobs dispatched",
  "count": 3
}
Примечание: Этот эндпоинт ставит фоновые задачи на обновление статусов. Он не возвращает актуальные статусы напрямую — используйте GET /invoices/{id} для проверки отдельного счёта после запуска обновления.

Статусы счетов

СтатусОписаниеМожно отменитьМожно вернуть
pendingОжидает оплатыДаНет
cancellingВ процессе отмены (async)НетНет
paidОплачен клиентомНетДа
cancelledОтменёнНетНет
expiredИстёк срок оплатыНетНет
partially_refundedЧастично возвращёнНетДа (остаток)
refundedПолностью возвращёнНетНет

Каталог (Catalog)

Управление каталогом товаров. Доступно только для организаций с включённым каталогом (has_catalog = true).

GET /catalog/units

Получить список доступных единиц измерения для товаров каталога.

Рекомендация: Получите список единиц измерения перед созданием товаров, чтобы использовать корректные unit_id.

Ответ (200 OK)

{
  "data": [
    { "id": 1, "name": "шт.", "name_kaz": "дн." },
    { "id": 2, "name": "кг", "name_kaz": "кг" },
    { "id": 3, "name": "литр", "name_kaz": "литр" },
    { "id": 4, "name": "метр", "name_kaz": "метр" },
    { "id": 5, "name": "грамм", "name_kaz": "грамм" },
    { "id": 37, "name": "м2", "name_kaz": "м2" },
    { "id": 38, "name": "сутки", "name_kaz": "тәулік" },
    { "id": 43, "name": "упак.", "name_kaz": "жиынтық" },
    { "id": 44, "name": "час", "name_kaz": "сағат" }
  ]
}

Описание полей

ПолеТипОписание
idintegerID единицы измерения (используется в unit_id при создании товара)
namestringНазвание на русском языке
name_kazstringНазвание на казахском языке

GET /catalog

Список товаров каталога с поиском и фильтрацией.

Query параметры

ПараметрТипПо умолчаниюОписание
pageinteger1Номер страницы
per_pageinteger50Записей на странице (1-200)
searchstringПоиск по названию
barcodestringФильтр по штрихкоду
first_charstringФильтр по первой букве

Ответ

{
  "data": [
    {
      "id": 1,
      "kaspi_item_id": 12345,
      "name": "Coffee Latte",
      "unit_id": 1,
      "selling_price": 1800,
      "image_url": "https://cdn.kaspi.kz/image.jpg",
      "barcode": "4870000123456",
      "status": "active",
      "synced_at": "2026-02-12T10:00:00+05:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "total": 50,
    "per_page": 50
  }
}

Описание полей товара

ПолеТипОписание
idintegerВнутренний ID товара в ApiPay
kaspi_item_idintegerID товара в Kaspi (назначается после синхронизации)
namestringНазвание товара
unit_idintegerID единицы измерения (см. GET /catalog/units)
selling_pricenumberЦена продажи
image_urlstring|nullURL изображения товара
barcodestring|nullШтрихкод (EAN-13)
statusstringСтатус: active, pending, failed, deleting, deleted
synced_atstring|nullДата последней синхронизации с Kaspi

POST /catalog/upload-image

Загрузить изображение для товара. Формат: multipart/form-data. Макс 10 МБ (jpg, png, gif, webp). Изображения оптимизируются (512x512 PNG) и дедуплицируются по MD5.

Ответ

{
  "image_id": "abc12345-def6-7890-ghij-klmnopqrstuv"
}

Используйте image_id при создании или обновлении товара.

POST /catalog

Создать один или несколько товаров (batch, 1-50 штук).

Запрос

{
  "items": [
    {
      "name": "Coffee Latte",
      "selling_price": 1800,
      "unit_id": 1,
      "image_id": "abc12345-def6-7890-ghij-klmnopqrstuv"
    }
  ]
}

Параметры items

ПолеТипОбязательноОписание
namestringДаНазвание товара, макс 255
selling_pricenumberДаЦена, мин 0.01
unit_idintegerДаID единицы измерения (из GET /catalog/units)
image_idstring (uuid)НетID изображения из upload-image

Ответ (202 Accepted)

{
  "data": [
    {
      "id": 101,
      "name": "Coffee Latte",
      "selling_price": 1800,
      "unit_id": 1,
      "barcode": null,
      "status": "pending"
    }
  ]
}
Асинхронная операция: Товары создаются со статусом pending и синхронизируются с Kaspi в фоне. Используйте GET /catalog для проверки финального статуса (active после успешной синхронизации).

PATCH /catalog/{id}

Обновить товар каталога. Все поля опциональны — обновляются только переданные.

Запрос

{
  "name": "Updated Name",
  "selling_price": 2000,
  "unit_id": 1,
  "image_id": "abc12345-def6-7890-ghij-klmnopqrstuv",
  "is_image_deleted": false
}

Параметры

ПолеТипОбязательноОписание
namestringНетНовое название, макс 255
selling_pricenumberНетНовая цена, мин 0.01
unit_idintegerНетID единицы измерения (из GET /catalog/units)
image_idstring (uuid)НетНовое изображение из upload-image
is_image_deletedbooleanНетУдалить текущее изображение

Ответ (200 OK)

{
  "message": "Catalog item update queued",
  "catalog_item_id": 1
}

DELETE /catalog/{id}

Удалить товар каталога.

Ответ

{
  "message": "Catalog item deletion queued",
  "catalog_item_id": 1
}

Подписки (Subscriptions)

Автоматическое создание повторяющихся счетов по расписанию. Поддержка grace period и retry при неоплате.

POST /subscriptions

Создать подписку. Два режима: без корзины (передать amount) или с корзиной (передать cart_items, сумма рассчитывается автоматически). Для организаций с каталогом cart_items обязателен.

Запрос (без корзины)

{
  "phone_number": "87001234567",
  "amount": 5000,
  "billing_period": "monthly",
  "billing_day": 15,
  "description": "Ежемесячная подписка",
  "subscriber_name": "Иван Иванов",
  "external_subscriber_id": "cust-123",
  "started_at": "2026-03-01",
  "max_retry_attempts": 3,
  "retry_interval_hours": 24,
  "grace_period_days": 7,
  "metadata": { "plan": "premium" }
}

Запрос (с корзиной — для организаций с каталогом)

{
  "phone_number": "87001234567",
  "billing_period": "monthly",
  "billing_day": 15,
  "description": "Ежемесячная подписка",
  "subscriber_name": "Иван Иванов",
  "cart_items": [
    { "catalog_item_id": 1, "count": 2 },
    { "catalog_item_id": 5, "count": 1 }
  ]
}

Параметры

ПолеТипОбязательноОписание
phone_numberstringДаФормат: 8XXXXXXXXXX
amountnumberДа (без корзины)100 - 1 000 000. Игнорируется при cart_items.
billing_periodstringДаdaily, weekly, biweekly, monthly, quarterly, yearly
billing_dayintegerНетДень периода (1-28)
descriptionstringНетМакс 255
subscriber_namestringНетИмя подписчика, макс 255
external_subscriber_idstringНетВаш ID подписчика, макс 255
started_atdateНетДата начала (по умолчанию сегодня)
max_retry_attemptsintegerНетМакс попыток повтора (1-10)
retry_interval_hoursintegerНетИнтервал между попытками, часы (1-168)
grace_period_daysintegerНетЛьготный период, дни (1-30)
metadataobjectНетПроизвольные данные JSON
cart_itemsarrayНетТовары корзины (только для организаций с каталогом). При передаче amount игнорируется.

Поля cart_items

ПолеТипОбязательноОписание
catalog_item_idintegerДаID товара из каталога (поле id из GET /catalog)
countintegerДаКоличество, мин 1
Рекомендация: Перед созданием подписки с корзиной получите актуальный список товаров через GET /catalog, чтобы использовать корректные catalog_item_id. При каждом биллинге цены пересчитываются из актуального каталога.

Ответ (201 Created)

{
  "message": "Subscription created",
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "5000.00",
    "cart_items": null,
    "billing_period": "monthly",
    "billing_period_label": "Ежемесячно",
    "billing_day": 15,
    "billing_day_label": "15-го числа",
    "description": "Ежемесячная подписка",
    "external_subscriber_id": "cust-123",
    "status": "active",
    "status_label": "Активна",
    "status_color": "green",
    "started_at": "2026-02-12T00:00:00+05:00",
    "next_billing_at": "2026-03-15T00:00:00+05:00",
    "next_billing_in_days": 31,
    "next_billing_label": "через 31 день",
    "paused_at": null,
    "cancelled_at": null,
    "failed_attempts": 0,
    "max_retry_attempts": 3,
    "retry_interval_hours": 24,
    "grace_period_days": 7,
    "in_grace_period": false,
    "is_sandbox": false,
    "metadata": { "plan": "premium" },
    "created_at": "2026-02-12T12:00:00+05:00",
    "updated_at": "2026-02-12T12:00:00+05:00"
  }
}

GET /subscriptions

Список подписок с фильтрацией и пагинацией.

Query параметры

ПараметрТипПо умолчаниюОписание
pageinteger1Номер страницы
per_pageinteger10Записей на странице (1-100)
statusstringФильтр: active, paused, cancelled, expired
phone_numberstringФильтр по номеру телефона
external_subscriber_idstringФильтр по вашему ID подписчика

Ответ (200 OK)

{
  "current_page": 1,
  "data": [
    {
      "id": 1,
      "subscriber_name": "Иван Иванов",
      "phone_number": "87001234567",
      "amount": "5000.00",
      "cart_items": null,
      "billing_period": "monthly",
      "billing_period_label": "Ежемесячно",
      "billing_day": 15,
      "billing_day_label": "15-го числа",
      "description": "Ежемесячная подписка",
      "external_subscriber_id": "cust-123",
      "status": "active",
      "status_label": "Активна",
      "status_color": "green",
      "started_at": "2026-02-01T00:00:00+05:00",
      "next_billing_at": "2026-03-15T00:00:00+05:00",
      "next_billing_in_days": 17,
      "next_billing_label": "через 17 дней",
      "paused_at": null,
      "cancelled_at": null,
      "failed_attempts": 0,
      "max_retry_attempts": 3,
      "retry_interval_hours": 24,
      "grace_period_days": 7,
      "in_grace_period": false,
      "is_sandbox": false,
      "metadata": null,
      "created_at": "2026-02-12T12:00:00+05:00",
      "updated_at": "2026-02-12T12:00:00+05:00"
    }
  ],
  "total": 10
}

GET /subscriptions/{id}

Получить подписку со статистикой и информацией о последнем платеже.

Ответ (200 OK)

{
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "5000.00",
    "cart_items": null,
    "billing_period": "monthly",
    "billing_period_label": "Ежемесячно",
    "billing_day": 15,
    "billing_day_label": "15-го числа",
    "description": "Ежемесячная подписка",
    "status": "active",
    "status_label": "Активна",
    "status_color": "green",
    "external_subscriber_id": "cust-123",
    "started_at": "2026-02-01T00:00:00+05:00",
    "next_billing_at": "2026-03-15T00:00:00+05:00",
    "next_billing_in_days": 17,
    "next_billing_label": "через 17 дней",
    "paused_at": null,
    "cancelled_at": null,
    "max_retry_attempts": 3,
    "retry_interval_hours": 24,
    "grace_period_days": 7,
    "failed_attempts": 0,
    "in_grace_period": false,
    "is_sandbox": false,
    "metadata": { "plan": "premium" },
    "stats": {
      "total_payments": 5,
      "successful_payments": 4,
      "failed_payments": 1,
      "total_amount": "20000.00"
    },
    "last_payment": {
      "id": 200,
      "status": "paid",
      "amount": "5000.00",
      "paid_at": "2026-02-15T12:00:00+05:00"
    },
    "created_at": "2026-02-01T12:00:00+05:00",
    "updated_at": "2026-02-15T12:00:00+05:00"
  }
}

Описание полей

ПолеТипОписание
idintegerID подписки
subscriber_namestring|nullИмя подписчика
phone_numberstringТелефон подписчика
amountstringСумма списания
billing_periodstringПериод: daily, weekly, biweekly, monthly, quarterly, yearly
billing_dayinteger|nullДень списания
statusstringСтатус подписки
next_billing_atstring|nullДата следующего списания
failed_attemptsintegerКоличество неудачных попыток оплаты
in_grace_periodbooleanНаходится ли в льготном периоде
statsobjectСтатистика: total_payments, successful_payments, failed_payments, total_amount
last_paymentobject|nullПоследний платёж подписки
metadataobject|nullПроизвольные данные

PUT /subscriptions/{id}

Обновить подписку. Все поля опциональны — обновляются только переданные. При передаче cart_items сумма пересчитывается автоматически.

Запрос

{
  "amount": 7000,
  "billing_day": 20,
  "description": "Обновлённая подписка",
  "subscriber_name": "Иван Иванов",
  "max_retry_attempts": 5,
  "retry_interval_hours": 12,
  "grace_period_days": 14,
  "metadata": { "plan": "enterprise" }
}

Параметры

ПолеТипОбязательноОписание
amountnumberНетНовая сумма (100 - 1 000 000)
billing_dayintegerНетДень периода (1-28)
descriptionstringНетОписание, макс 255
subscriber_namestringНетИмя подписчика, макс 255
max_retry_attemptsintegerНетМакс попыток повтора (1-10)
retry_interval_hoursintegerНетИнтервал между попытками (1-168)
grace_period_daysintegerНетЛьготный период, дни (1-30)
metadataobjectНетПроизвольные данные JSON
cart_itemsarrayНетНовая корзина товаров (формат как при создании). Сумма пересчитывается автоматически.

Ответ (200 OK)

{
  "message": "Subscription updated",
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "7000.00",
    "cart_items": null,
    "billing_period": "monthly",
    "billing_period_label": "Ежемесячно",
    "billing_day": 20,
    "billing_day_label": "20-го числа",
    "description": "Обновлённая подписка",
    "status": "active",
    "status_label": "Активна",
    "status_color": "green",
    "next_billing_at": "2026-03-20T00:00:00+05:00",
    "next_billing_in_days": 22,
    "paused_at": null,
    "cancelled_at": null,
    "failed_attempts": 0,
    "max_retry_attempts": 5,
    "retry_interval_hours": 12,
    "grace_period_days": 14,
    "in_grace_period": false,
    "is_sandbox": false,
    "metadata": { "plan": "enterprise" },
    "created_at": "2026-02-12T12:00:00+05:00",
    "updated_at": "2026-02-26T10:00:00+05:00"
  }
}

POST /subscriptions/{id}/pause

Поставить подписку на паузу. Только для статуса active.

Ответ (200 OK)

{
  "message": "Subscription paused",
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "5000.00",
    "billing_period": "monthly",
    "billing_day": 15,
    "status": "paused",
    "status_label": "На паузе",
    "status_color": "orange",
    "next_billing_at": null,
    "paused_at": "2026-02-26T10:00:00+05:00",
    "cancelled_at": null,
    "updated_at": "2026-02-26T10:00:00+05:00"
  }
}

POST /subscriptions/{id}/resume

Возобновить подписку после паузы. Только для статуса paused.

Ответ (200 OK)

{
  "message": "Subscription resumed",
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "5000.00",
    "billing_period": "monthly",
    "billing_day": 15,
    "status": "active",
    "status_label": "Активна",
    "status_color": "green",
    "next_billing_at": "2026-03-15T00:00:00+05:00",
    "paused_at": null,
    "cancelled_at": null,
    "updated_at": "2026-02-26T10:00:00+05:00"
  }
}

POST /subscriptions/{id}/cancel

Отменить подписку навсегда. Нельзя восстановить.

Внимание: Отмена подписки необратима. Создайте новую подписку, если потребуется возобновить списания.

Ответ (200 OK)

{
  "message": "Subscription cancelled",
  "subscription": {
    "id": 1,
    "subscriber_name": "Иван Иванов",
    "phone_number": "87001234567",
    "amount": "5000.00",
    "billing_period": "monthly",
    "billing_day": 15,
    "status": "cancelled",
    "status_label": "Отменена",
    "status_color": "red",
    "next_billing_at": null,
    "paused_at": null,
    "cancelled_at": "2026-02-26T10:00:00+05:00",
    "updated_at": "2026-02-26T10:00:00+05:00"
  }
}

GET /subscriptions/{id}/invoices

Список счетов, созданных подпиской.

Query параметры

ПараметрТипПо умолчаниюОписание
pageinteger1Номер страницы
per_pageinteger10Записей на странице (1-100)

Ответ (200 OK)

{
  "data": [
    {
      "id": 1,
      "invoice_id": 200,
      "billing_period_start": "2026-02-01",
      "billing_period_end": "2026-02-28",
      "billing_period_label": "01.02.2026 — 28.02.2026",
      "amount": "5000.00",
      "attempt_number": 1,
      "status": "paid",
      "status_label": "Оплачен",
      "status_color": "green",
      "paid_at": "2026-02-15T12:00:00+05:00",
      "failure_reason": null,
      "invoice": {
        "id": 200,
        "kaspi_invoice_id": "13234689513",
        "status": "paid"
      },
      "created_at": "2026-02-15T00:00:00+05:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "total": 5,
    "per_page": 20
  }
}
Примечание: Каждый элемент — это запись subscription_invoice (попытка биллинга), а не обычный счёт. Содержит период биллинга, номер попытки и вложенный объект invoice с данными реального счёта Kaspi.

Статусы подписок

СтатусОписание
activeСписания по расписанию
pausedВременно приостановлена
cancelledОтменена навсегда
expiredИстекла (после grace period)

Периоды списания

ПериодОписание
dailyКаждый день
weeklyКаждую неделю
biweeklyКаждые 2 недели
monthlyКаждый месяц
quarterlyКаждые 3 месяца
yearlyКаждый год

Grace Period (льготный период)

Если клиент не оплатил счёт и все retry-попытки исчерпаны:

  1. Подписка переходит в grace period (in_grace_period: true)
  2. Если оплата поступит в течение grace_period_days — подписка продолжает работать
  3. Если нет — подписка переходит в статус expired

Возвраты (Refunds)

Полные и частичные возвраты по оплаченным счетам.

GET /refunds

Список всех возвратов с фильтрацией и пагинацией.

Query параметры

ПараметрТипПо умолчаниюОписание
pageinteger1Номер страницы
per_pageinteger10Записей на странице (1-100)
status[]arrayФильтр: pending, processing, completed, failed
invoice_idintegerФильтр по ID счёта
date_fromstringДата от (YYYY-MM-DD)
date_tostringДата до (YYYY-MM-DD)

Ответ (200 OK)

{
  "current_page": 1,
  "data": [
    {
      "id": 456,
      "invoice_id": 124,
      "amount": "2000.00",
      "reason": "Запрос клиента",
      "status": "completed",
      "created_at": "2026-02-12T11:00:00+05:00"
    }
  ],
  "total": 3
}

Описание полей возврата

ПолеТипОписание
idintegerID возврата
invoice_idintegerID счёта, по которому сделан возврат
amountstringСумма возврата
reasonstring|nullПричина возврата
statusstringСтатус: pending, processing, completed, failed
created_atstringДата создания

Статусы возвратов

СтатусОписание
pendingСоздан, в очереди на обработку
processingОбрабатывается Kaspi
completedУспешно возвращён
failedОшибка возврата

Webhooks

Webhooks настраиваются в личном кабинете ApiPay.kz (раздел Настройки → Подключение). При создании webhook вы получите secret (показывается один раз). Используйте его для верификации подписи входящих уведомлений.

События

СобытиеОписание
invoice.status_changedИзменение статуса счёта (paid, cancelled, expired)
invoice.refundedСоздан возврат по оплаченному счёту
subscription.payment_succeededУспешный платёж по подписке
subscription.payment_failedНеудачный платёж по подписке
subscription.grace_period_startedНачался льготный период подписки
subscription.expiredПодписка истекла (после grace period)
webhook.testТестовый webhook

invoice.status_changed

{
  "event": "invoice.status_changed",
  "invoice": {
    "id": 42,
    "external_order_id": "order_123",
    "amount": "15000.00",
    "subtotal": "16500.00",
    "discount_sum": "1500.00",
    "discount_percentage": "10",
    "status": "paid",
    "description": "Оплата заказа",
    "kaspi_invoice_id": "13234689513",
    "client_name": "Иван Иванов",
    "client_phone": "87071234567",
    "paid_at": "2026-02-12T14:35:00+05:00"
  },
  "source": "My API Key",
  "timestamp": "2026-02-12T14:35:01+05:00"
}

invoice.refunded

{
  "event": "invoice.refunded",
  "refund": {
    "id": 5,
    "amount": "2000.00",
    "status": "completed",
    "reason": "Возврат товара",
    "created_at": "2026-02-12T10:00:00+05:00"
  },
  "invoice": {
    "id": 42,
    "external_order_id": "order_123",
    "amount": "5000.00",
    "subtotal": "5500.00",
    "discount_sum": "500.00",
    "total_refunded": "2000.00",
    "available_for_refund": "3000.00",
    "is_fully_refunded": false,
    "status": "paid",
    "kaspi_invoice_id": "13234689513"
  },
  "source": "My API Key",
  "timestamp": "2026-02-12T10:00:01+05:00"
}
Примечание: Поля subtotal, discount_sum и discount_percentage появляются в webhook payloads только при наличии скидки в счёте.

subscription.payment_succeeded

{
  "event": "subscription.payment_succeeded",
  "subscription": {
    "id": 10,
    "external_subscriber_id": "CLIENT-001",
    "phone_number": "87071234567",
    "subscriber_name": "Иван Иванов",
    "amount": "5000.00",
    "billing_period": "monthly",
    "status": "active",
    "next_billing_at": "2026-03-01T00:00:00+05:00",
    "failed_attempts": 0,
    "in_grace_period": false
  },
  "invoice_id": 200,
  "amount": "5000.00",
  "paid_at": "2026-02-01T12:00:00+05:00",
  "source": "My API Key",
  "timestamp": "2026-02-01T12:00:01+05:00"
}

subscription.payment_failed

{
  "event": "subscription.payment_failed",
  "subscription": {
    "id": 10,
    "phone_number": "87071234567",
    "amount": "5000.00",
    "billing_period": "monthly",
    "status": "active",
    "failed_attempts": 2,
    "in_grace_period": false
  },
  "invoice_id": 201,
  "amount": "5000.00",
  "reason": "Invoice expired",
  "attempt_number": 2,
  "source": "My API Key",
  "timestamp": "2026-02-02T12:00:01+05:00"
}

subscription.grace_period_started

{
  "event": "subscription.grace_period_started",
  "subscription": {
    "id": 10,
    "phone_number": "87071234567",
    "amount": "5000.00",
    "status": "active",
    "failed_attempts": 3,
    "in_grace_period": true
  },
  "grace_period_days": 3,
  "expires_at": "2026-02-05T12:00:00+05:00",
  "source": "My API Key",
  "timestamp": "2026-02-02T12:00:01+05:00"
}

subscription.expired

{
  "event": "subscription.expired",
  "subscription": {
    "id": 10,
    "phone_number": "87071234567",
    "amount": "5000.00",
    "status": "expired",
    "next_billing_at": null,
    "failed_attempts": 3,
    "in_grace_period": false
  },
  "source": "My API Key",
  "timestamp": "2026-02-05T12:00:01+05:00"
}

Поле source

Все webhook payload содержат поле source — имя API-ключа, создавшего ресурс (счёт или подписку). Может быть null.

Политика повторов

Верификация подписи

Заголовок: X-Webhook-Signature: sha256=<HMAC-SHA256>

Node.js

const crypto = require('crypto')

function verifyWebhook(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

PHP

function verifyWebhook($payload, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

Python

import hmac, hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Примеры кода

Создание счёта

JavaScript/Node.js

const response = await fetch('https://bpapi.bazarbay.site/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: 10000,
    phone_number: '87001234567',
    description: 'Payment for order #123'
  })
})
const data = await response.json()
console.log('Invoice created:', data.id)

Python

import requests

response = requests.post(
    'https://bpapi.bazarbay.site/api/v1/invoices',
    headers={'X-API-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},
    json={'amount': 10000, 'phone_number': '87001234567'}
)
data = response.json()
print(f"Invoice created: {data['id']}")

PHP

$ch = curl_init('https://bpapi.bazarbay.site/api/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['X-API-Key: YOUR_API_KEY', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['amount' => 10000, 'phone_number' => '87001234567']),
    CURLOPT_RETURNTRANSFER => true
]);
$response = json_decode(curl_exec($ch), true);
echo "Invoice created: " . $response['id'];

cURL

curl -X POST https://bpapi.bazarbay.site/api/v1/invoices \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 10000, "phone_number": "87001234567"}'

Создание счёта с корзиной

JavaScript/Node.js

const response = await fetch('https://bpapi.bazarbay.site/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    phone_number: '87001234567',
    description: 'Cart order',
    cart_items: [
      { catalog_item_id: 1, count: 2 },
      { catalog_item_id: 5, count: 3 }
    ]
  })
})

Создание подписки

cURL

curl -X POST https://bpapi.bazarbay.site/api/v1/subscriptions \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "87001234567",
    "amount": 5000,
    "billing_period": "monthly",
    "description": "Monthly subscription"
  }'

JavaScript/Node.js

const response = await fetch('https://bpapi.bazarbay.site/api/v1/subscriptions', {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    phone_number: '87001234567',
    amount: 5000,
    billing_period: 'monthly',
    description: 'Monthly subscription'
  })
})
const data = await response.json()
console.log('Subscription:', data.subscription.id)

Загрузка изображения + создание товара

cURL

# Шаг 1: Загрузить изображение
IMAGE_ID=$(curl -s -X POST https://bpapi.bazarbay.site/api/v1/catalog/upload-image \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "image=@product.jpg" | jq -r '.image_id')

# Шаг 2: Создать товар с изображением
curl -X POST https://bpapi.bazarbay.site/api/v1/catalog \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"items\": [{
      \"name\": \"Coffee Latte\",
      \"selling_price\": 1800,
      \"unit_id\": 1,
      \"image_id\": \"$IMAGE_ID\"
    }]
  }"

Возврат

cURL

# Полный возврат
curl -X POST https://bpapi.bazarbay.site/api/v1/invoices/42/refund \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Customer request"}'

# Частичный возврат
curl -X POST https://bpapi.bazarbay.site/api/v1/invoices/42/refund \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "reason": "Partial return"}'

Коды ошибок

КодОписание
400Bad Request — некорректный запрос или состояние
401Unauthorized — неверный, отсутствующий или истёкший API ключ
403Forbidden — организация не верифицирована
404Not Found — ресурс не найден
422Validation Error — ошибка валидации полей
429Too Many Requests — превышен rate limit (проверьте retry_after)
500Server Error — ошибка сервера
502Bad Gateway — ошибка Kaspi API
503Service Unavailable — Kaspi сессия истекла

Формат ошибки

{
  "message": "Описание ошибки",
  "errors": {
    "field_name": ["детали ошибки"]
  }
}