Архитектура (B2B2C)
ваш сервис (neuroai.kz) PayBot Kaspi
───────────────────────── ────── ─────
POST /partner/v1/clients → Client(partner_id=ВЫ)
GET login_url → пользователь логинится
+ подключает Kaspi ← OAuth/Kaspi
POST /partner/v1/checkout → PaymentLink + QR → создать QR
редирект на url → hosted checkout
↑ оплата ← polling
webhook → ваш URL ← payment.completed- •Один партнёрский ключ → доступ к данным всех ваших клиентов.
- •Один webhook URL → события всех ваших клиентов прилетают в одну точку с
client.idиexternal_idвнутри. - •Каждый клиент получает свой кабинет PayBot, свой Kaspi-аккаунт, свои тарифы. Вы видите их в
/partner/clients.
Получение ключа
1. Вы должны быть активным партнёром PayBot. Если ещё не подавали заявку — /partner/auth/register.
2. В кабинете /partner/integration → Создать API ключ → выбрать live или test.
3. Скопируйте значение — оно показывается один раз.
Формат: pk_live_<32 hex> или pk_test_<32 hex>. Хранится только SHA-256 хеш.
Все запросы — с заголовком:
X-Partner-API-Key: pk_live_a1b2c3d4...Базовый URL: https://api.paybot.kz
Проверка ключа
curl https://api.paybot.kz/partner/v1/ping \
-H "X-Partner-API-Key: pk_live_..."{ "ok": true, "partner_id": 3, "partner_slug": "neuroai", "mode": "live" }Регистрация клиента
Идемпотентный апсёрт по external_id (ваш внутренний ID пользователя).
Повторный вызов с тем же external_id вернёт уже созданный профиль
без ошибки.
curl -X POST https://api.paybot.kz/partner/v1/clients \
-H "X-Partner-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "ТОО Альфа",
"email": "[email protected]",
"phone": "+77071234567",
"external_id": "u_42",
"plan": "trial",
"send_invite_email": false
}'{
"created": true,
"client": {
"id": 178,
"public_id": "8d7a1f7c-...",
"name": "ТОО Альфа",
"email": "[email protected]",
"phone": "+77071234567",
"partner_external_id": "u_42",
"plan": "trial",
"plan_active": true,
"plan_expires_at": "2026-05-24T12:00:00Z",
"kaspi_connected": false,
"is_active": true,
"created_at": "2026-05-21T12:00:00Z"
},
"login_url": "https://paybot.kz/api/auth/magic?token=eyJhbGc...",
"onboarding_url": "https://paybot.kz/dashboard/kaspi-connect"
}- •
login_url— одноразовая 15-минутная ссылка. Отправьте её
пользователю — он попадёт прямо в кабинет.
- •
onboarding_url— куда направить клиента после входа, чтобы он
подключил свой Kaspi-аккаунт.
kaspi_connected подсказывает текущее состояние.Магик-ссылка для существующего клиента
curl -X POST https://api.paybot.kz/partner/v1/clients/u_42/login-link \
-H "X-Partner-API-Key: pk_live_..."{ "login_url": "https://paybot.kz/api/auth/magic?token=...", "expires_at": "..." }В пути допускается как PayBot ID (число), так и ваш external_id (строка).
Список клиентов
curl "https://api.paybot.kz/partner/v1/clients?limit=50&plan=trial" \
-H "X-Partner-API-Key: pk_live_..."Cursor-пагинация: next_cursor + has_more. Фильтры: external_id, plan.
Hosted Checkout
Когда клиент подключил Kaspi, вы можете создать платёжную ссылку.
Сценарий — редирект пользователя из вашего интерфейса на
paybot.kz/checkout/, где он увидит брендированную страницу
оплаты с QR + кнопкой Kaspi.
curl -X POST https://api.paybot.kz/partner/v1/checkout \
-H "X-Partner-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "u_42",
"amount": 5000,
"description": "Подписка neuroai PRO",
"success_url": "https://neuroai.kz/billing/success",
"cancel_url": "https://neuroai.kz/billing/cancel",
"expires_in_minutes": 60
}'{
"id": 1027,
"token": "pl_z9XyABCdEf",
"url": "https://paybot.kz/checkout/pl_z9XyABCdEf",
"qr_image_url": "https://api.qrserver.com/...",
"amount": 5000,
"description": "Подписка neuroai PRO",
"status": "active",
"expires_at": "2026-05-21T13:00:00Z",
"client_id": 178,
"external_id": "u_42"
}Редиректите пользователя на url. После оплаты он попадёт на
success_url, а вам прилетит webhook payment.completed.
Ошибка 422 kaspi_not_connected — клиент ещё не подключил Kaspi.
Отправьте ему onboarding_url / новую магик-ссылку.
Webhook
Настройка
В кабинете /partner/integration → Webhook → введите URL и
выберите события. Секрет показывается один раз.
Или через API кабинета (JWT-авторизация):
curl -X PUT "https://api.paybot.kz/partner/integration/webhook?mode=live" \
-H "Authorization: Bearer <ваш партнёрский JWT>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://neuroai.kz/api/paybot/webhook",
"events": ["payment.completed", "payment.expired", "client.created"],
"is_active": true
}'Формат события
PayBot отправляет HTTPS POST на ваш URL:
| Заголовок | Описание |
|---|---|
X-PayBot-Event-Id | pwh_evt_abc123 — уникальный ID события |
X-PayBot-Event | имя события (например payment.completed) |
X-PayBot-Timestamp | unix-секунды на момент отправки |
X-PayBot-Signature | sha256= — HMAC-SHA256(secret, "{timestamp}.{body}") |
Тело — JSON:
{
"id": "pwh_evt_4c6f0e2b1a",
"event": "payment.completed",
"mode": "live",
"partner_id": 3,
"created_at": "2026-05-21T12:34:56Z",
"data": {
"client": {
"id": 178,
"external_id": "u_42",
"email": "[email protected]"
},
"payment": {
"operation_id": "QR-9F8E7D6C",
"type": "qr",
"amount": 5000,
"status": "paid",
"description": "Подписка neuroai PRO",
"sender_name": "ИВАНОВ И. И.",
"transaction_id": "TX-2025...",
"paid_at": "2026-05-21T12:34:55Z"
}
}
}Проверка подписи
import hmac, hashlib, time
def verify(secret: str, timestamp: str, body: str, sig: str) -> bool:
expected = hmac.new(
secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", sig):
return False
if abs(int(time.time()) - int(timestamp)) > 300: # 5 min skew
return False
return Trueimport crypto from "node:crypto";
export function verifyPayBotSignature(secret, timestamp, body, signature) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(`sha256=${expected}`), Buffer.from(signature))) {
return false;
}
return Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
}Retry
Если ваш endpoint вернул не 2xx — PayBot повторит до 5 раз с
задержками 0s → 30s → 5min → 30min → 2h. После 5-й попытки
webhook помечается как failed, а сам endpoint — failing_since
(нужно перенастроить вручную, иначе следующие события не отправятся).
Доступные события
| Событие | Когда |
|---|---|
client.created | Вы создали клиента через POST /partner/v1/clients |
client.kaspi_connected | Клиент впервые подключил Kaspi |
client.plan_changed | Тариф клиента сменился |
payment.completed | Оплата успешно прошла |
payment.cancelled | Платёж отменён пользователем или Kaspi |
payment.expired | QR/счёт истёк без оплаты |
payment.refunded | Возврат проведён |
partner.webhook.test | Тестовое событие из кабинета |
Тестовая отправка
curl -X POST "https://api.paybot.kz/partner/integration/webhook/test?mode=live" \
-H "Authorization: Bearer <partner JWT>" \
-H "Content-Type: application/json" \
-d '{"event_type": "partner.webhook.test"}'Журнал доставок
/partner/integration/webhook/deliveries — последние 50 отправок с
кодами ответа, ошибками и временем доставки. Используйте для
диагностики «event пришёл / не пришёл».
Платежи
# Список платежей по всем клиентам партнёра
curl "https://api.paybot.kz/partner/v1/payments?status=paid&limit=100" \
-H "X-Partner-API-Key: pk_live_..."
# Один конкретный платёж
curl "https://api.paybot.kz/partner/v1/payments/QR-9F8E7D6C" \
-H "X-Partner-API-Key: pk_live_..."
# Только платежи одного клиента
curl "https://api.paybot.kz/partner/v1/payments?client_ref=u_42" \
-H "X-Partner-API-Key: pk_live_..."Сценарий: SaaS-биллинг под Kaspi
import requests
PAYBOT = "https://api.paybot.kz"
HEAD = {"X-Partner-API-Key": "pk_live_..."}
# 1. При регистрации в neuroai
def on_signup(user):
r = requests.post(f"{PAYBOT}/partner/v1/clients", json={
"name": user.full_name,
"email": user.email,
"external_id": str(user.id),
"plan": "trial",
}, headers=HEAD).json()
user.paybot_id = r["client"]["id"]
# Отправьте r["login_url"] клиенту на email или покажите кнопку
send_email(user.email, "Привяжите Kaspi", r["login_url"])
# 2. Пользователь нажал "Оплатить тариф" на neuroai.kz
def on_pay_click(user, amount):
r = requests.post(f"{PAYBOT}/partner/v1/checkout", json={
"external_id": str(user.id),
"amount": amount,
"description": f"Подписка neuroai PRO",
"success_url": f"https://neuroai.kz/billing/{user.id}/success",
"cancel_url": f"https://neuroai.kz/billing/{user.id}/cancel",
}, headers=HEAD).json()
if r.get("status") != "active":
raise RuntimeError("checkout creation failed")
return r["url"] # Редирект пользователя сюда
# 3. Когда оплата пришла — webhook payment.completed:
def on_webhook(event):
if event["event"] != "payment.completed":
return
p = event["data"]["payment"]
c = event["data"]["client"]
activate_subscription(user_id=c["external_id"], amount=p["amount"])Бесшовная (white-label) интеграция
Если вы хотите, чтобы конечный пользователь никогда не видел paybot.kz,
все шаги онбординга и настройки можно сделать через ваш собственный UI.
В сухом остатке: ваш бэкенд вызывает PayBot API, ваш фронт отображает
формы под вашим брендом, клиент даже не знает о существовании PayBot.
Подключение Kaspi через ваш UI
Никаких редиректов. Две формы в вашем интерфейсе → два HTTP-вызова.
# 1. Клиент ввёл свой номер Kaspi
curl -X POST https://api.paybot.kz/partner/v1/clients/u_42/kaspi/start \
-H "X-Partner-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"phone": "+77071234567"}'
# → SMS улетела клиенту от KaspiBank{ "state": "awaiting_otp", "phone": "+77071234567", "connected": false }# 2. Клиент ввёл код из SMS
curl -X POST https://api.paybot.kz/partner/v1/clients/u_42/kaspi/verify \
-H "X-Partner-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"code": "1234"}'{
"connected": true,
"phone": "+77071234567",
"org_name": "ИП Иванов И.И.",
"org_bin": "880101300123"
}Дополнительно:
# Переотправить SMS (если первая не пришла)
POST /partner/v1/clients/{ref}/kaspi/resend
# Проверить текущее состояние (idle | awaiting_otp | onboarded)
GET /partner/v1/clients/{ref}/kaspi/statusЛимит на SMS: не более 3 SMS на один номер за 10 минут.
После успешного verify PayBot отправит вам webhook client.kaspi_connected
(если он включён в подписке) с полями client.id, external_id, kaspi.phone, kaspi.org_name.
Webhook от лица конкретного клиента
В дополнение к партнёрскому webhook (один на всех клиентов), вы можете
зарегистрировать отдельный webhook на каждого клиента — например, если
вы реплицируете каждого клиента отдельным endpoint'ом, или передаёте
webhook URL обратно в его собственный сервис.
# Создать
curl -X POST https://api.paybot.kz/partner/v1/clients/u_42/webhooks \
-H "X-Partner-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://neuroai.kz/api/tenant/u_42/paybot",
"events": ["payment.completed", "payment.refunded"],
"description": "tenant feed"
}'{
"id": 17,
"url": "https://neuroai.kz/api/tenant/u_42/paybot",
"events": ["payment.completed", "payment.refunded"],
"description": "tenant feed",
"is_active": true,
"failing_since": null,
"created_at": "...",
"secret": "whsec_..."
}Секрет показывается один раз. Подпись и retry — те же, что у клиентского
webhook (см. Webhooks).
# Список
GET /partner/v1/clients/{ref}/webhooks
# Удалить
DELETE /partner/v1/clients/{ref}/webhooks/{webhook_id}API-ключ клиента через партнёрский API
Если ваш сценарий предполагает, что клиент сам будет делать прямые
запросы к PayBot (через ваш SDK), вы можете программно ротировать его
ключ:
# Узнать префикс активного ключа (полный — не покажем)
GET /partner/v1/clients/u_42/api-key
# → {"prefix": "kp_live_8f3a", "mode": "live"}
# Сгенерировать новый ключ (старый сразу перестаёт работать)
POST /partner/v1/clients/u_42/api-key/rotate{
"prefix": "kp_live_e21c",
"mode": "live",
"api_key": "kp_live_e21c4a8f5c2b9d1e7..."
}Полный ключ возвращается только на rotate — больше его восстановить
нельзя. Передайте его клиенту через защищённый канал (или сохраните в
своей секретнице, если клиент пользуется PayBot только через ваш SaaS).
Полный headless flow на Python
import requests
PAYBOT = "https://api.paybot.kz"
HEAD = {"X-Partner-API-Key": "pk_live_..."}
def onboard_user(user, kaspi_phone, otp_code):
# 1. Регистрация (идемпотентна по external_id)
requests.post(f"{PAYBOT}/partner/v1/clients", json={
"name": user.full_name,
"email": user.email,
"external_id": str(user.id),
}, headers=HEAD)
# 2. SMS на номер Kaspi
requests.post(
f"{PAYBOT}/partner/v1/clients/{user.id}/kaspi/start",
json={"phone": kaspi_phone}, headers=HEAD,
)
# ... ждём, пока клиент введёт код в форме у нас ...
# 3. Верификация OTP
r = requests.post(
f"{PAYBOT}/partner/v1/clients/{user.id}/kaspi/verify",
json={"code": otp_code}, headers=HEAD,
).json()
assert r["connected"]
# 4. (Опционально) персональный webhook на этого клиента
requests.post(
f"{PAYBOT}/partner/v1/clients/{user.id}/webhooks",
json={
"url": f"https://neuroai.kz/paybot/{user.id}",
"events": ["payment.completed"],
},
headers=HEAD,
)
# 5. Готово — теперь можно делать checkout
co = requests.post(f"{PAYBOT}/partner/v1/checkout", json={
"external_id": str(user.id),
"amount": 5000,
"description": "Тариф PRO",
}, headers=HEAD).json()
return co["url"]Юридический момент
При полностью headless-сценарии конечный пользователь формально не
принимает Terms of Service PayBot. Партнёр (вы) принимаете их за всех
своих клиентов в рамках партнёрского соглашения. Если клиент захочет
вывести заработанное на свой банковский счёт — он всё равно один раз
должен будет зайти в paybot.kz для KYC + ввода реквизитов. Для этого
используйте POST /partner/v1/clients/{ref}/login-link.
Лимиты и ограничения
- •Rate limit: 240 req/min на один (партнёр, mode). Под нагрузкой —
напишите в поддержку, поднимем лимит.
- •Идемпотентность:
POST /partner/v1/clientsидемпотентен по
external_id. Для POST /partner/v1/checkout поддерживается
заголовок Idempotency-Key.
- •Тестовый режим: ключ
pk_test_*не дёргает Kaspi и не создаёт
реальных списаний. Используйте для разработки и CI.
- •Безопасность ключей: ключ нельзя восстановить. Если утёк —
отзовите через /partner/integration/api-keys/{id} (DELETE) и
выпустите новый. Хранить в .env/секретнице вашего сервиса.
FAQ
Можно ли использовать один API-ключ из нескольких сервисов?
Технически — да, но мы рекомендуем выпустить отдельный ключ для каждой
среды (prod / staging / CI), чтобы быстрее локализовать утечку.
Что если я создам клиента, который уже зарегистрирован сам в PayBot?
Если этот клиент ещё не привязан к какому-либо партнёру и его email
совпадает — вы получите 409 client_email_taken. Используйте
процедуру claim для
запроса на привязку существующего клиента.
Как получить полную выписку платежей за период?
Через /partner/v1/payments с пагинацией. Для CSV-выгрузки —
используйте кабинет /partner/clients.
Webhook за партнёра приходит, но не приходит клиенту — это нормально?
Да. Если конкретный клиент не настроил свой собственный webhook
(/dashboard/webhooks) — событие уходит только вам как партнёру.
Если у клиента настроен — оба endpoint получат своё уведомление.