Отдельного SDK-пакета у PayBot нет — API это обычный JSON поверх HTTPS, поэтому интегрируются прямыми вызовами стандартными средствами языка: requests/httpx в Python, fetch в Node.js, curl в PHP, net/http в Go. Ниже — минимальные рабочие сниппеты для каждого языка: создать Kaspi QR через POST /v2/qr с заголовками X-API-Key и Idempotency-Key, проверить статус, а также проверить подпись входящего вебхука (HMAC-SHA256 над "{timestamp}.{body}").
Базовый URL — https://api.paybot.kz. Прежде чем копировать код, получите API-ключ и разберитесь с режимами в Начало работы. Формат события и заголовки доставки описаны в Вебхуки. Полный список путей — в API Reference.
Что нужно для любого языка
Все примеры используют один и тот же контракт, независимо от языка.
- •Заголовок
X-API-Key: kp_live_(илиkp_test_...в тестовом режиме). - •Заголовок
Idempotency-Key:на каждом мутирующем POST — генерируйте один UUID на намерение пользователя, повтор той же операции шлите с тем же ключом. - •
Content-Type: application/json. - •Суммы — целые в тенге (₸):
amount: 5000= 5000 ₸. - •Ответ создания QR —
{operation_id, qr_token, deep_link, expires_at, status}; статус читается черезGET /v2/qr/{operation_id}.
Python
Через requests. Создаём QR, затем опрашиваем статус.
import uuid
import requests
BASE = "https://api.paybot.kz"
API_KEY = "kp_live_0123456789abcdef0123456789abcdef"
def create_qr(amount: int, comment: str) -> dict:
resp = requests.post(
f"{BASE}/v2/qr",
headers={
"X-API-Key": API_KEY,
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
json={"amount": amount, "comment": comment},
timeout=30,
)
resp.raise_for_status()
return resp.json()
def get_status(operation_id: str) -> dict:
resp = requests.get(
f"{BASE}/v2/qr/{operation_id}",
headers={"X-API-Key": API_KEY},
timeout=30,
)
resp.raise_for_status()
return resp.json()
qr = create_qr(5000, "Заказ #1042")
print(qr["deep_link"], qr["expires_at"])
print("status:", get_status(qr["operation_id"])["status"])Проверка подписи вебхука на приёмнике (Flask). Пересобираем "{timestamp}.{raw_body}", считаем HMAC-SHA256 секретом вебхука и сравниваем в constant-time с частью после sha256=.
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = b"whsec_your_webhook_secret"
@app.post("/paybot/webhook")
def webhook():
raw = request.get_data() # сырое тело, не парсить до проверки
ts = request.headers.get("X-Webhook-Timestamp", "")
header_sig = request.headers.get("X-Webhook-Signature", "") # "sha256=<hex>"
signed = f"{ts}.".encode() + raw
expected = hmac.new(WEBHOOK_SECRET, signed, hashlib.sha256).hexdigest()
received = header_sig.split("=", 1)[-1]
if not hmac.compare_digest(expected, received):
abort(400, "bad signature")
event = request.get_json()
if event["type"] == "payment.completed":
pass # выдать заказ
return "", 200Node.js (JavaScript / TypeScript)
Через встроенный fetch (Node 18+) и node:crypto.
import { randomUUID, createHmac, timingSafeEqual } from "node:crypto";
const BASE = "https://api.paybot.kz";
const API_KEY = "kp_live_0123456789abcdef0123456789abcdef";
async function createQr(amount, comment) {
const resp = await fetch(`${BASE}/v2/qr`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ amount, comment }),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
async function getStatus(operationId) {
const resp = await fetch(`${BASE}/v2/qr/${operationId}`, {
headers: { "X-API-Key": API_KEY },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
const qr = await createQr(5000, "Заказ #1042");
console.log(qr.deep_link, qr.expires_at);
console.log("status:", (await getStatus(qr.operation_id)).status);Проверка подписи вебхука (Express, express.raw — тело нужно сырым).
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = "whsec_your_webhook_secret";
app.post("/paybot/webhook", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-Webhook-Timestamp") || "";
const headerSig = req.header("X-Webhook-Signature") || ""; // "sha256=<hex>"
const signed = `${ts}.` + req.body.toString("utf8");
const expected = createHmac("sha256", SECRET).update(signed).digest("hex");
const received = headerSig.split("=")[1] || "";
const a = Buffer.from(expected), b = Buffer.from(received);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "payment.completed") {
// выдать заказ
}
res.sendStatus(200);
});PHP
Через curl. Создание QR и проверка подписи вебхука.
<?php
const BASE = "https://api.paybot.kz";
const API_KEY = "kp_live_0123456789abcdef0123456789abcdef";
function create_qr(int $amount, string $comment): array {
$ch = curl_init(BASE . "/v2/qr");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: " . API_KEY,
"Idempotency-Key: " . bin2hex(random_bytes(16)),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["amount" => $amount, "comment" => $comment]),
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true);
}
$qr = create_qr(5000, "Заказ #1042");
echo $qr["deep_link"], " ", $qr["expires_at"], PHP_EOL;Проверка подписи входящего вебхука:
<?php
$secret = "whsec_your_webhook_secret";
$raw = file_get_contents("php://input"); // сырое тело
$ts = $_SERVER["HTTP_X_WEBHOOK_TIMESTAMP"] ?? "";
$headerSig = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? ""; // "sha256=<hex>"
$expected = hash_hmac("sha256", $ts . "." . $raw, $secret);
$received = explode("=", $headerSig, 2)[1] ?? "";
if (!hash_equals($expected, $received)) {
http_response_code(400);
exit("bad signature");
}
$event = json_decode($raw, true);
if ($event["type"] === "payment.completed") {
// выдать заказ
}
http_response_code(200);Go
Через net/http и crypto/hmac.
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
)
const base = "https://api.paybot.kz"
const apiKey = "kp_live_0123456789abcdef0123456789abcdef"
func idempotencyKey() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func createQR(amount int, comment string) (map[string]any, error) {
body, _ := json.Marshal(map[string]any{"amount": amount, "comment": comment})
req, _ := http.NewRequest("POST", base+"/v2/qr", bytes.NewReader(body))
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Idempotency-Key", idempotencyKey())
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
return out, nil
}
func main() {
qr, _ := createQR(5000, "Заказ #1042")
fmt.Println(qr["deep_link"], qr["expires_at"])
}Проверка подписи вебхука в HTTP-хендлере:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
var secret = []byte("whsec_your_webhook_secret")
func webhook(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body) // сырое тело
ts := r.Header.Get("X-Webhook-Timestamp")
headerSig := r.Header.Get("X-Webhook-Signature") // "sha256=<hex>"
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(raw)
expected := hex.EncodeToString(mac.Sum(nil))
received := strings.TrimPrefix(headerSig, "sha256=")
if !hmac.Equal([]byte(expected), []byte(received)) {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}FAQ
Есть ли официальный пакет `pip install paybot` или `npm i paybot`?
Нет. Официального SDK-пакета PayBot не публикует — API намеренно простой (JSON поверх HTTPS), и мы показываем прямые вызовы стандартными библиотеками. Это избавляет от версионных конфликтов и лишних зависимостей. Полный контракт — в API Reference.
Почему в примерах подписи вебхука тело читается «сырым»?
Подпись считается над байтами тела ровно так, как их отправил PayBot (компактный JSON без пробелов). Если сначала распарсить и заново сериализовать тело, байты изменятся и HMAC не совпадёт. Поэтому берите raw body до парсинга — express.raw, php://input, io.ReadAll. Детали — в Вебхуки.
Как переключиться с боевого режима на тестовый?
Замените ключ kp_live_... на kp_test_.... Тестовые ключи не тратят лимит платежей и не действуют на реальные деньги. Больше про режимы — в Начало работы.
Обязателен ли Idempotency-Key в этих сниппетах?
Да, для POST /v2/qr, POST /v2/invoices, POST /v2/refunds и отмены счёта. Без него вернётся 400 idempotency_key_required. Генерируйте один ключ на нажатие кнопки пользователем, а не на каждый HTTP-ретрай.