Qut Pay Сайт Кабинет База знаний Инструкции Документация API ҚАЗРУС
ГлавнаяБаза знаний → Справочник

Python SDK

Обновлено: 2026-09-14 · Версия в Markdown

Коротко

Python SDK — это один модуль qutpay.py. Работает на Python 3.8 и новее и использует только стандартную библиотеку (urllib, hmac, hashlib) — ни requests, ни других зависимостей не требуется.

Из модуля экспортируются три вещи:

ИмяДля чего
QutPayКлиент API: создать счёт, прочитать, отменить, вернуть
verify_webhookПроверка подписи входящего уведомления
QutPayErrorОшибка: .status (HTTP) и .code (машинный код)

Архив: https://api.qut.kz/downloads/qutpay-sdk-python.zip

Установка

Распакуйте архив и поставьте из папки:

unzip qutpay-sdk-python.zip -d qutpay-sdk
pip install ./qutpay-sdk

Или просто положите qutpay.py рядом со своим кодом — внешних зависимостей нет, так тоже работает. Имя дистрибутива — qutpay, импортируемый модуль тоже qutpay.

Инициализация

import os
from qutpay import QutPay, QutPayError, verify_webhook

qp = QutPay(
    api_key=os.environ["QUTPAY_API_KEY"],   # qp_live_… или qp_test_…
    base_url="https://api.qut.kz",          # значение по умолчанию
    timeout=20.0,                            # секунды
)

С пустым api_key будет ValueError. Ключ держите в переменной окружения — не в коде и тем более не в репозитории.

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

Аргументы передаются в snake_case, SDK сам переводит их в camelCase, который ждёт API.

inv = qp.create_invoice(
    amount=12500,
    kind="qr",                               # "qr" (по умолчанию) или "phone"
    description="Заказ №4471",
    external_order_id="4471",
    customer={"name": "Айгуль", "phone": "77011234567", "email": "a@b.kz"},
    success_url="https://site.kz/ok",
    fail_url="https://site.kz/fail",
    metadata={"branch": "almaty-abay"},
    idempotency_key="order-4471",            # уходит в заголовок Idempotency-Key
)

print(inv["id"], inv["status"], inv["payUrl"], inv["expiresAt"])

amount — единственный позиционный аргумент, остальные только по имени. Ответ — обычный dict: id, status, payUrl, qrUrl, deepLink, qrImageUrl, expiresAt. Поля ответа остаются в camelCase.

Остальные методы

МетодЧто делает
qp.get_invoice(invoice_id)Читает счёт. qp.get_invoice(invoice_id, live=True) дополнительно опрашивает Kaspi
qp.list_invoices(status="paid", limit=50)Список. Фильтры: status, external_order_id, search, from_, to, limit, offset
qp.cancel_invoice(invoice_id)Отменяет открытый счёт
qp.refund_invoice(invoice_id, amount=None, reason=None)Возврат. amount=None — полный
qp.simulate_invoice(invoice_id, "paid")Только песочница: имитация оплаты
qp.account()Краткая информация об организации — удобно для проверки ключа
fresh = qp.get_invoice(inv["id"], live=True)
if fresh["status"] == "paid":
    mark_order_paid("4471")

qp.refund_invoice(inv["id"], 5000, "Часть товара возвращена")

В SDK нет методов для подписок, bulk и форм-хуков. Вызывайте их напрямую по HTTP:

import json, urllib.request

req = urllib.request.Request(
    "https://api.qut.kz/api/v1/invoices/bulk",
    data=json.dumps({"invoices": [{"amount": 1000, "externalOrderId": "a-1"}]}).encode(),
    headers={"X-API-Key": os.environ["QUTPAY_API_KEY"], "Content-Type": "application/json"},
    method="POST",
)
with urllib.request.urlopen(req) as resp:
    result = json.loads(resp.read())   # HTTP 207, результат по каждому элементу

Проверка подписи вебхука

verify_webhook(secret, raw_body, headers, tolerance_sec=300)  # → True / False

raw_bodyнетронутые байты или строка. Объект из request.json не подойдёт: подпись считается ровно по пришедшим байтам. Функция читает заголовки X-Webhook-Signature и X-Webhook-Timestamp без учёта регистра, проверяет префикс sha256=, отбрасывает доставки старше 5 минут и сравнивает через hmac.compare_digest.

Пример на Flask

import json, os
from flask import Flask, request, abort, redirect
from qutpay import QutPay, QutPayError, verify_webhook

app = Flask(__name__)
qp = QutPay(api_key=os.environ["QUTPAY_API_KEY"])
SECRET = os.environ["QUTPAY_WEBHOOK_SECRET"]


@app.post("/buy")
def buy():
    order = create_order(request.form)
    try:
        inv = qp.create_invoice(
            amount=order.total,
            description=f"Заказ №{order.id}",
            external_order_id=str(order.id),
            success_url=f"https://site.kz/orders/{order.id}",
            idempotency_key=f"order-{order.id}",
        )
    except QutPayError as e:
        app.logger.error("qutpay %s %s %s", e.status, e.code, e)
        abort(502)
    save_invoice_id(order.id, inv["id"])
    return redirect(inv["payUrl"], code=303)


@app.post("/qutpay-webhook")
def hook():
    raw = request.get_data()                 # bytes, нетронутые
    if not verify_webhook(SECRET, raw, request.headers):
        abort(401)

    e = json.loads(raw)
    invoice = e["invoice"]

    # Идемпотентность: событие может прийти дважды
    if not already_handled(invoice["id"], invoice["status"]):
        if e["event"] == "invoice.paid":
            mark_order_paid(invoice["externalOrderId"], invoice)
        elif e["event"] == "invoice.refunded":
            mark_order_refunded(invoice["externalOrderId"])
        remember_handled(invoice["id"], invoice["status"])

    return "", 200                           # без 2xx доставка повторится 11 раз

Пример на Django

В Django тело лежит в request.body, заголовки — в request.headers. Маршрут обязательно надо освободить от проверки CSRF.

# views.py
import json, os
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from qutpay import verify_webhook

SECRET = os.environ["QUTPAY_WEBHOOK_SECRET"]


@csrf_exempt
@require_POST
def qutpay_webhook(request):
    raw = request.body                       # bytes, нетронутые
    if not verify_webhook(SECRET, raw, request.headers):
        return HttpResponseForbidden(status=401)

    e = json.loads(raw)
    invoice = e["invoice"]
    handle_event.delay(e["event"], invoice)  # тяжёлую работу — в Celery
    return HttpResponse(status=200)
# urls.py
from django.urls import path
from .views import qutpay_webhook

urlpatterns = [path("qutpay-webhook", qutpay_webhook)]

Если включён ATOMIC_REQUESTS, не делайте долгую работу внутри транзакции: когда обработчик отвечает дольше 8 секунд, доставка считается неуспешной и повторяется.

Обработка ошибок

На любой неуспешный ответ SDK бросает QutPayError:

ПолеЗначение
.statusHTTP-статус. При сетевой ошибке и таймауте — 0
.codeМашинный код: invalid_amount, invoice_not_found, request_rate_limited… При таймауте timeout, при обрыве сети network_error
str(e)Текст для человека
try:
    inv = qp.create_invoice(amount=amount, external_order_id=order_id)
except QutPayError as e:
    if e.code == "kaspi_session_expired":
        notify_admin("Привязка кассира оборвалась")
    elif e.code in ("tariff_limit_reached", "tariff_daily_burst"):
        notify_admin("Лимит")                # повторять бессмысленно
    elif e.code in ("request_rate_limited", "timeout", "network_error"):
        retry_later()                         # бэкофф 1, 2, 4, 8 секунд
    else:
        raise

Логику стройте по .code: текст может измениться, код — нет. Полный список: Каталог ошибок.

Вопросы и ответы

Можно поставить из PyPI? Нет, пакет не опубликован в публичном реестре. Скачайте архив и выполните pip install ./qutpay-sdk либо скопируйте файл к себе.

Есть ли async-версия? Нет, клиент синхронный. В FastAPI вызывайте его через run_in_threadpool или напишите свой клиент на httpxverify_webhook при этом можно использовать как есть.

Как получить raw body в FastAPI? raw = await request.body(), заголовки — request.headers.

Почему поля ответа не в snake_case? Входные аргументы сделаны в snake_case для удобства, а ответ возвращается как есть от API: payUrl, externalOrderId, expiresAt.

С чего начать тестирование? Создайте счёт тестовым ключом и вызовите qp.simulate_invoice(inv["id"], "paid") — вебхук при этом уходит по-настоящему: Симуляция оплаты в песочнице.

Связанные статьи

Создание счёта: все поляПолный справочник по POST /api/v1/invoices — тип и ограничение каждого поля, все поля ответа, примеры на curl и Node, разница между qr и phone и список частых ошибок с решениями.Безопасность вебхуков и проверка подписиКак устроена подпись, почему обязателен raw body, как проверять timestamp, примеры кода для Express, Laravel, Django и чистого Node, идемпотентная обработка и разбор частых ошибок.API возвратов — полный и частичный возвратСправочник по методу POST /invoices/{id}/refund: поля запроса, полный и частичный возврат, ограничение суммы, все коды ошибок, что делать при refund_unknown и какие события приходят после возврата.Каталог ошибок — что возвращает API и что делатьВсе основные коды ошибок Qut Pay API по группам: авторизация, привязка Kaspi, счета, возвраты, лимиты тарифа, вебхуки, подписки. Причина и решение для каждой.Симуляция оплаты в песочницеЭндпоинт simulate меняет статус счёта в песочнице: paid, failed, expired. Kaspi не вызывается, вебхук приходит как при настоящей оплате. Цикл проверки и написание автотестов.

Остались вопросы? WhatsApp +77788813333 · kazprose@gmail.com
Написать можно и из кабинета: Поддержка.

Qut Pay — независимый сервис, не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.