Как принять оплату Kaspi на сайте: виджет или свой код?

Обновлено 6 июля 2026 · Решение проблем · Версия в Markdown
Содержание
  1. Путь 1. Готовый виджет widget.js
  2. Путь 2. Свой бэкенд: форма → счёт по номеру → вебхук
  3. Безопасность: три правила, которые нельзя нарушать
  4. А если сайт на Tilda или другом конструкторе?
  5. Частые ошибки
  6. Вопросы и ответы

Путь 1. Готовый виджет widget.js

Что делает

Виджет рисует кнопку «Оплатить через Kaspi», по клику открывает модалку с QR-кодом Kaspi, показывает таймер и сам опрашивает статус (раз в 5 секунд, до 10 минут), после оплаты показывает «Оплачено». Вес — до 10 КБ gzip, никаких зависимостей и конфликтов со стилями сайта. API-ключ виджет не хранит и не запрашивает — все секреты остаются на вашем сервере.

Подключение на странице

<script src="https://apipay.kz/widget.js" defer></script>

<div
  data-apipay
  data-amount="5000"
  data-description="Заказ №42"
  data-merchant-endpoint="/api/create-invoice"
  data-check-endpoint="/api/check-invoice"
  data-label="Оплатить через Kaspi"
></div>

Опциональная настройка внешнего вида: data-color (цвет кнопки), data-shape (rounded|pill|sharp|soft), data-size (sm|md|lg), data-full-width="true". Есть и программный API: window.ApiPayWidget.open(opts), .mount(el), .refresh().

Контракт вашего серверного эндпоинта

Виджет ходит не в ApiPay, а в ваш бэкенд (поэтому ключ не светится):

POST {data-merchant-endpoint}   body: { amount, description }
  → { invoice_id, qr_image_url, qr_token_url, qr_expires_at }

GET {data-check-endpoint}?id={invoice_id}
  → { status: "pending" | "paid" | "cancelled" | "expired" }

Внутри merchant-эндпоинта вы создаёте QR-счёт (POST /invoices/qr с X-API-Key) и возвращаете его поля как есть. Критично: data-amount приходит из HTML и легко подменяется в DevTools — сервер обязан брать реальную сумму из своей корзины/заказа, а не доверять присланной. Помните про природу QR: он живёт ~5 минут — покупатель должен платить сразу (детали: «QR-счёт: TTL и лимиты»).

Путь 2. Свой бэкенд: форма → счёт по номеру → вебхук

Подходит, когда покупатель не обязан платить «здесь и сейчас»: счёт по номеру живёт 24 часа, покупатель получает push в приложении Kaspi. Полный минимальный сервер (Node 18+/Express, один файл):

// npm i express   |   запуск: APIPAY_API_KEY=... APIPAY_WEBHOOK_SECRET=... node server.js
const express = require("express");
const crypto = require("crypto");

const app = express();
const orders = new Map(); // demo-хранилище: external_order_id → {status, chatData}; в проде — БД

// 1) Форма отправляет сюда номер телефона покупателя
app.post("/api/pay", express.json(), async (req, res) => {
  const phone = String(req.body.phone || "").replace(/\D/g, "").slice(-10);
  if (phone.length !== 10) return res.status(422).json({ error: "Формат: 87001234567" });

  const orderId = `site-${Date.now()}`;
  const amount = 5000; // ВСЕГДА со своей стороны (корзина/заказ), НЕ из запроса браузера!

  const r = await fetch("https://api.apipay.kz/api/v1/invoices", {
    method: "POST",
    headers: { "X-API-Key": process.env.APIPAY_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      phone_number: "8" + phone, amount, description: "Оплата заказа на сайте",
      external_order_id: orderId, external_order_id_idempotency: orderId,
    }),
  });
  if (r.status !== 201) return res.status(502).json({ error: "Счёт не создан, попробуйте позже" });

  orders.set(orderId, { status: "processing" });
  res.json({ order_id: orderId }); // фронт покажет «Откройте Kaspi — там счёт»
});

// 2) Вебхук ApiPay: единственный источник правды об оплате
app.post("/webhooks/apipay", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", process.env.APIPAY_WEBHOOK_SECRET)
    .update(req.body).digest("hex"); // подпись — по СЫРОМУ телу
  const got = Buffer.from(req.get("X-Webhook-Signature") || "");
  const exp = Buffer.from(expected);
  if (got.length !== exp.length || !crypto.timingSafeEqual(exp, got)) return res.status(401).end();
  res.status(200).end(); // ответ быстрее 5 секунд, обработка после

  const { event, invoice } = JSON.parse(req.body);
  if (event === "invoice.status_changed" && orders.has(invoice.external_order_id)) {
    orders.get(invoice.external_order_id).status = invoice.status; // paid / expired / cancelled / error
  }
});

// 3) Страница «ждём оплату» опрашивает СВОЙ бэкенд (не ApiPay!)
app.get("/api/order-status", (req, res) => {
  res.json(orders.get(req.query.id) || { status: "unknown" });
});

app.use(express.static("public")); // форма и страница статуса
app.listen(3000);

Фронтенд предельно простой: форма шлёт номер на /api/pay, затем страница раз в 3–5 секунд спрашивает /api/order-status?id=… и показывает «Оплачено», когда вебхук перевёл заказ в paid. Опрос собственного бэкенда — нормально; нельзя опрашивать из браузера сам ApiPay (для этого пришлось бы светить ключ). Учтите легитимные гонки статусов (expired → paid): «Как создать счёт по номеру».

Безопасность: три правила, которые нельзя нарушать

  1. API-ключ — только на сервере. Ключ в HTML/JS виден каждому посетителю: с ним можно выставлять счета от вашего имени. Виджет спроектирован так, что ключ ему не нужен, — не «упрощайте» схему.
  2. Сумму определяет сервер. Всё, что пришло из браузера (data-amount, поля формы), — недоверенное: сверяйте с корзиной на бэкенде.
  3. Оплату подтверждает только вебхук с проверенной подписью. Не редирект «спасибо за оплату», не ответ виджета — только invoice.status_changed: paid, чья подпись X-Webhook-Signature сошлась по raw body. Настройка и примеры проверки: «Как настроить вебхуки ApiPay».

А если сайт на Tilda или другом конструкторе?

Честный ответ: фронт-часть встраивается, серверная — нет. Конструкторы не дают запускать серверный код, а для оплаты нужны две серверные вещи: место, где живёт API-ключ, и URL для вебхука. Рабочие варианты:

  • Мини-бэкенд (любой VPS/PaaS, код выше — 60 строк) + на Tilda вставка HTML-блока с виджетом или формой, указывающей на ваш /api/pay.
  • n8n вместо кода: сценарий «форма → создать счёт → принять вебхук → письмо/уведомление» собирается мышкой — «Интеграция ApiPay с n8n».
  • Lovable/AI-конструкторы с серверными функциями: серверную часть генерирует ИИ — дайте агенту apipay.kz/llms.txt и «плейбук для ИИ».

Готового плагина «для Tilda/WordPress в один клик» пока нет — не верьте страницам, которые обещают обратное.

Частые ошибки

  • API-ключ в клиентском JS. Самая опасная ошибка. Ключ утёк — перегенерируйте немедленно («API-ключ и вебхук-секрет»).
  • Доверять сумме из браузера. Подмена data-amount/поля формы = оплата 10 ₸ вместо 10 000 ₸. Сумма — из вашей БД.
  • Засчитывать оплату по возврату покупателя на «страницу спасибо». Только вебхук с проверенной подписью.
  • Виджет + покупатель «оплатит потом». Виджет показывает QR (~5 минут). Для «потом» — путь 2 (счёт по номеру, 24 часа).
  • Вебхук на localhost/за Basic auth. Нужен открытый публичный HTTPS-URL; локально — ngrok («Локальное тестирование»).
  • Тест в песочнице, ожидание реального push. В тестовом режиме счета в Kaspi не уходят; оплату имитируйте в кабинете, в прод — через «Рабочий режим».

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

Что выбрать: виджет или свой бэкенд?

Покупатель платит в момент заказа на странице — виджет (быстрее внедрить). Нужны «оплатит в течение дня», свои экраны и логика заказов — путь 2. Их можно совмещать.

Можно ли обойтись совсем без сервера?

Нет: API-ключ и вебхук требуют серверной точки. Минимум без кода — n8n-сценарий.

Виджет платный?

Виджет — часть сервиса, отдельно не тарифицируется; действует ваша подписка ApiPay. Подключение — по шагам этой статьи; если что-то не выходит, напишите в поддержку.

Как виджет узнаёт про оплату — вебхук не нужен?

Виджет опрашивает ваш check-эндпоинт (раз в 5 секунд, до 10 минут) — этого хватает для экрана покупателя. Но заказ в вашей системе всё равно подтверждайте вебхуком: это единственный надёжный канал.

Что покупатель видит при оплате по номеру?

Push в приложении Kaspi: счёт с описанием и кнопкой «Оплатить» — одно касание. Деньги сразу на вашем Kaspi-счету.

Поддерживается ли WooCommerce/WordPress?

Через REST API (путь 2) — да; готового плагина нет.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/kaspi-oplata-na-sayte.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.