> Источник: 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. Готовый виджет 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`). ApiPay отдаёт идентификатор счёта в поле **`id`** — верните его виджету как **`invoice_id`**, иначе опрос статуса не запустится и модалка так и останется в «Ожидаем оплату…». Поля `qr_image_url`, `qr_token_url`, `qr_expires_at` называются одинаково — их отдавайте как есть. **Критично:** `data-amount` приходит из HTML и легко подменяется в DevTools — сервер обязан брать реальную сумму из своей корзины/заказа, а не доверять присланной.

Окно на скан QR короткое, точный момент берите из поля `qr_expires_at` и не зашивайте длительность константой — сколько живёт QR и что с ним происходит дальше: «[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 (для этого пришлось бы светить ключ). Порядок доставки вебхуков не гарантирован, поэтому корректирующий `paid` и «опоздавший» `expired`/`cancelled` могут прийти в обратном порядке: не понижайте статус уже оплаченного заказа — вместо безусловного присваивания пишите `if (o.status !== 'paid') o.status = invoice.status;` Про сами гонки статусов — «[Как создать счёт по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru)».

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

1. **API-ключ — только на сервере.** Ключ в HTML/JS виден каждому посетителю: с ним можно выставлять счета от вашего имени. Виджет спроектирован так, что ключ ему не нужен. Если ключ утёк — перегенерируйте его немедленно («[API-ключ и вебхук-секрет](/guides/api-klyuch-i-webhook-secret)»).
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 в один клик» пока нет — WooCommerce и подобные подключаются через REST API (путь 2).

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

- **Вебхук на localhost, за Basic auth или на туннельном адресе.** Нужен открытый публичный HTTPS-URL. Туннель (ngrok, «[Локальное тестирование](/local-testing)») годится только для разработки: в рабочем режиме кабинет ждёт постоянный адрес на вашем домене.
- **Тест в песочнице, ожидание реального push.** В тестовом режиме счета в Kaspi не уходят; оплату имитируйте в кабинете, в прод — через «Рабочий режим».

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

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

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

Смотрите также: [Вебхуки 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
