> Источник: https://apipay.kz/guides/kaspi-oplata-na-sayte · Обновлено: 2026-07-06 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

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

**TL;DR.** Есть два пути. **Путь 1 — виджет**: подключаете `widget.js` (≤10 КБ, без зависимостей) одной строкой — на сайте появляется кнопка «Оплатить через Kaspi», по клику открывается QR; от вас нужен один серверный эндпоинт, который создаёт QR-счёт. **Путь 2 — свой бэкенд**: форма с номером телефона → `POST /invoices` → покупатель платит по push в Kaspi (счёт живёт 24 часа) → вебхук `paid` обновляет заказ. Железное правило обоих путей: **API-ключ живёт только на сервере** — в браузере ему делать нечего. Конструкторы (Tilda и др.): фронт-часть встраивается, но серверная точка всё равно нужна — свой мини-бэкенд или n8n.

## Коротко

| Вопрос | Ответ |
|---|---|
| Путь 1: виджет | `<script src="https://apipay.kz/widget.js" defer>` + `<div data-apipay …>`; показывает QR, сам следит за статусом |
| Что нужно для виджета | Один серверный эндпоинт, создающий QR-счёт (`POST /invoices/qr`) |
| Путь 2: свой бэкенд | Форма → `POST /invoices` (push в Kaspi, 24 часа) → вебхук `paid` |
| Где живёт API-ключ | Только на сервере. В HTML/JS — никогда |
| Как виджет узнаёт об оплате | Опрашивает ВАШ эндпоинт раз в 5 секунд до 10 минут; ваш бэкенд узнаёт из вебхука |
| Tilda/конструкторы | Кнопку/форму вставить можно; серверная точка — свой бэкенд или n8n |
| QR против счёта по номеру | QR — «платит сейчас» (~5 минут); счёт по номеру — 24 часа |

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

### Что делает

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

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

```html
<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 и лимиты](/guides/qr-schet-ttl-i-limity)»).

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

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

```js
// 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`): «[Как создать счёт по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru)».

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**Поддерживается ли WooCommerce/WordPress?**
Через REST API (путь 2) — да; готового плагина нет.

Смотрите также: [Вебхуки ApiPay](/guides/nastroyka-webhookov-apipay) · [QR-счёт: TTL и лимиты](/guides/qr-schet-ttl-i-limity) · [Как создать счёт по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru) · [Kaspi-оплата в Telegram-боте](/guides/kaspi-oplata-v-telegram-bote) · пиллар «[Как принимать оплату Kaspi через API](/kaspi-api)».

---

ApiPay — независимый сервис и не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.
База знаний: https://apipay.kz/guides
