О насБлогКонтакты
Веб-разработка6 апреля 2026 г. 8 мин 101Обновлено: 3 мая 2026 г.

Интеграция Kaspi Pay API в 2026: полное руководство для разработчиков

AunimedaAunimeda
📋 Содержание

Интеграция Kaspi Pay API в 2026: полное руководство для разработчиков

Kaspi Pay в 2026 году - это уже не просто платёжный сервис, это финансовая инфраструктура Казахстана. 14+ миллионов активных пользователей, более 90% платёжеспособного населения страны. Kaspi Gold - карта номер один, Kaspi.kz - маркетплейс с крупнейшим трафиком в РК. Если ваш продукт работает с казахстанской аудиторией, Kaspi Pay - это не опция.

В 2026 году Kaspi обновил Merchant API до версии v2 с новой системой подписей, улучшенными вебхуками и поддержкой рассрочки (Kaspi Рассрочка). Эта статья охватывает всё актуальное.


Какой метод оплаты выбрать

Метод Где применять Как работает
eCommerce API (редирект) Веб-сайт, SPA Редирект на hosted-страницу Kaspi
Payment Link WhatsApp, Telegram, SMS, email Пользователь тапает ссылку → Kaspi app
QR-код Офлайн-касса, счёт-фактура, экран Сканирование камерой Kaspi
Deep Link Нативное iOS/Android приложение Прямое открытие Kaspi app
Kaspi Рассрочка Интернет-магазин, B2C Покупка в рассрочку 3/6/12 мес.

Рекомендация для 2026: для e-commerce используйте eCommerce API + Kaspi Рассрочку параллельно - рассрочка увеличивает конверсию на товарах от 30 000 ₸.


Регистрация мерчанта

  1. Зайдите на kaspi.kz/merchantapi
  2. Заполните анкету: БИН компании или ИИН ИП, расчётный счёт в казахстанском банке
  3. Подтвердите e-mail и телефон руководителя
  4. Дождитесь верификации (обычно 1–3 рабочих дня)
  5. Получите в личном кабинете: TradePointId, ApiKey, доступ к тест-окружению

Окружения:

Тест:       https://testpay.kaspi.kz/api/v2
Продакшн:   https://pay.kaspi.kz/api/v2

В тестовом окружении подтверждайте оплату через тестовое приложение Kaspi (запросите ссылку у менеджера поддержки мерчантов).


Подпись запросов (API v2)

В v2 изменился алгоритм подписи - теперь используется HMAC-SHA256 вместо SHA256:

const crypto = require('crypto');

function generateSignature(params, apiKey) {
  // Сортируем ключи алфавитно, склеиваем значения
  const sortedValues = Object.keys(params)
    .sort()
    .map(key => params[key])
    .join('');

  return crypto
    .createHmac('sha256', apiKey)
    .update(sortedValues)
    .digest('hex');
}

Шаг 1: Создание платёжного заказа

// Node.js / Express
import axios from 'axios';
import crypto from 'crypto';

const KASPI_TRADE_POINT_ID = process.env.KASPI_TRADE_POINT_ID;
const KASPI_API_KEY        = process.env.KASPI_API_KEY;
const KASPI_BASE_URL       = process.env.NODE_ENV === 'production'
  ? 'https://pay.kaspi.kz/api/v2'
  : 'https://testpay.kaspi.kz/api/v2';

function generateSignature(params) {
  const sortedValues = Object.keys(params).sort().map(k => params[k]).join('');
  return crypto.createHmac('sha256', KASPI_API_KEY).update(sortedValues).digest('hex');
}

export async function createKaspiOrder(order) {
  const params = {
    amount:       String(Math.round(order.amount)), // тенге, целое
    description:  order.description,
    failUrl:      order.failUrl,
    orderId:      order.orderId,                    // ваш уникальный ID
    returnUrl:    order.returnUrl,
    tradePointId: KASPI_TRADE_POINT_ID,
  };

  const body = {
    ...params,
    signature: generateSignature(params),
  };

  const { data } = await axios.post(`${KASPI_BASE_URL}/orders/create`, body, {
    headers: {
      'Content-Type':  'application/json',
      'Authorization': `Bearer ${KASPI_API_KEY}`,
    },
    timeout: 10_000,
  });

  // data.paymentUrl - редиректим пользователя сюда
  // data.orderId    - kaspi-side ID, сохраните в БД
  return data;
}

// Роут оформления заказа
app.post('/api/checkout', async (req, res) => {
  const { amount, cart } = req.body;
  const orderId = `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;

  // Сохраняем заказ в БД со статусом 'pending'
  await db.orders.create({
    orderId,
    amount,
    cart,
    status: 'pending',
    createdAt: new Date(),
  });

  try {
    const kaspi = await createKaspiOrder({
      amount,
      orderId,
      description: `Заказ ${orderId}`,
      returnUrl: `${process.env.SITE_URL}/payment/success?orderId=${orderId}`,
      failUrl:   `${process.env.SITE_URL}/payment/fail?orderId=${orderId}`,
    });

    await db.orders.update({ orderId }, { kaspiOrderId: kaspi.orderId });

    res.json({ paymentUrl: kaspi.paymentUrl });
  } catch (err) {
    await db.orders.update({ orderId }, { status: 'error' });
    res.status(502).json({ error: 'Не удалось создать платёж Kaspi' });
  }
});

Шаг 2: Обработка вебхука

Kaspi отправляет POST-запрос при любом изменении статуса платежа. Настройте URL вебхука в личном кабинете мерчанта.

app.post('/webhooks/kaspi', express.raw({ type: 'application/json' }), async (req, res) => {
  let payload;
  try {
    payload = JSON.parse(req.body.toString());
  } catch {
    return res.status(400).json({ error: 'Bad JSON' });
  }

  // Верифицируем подпись
  const { signature, ...rest } = payload;
  const expected = generateSignature(rest);

  if (signature !== expected) {
    console.warn('Kaspi webhook: invalid signature', payload);
    return res.status(400).json({ error: 'Invalid signature' });
  }

  const order = await db.orders.findOne({ kaspiOrderId: payload.orderId });
  if (!order) return res.status(404).json({ error: 'Order not found' });

  switch (payload.status) {
    case 'APPROVED':
      await db.orders.update(
        { kaspiOrderId: payload.orderId },
        {
          status:             'paid',
          paidAt:             new Date(),
          kaspiTransactionId: payload.transactionId,
          paidAmount:         payload.amount,
        }
      );
      await sendConfirmationEmail(order);
      await notifyWarehouse(order);
      break;

    case 'DECLINED':
    case 'CANCELLED':
      await db.orders.update(
        { kaspiOrderId: payload.orderId },
        { status: payload.status.toLowerCase() }
      );
      break;

    case 'REFUNDED':
      await db.orders.update(
        { kaspiOrderId: payload.orderId },
        { status: 'refunded', refundedAt: new Date() }
      );
      break;
  }

  // Kaspi ждёт 200 - иначе будет повторная отправка
  res.status(200).json({ received: true });
});

Шаг 3: Проверка статуса (polling на returnUrl)

Вебхуки могут задерживаться. Всегда дополнительно проверяйте статус на странице успешной оплаты:

async function getKaspiOrderStatus(kaspiOrderId) {
  const { data } = await axios.get(
    `${KASPI_BASE_URL}/orders/${kaspiOrderId}/status`,
    { headers: { Authorization: `Bearer ${KASPI_API_KEY}` } }
  );
  // 'APPROVED' | 'PENDING' | 'DECLINED' | 'CANCELLED' | 'REFUNDED'
  return data.status;
}

app.get('/payment/success', async (req, res) => {
  const { orderId } = req.query;
  const order = await db.orders.findOne({ orderId });

  if (!order) return res.redirect('/');

  // Не доверяем параметру URL - проверяем напрямую у Kaspi
  const kaspiStatus = await getKaspiOrderStatus(order.kaspiOrderId);

  if (kaspiStatus === 'APPROVED') {
    if (order.status !== 'paid') {
      // Вебхук ещё не пришёл - обрабатываем здесь
      await db.orders.update({ orderId }, { status: 'paid', paidAt: new Date() });
    }
    return res.render('success', { order });
  }

  if (kaspiStatus === 'PENDING') {
    // Оплата ещё обрабатывается - показываем экран ожидания
    return res.render('pending', { order, pollInterval: 3000 });
  }

  return res.render('fail', { order });
});

Kaspi Рассрочка (Рассрочка 0-0-12)

Kaspi Рассрочка - убийца продаж. Покупатель платит без процентов (3, 6 или 12 месяцев), вы получаете 100% суммы сразу за вычетом комиссии.

// Создание заказа с поддержкой рассрочки
export async function createOrderWithInstallment(order) {
  const params = {
    amount:        String(Math.round(order.amount)),
    description:   order.description,
    failUrl:       order.failUrl,
    orderId:       order.orderId,
    returnUrl:     order.returnUrl,
    tradePointId:  KASPI_TRADE_POINT_ID,
    // Разрешаем рассрочку: передаём доступные периоды
    installments:  JSON.stringify([3, 6, 12]),
  };

  const body = { ...params, signature: generateSignature(params) };

  const { data } = await axios.post(`${KASPI_BASE_URL}/orders/create`, body, {
    headers: {
      'Content-Type':  'application/json',
      'Authorization': `Bearer ${KASPI_API_KEY}`,
    },
  });

  return data; // paymentUrl откроет страницу с выбором: оплата разом или рассрочка
}

Вебхук для рассрочки содержит дополнительное поле:

{
  "status": "APPROVED",
  "paymentType": "INSTALLMENT",
  "installmentPeriod": 12,
  "orderId": "...",
  "transactionId": "...",
  "amount": 120000
}

Мобильная интеграция: Flutter

// pubspec.yaml
dependencies:
  url_launcher: ^6.3.0
  http: ^1.2.0

// kaspi_payment_service.dart
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class KaspiPaymentService {
  final String _backendUrl;
  KaspiPaymentService(this._backendUrl);

  /// Создаёт заказ на вашем бэкенде, открывает Kaspi
  Future<PaymentResult> startPayment({
    required double amount,
    required String orderId,
    required String description,
  }) async {
    // 1. Создаём заказ на сервере
    final response = await http.post(
      Uri.parse('$_backendUrl/api/checkout'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'amount': amount.round(),
        'orderId': orderId,
        'description': description,
      }),
    );

    if (response.statusCode != 200) {
      return PaymentResult.error('Ошибка создания заказа');
    }

    final data = jsonDecode(response.body);
    final paymentUrl = data['paymentUrl'] as String;

    // 2. Открываем Kaspi app или браузер
    final uri = Uri.parse(paymentUrl);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
      return PaymentResult.pending(orderId);
    } else {
      // Fallback - WebView
      await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
      return PaymentResult.pending(orderId);
    }
  }

  /// Проверяет статус заказа (вызывается при возврате в приложение)
  Future<String> checkStatus(String orderId) async {
    final response = await http.get(
      Uri.parse('$_backendUrl/api/orders/$orderId/status'),
    );
    final data = jsonDecode(response.body);
    return data['status'] as String; // 'paid' | 'pending' | 'failed'
  }
}

// Использование в экране оформления заказа
class CheckoutScreen extends StatelessWidget {
  final _kaspi = KaspiPaymentService('https://your-backend.kz');

  Future<void> _pay(BuildContext context, double total) async {
    final orderId = 'APP-${DateTime.now().millisecondsSinceEpoch}';

    final result = await _kaspi.startPayment(
      amount: total,
      orderId: orderId,
      description: 'Заказ из приложения',
    );

    if (result.isPending) {
      // Приложение ушло на фон - при возврате проверяем статус
      // Используйте AppLifecycleObserver или universal_links
    }
  }
}

Отслеживание возврата из Kaspi app:

// Подпишитесь на AppLifecycleState в виджете корзины
class _CartState extends State<CartScreen> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed && _awaitingPayment) {
      _checkPaymentResult();
    }
  }

  Future<void> _checkPaymentResult() async {
    final status = await _kaspi.checkStatus(_currentOrderId);
    if (status == 'paid') {
      Navigator.pushReplacementNamed(context, '/order-success');
    } else if (status == 'failed') {
      _showErrorDialog();
    }
    // 'pending' - ждём ещё
  }
}

QR-платёж (для кассы или счёта)

// Генерация QR
async function createKaspiQR(amount, orderId) {
  const params = {
    amount:       String(amount),
    orderId,
    tradePointId: KASPI_TRADE_POINT_ID,
  };

  const { data } = await axios.post(`${KASPI_BASE_URL}/qr/create`, {
    ...params,
    signature: generateSignature(params),
  }, {
    headers: { Authorization: `Bearer ${KASPI_API_KEY}` }
  });

  // data.qrCode  - base64 PNG QR-изображения, отрисуйте на экране кассы
  // data.qrToken - токен для polling статуса
  return data;
}

// Polling статуса QR (3 секунды × 60 попыток = 3 минуты)
async function waitForQRPayment(qrToken) {
  for (let attempt = 0; attempt < 60; attempt++) {
    await new Promise(r => setTimeout(r, 3000));

    const { data } = await axios.get(
      `${KASPI_BASE_URL}/qr/${qrToken}/status`,
      { headers: { Authorization: `Bearer ${KASPI_API_KEY}` } }
    );

    if (data.status === 'APPROVED') return { success: true, transaction: data };
    if (data.status === 'DECLINED') return { success: false, reason: 'declined' };
  }
  return { success: false, reason: 'timeout' };
}

Возврат денег (Refund API)

async function refundKaspiOrder(kaspiOrderId, amount) {
  const params = {
    amount:  String(amount),
    orderId: kaspiOrderId,
  };

  const { data } = await axios.post(
    `${KASPI_BASE_URL}/orders/${kaspiOrderId}/refund`,
    { ...params, signature: generateSignature(params) },
    { headers: { Authorization: `Bearer ${KASPI_API_KEY}` } }
  );

  return data; // { status: 'REFUNDED', refundId: '...' }
}

Частичный возврат поддерживается - передайте сумму меньше оригинальной. kaspiTransactionId из вебхука при необходимости используется при спорах.


Типичные ошибки

Код Причина Решение
INVALID_SIGNATURE Неверный порядок полей или алгоритм Используйте HMAC-SHA256, сортируйте ключи
ORDER_ALREADY_EXISTS Дублирующийся orderId UUID или Date.now() + случайный суффикс
TRADE_POINT_NOT_FOUND Неверный TradePointId Проверьте .env, не копируйте пробелы
AMOUNT_TOO_SMALL Сумма меньше 100 ₸ Валидируйте на клиенте перед отправкой
INVALID_RETURN_URL URL не в белом списке Добавьте все домены в портале мерчанта
INSTALLMENT_NOT_ALLOWED Рассрочка не подключена Свяжитесь с менеджером Kaspi для активации

Чеклист перед продакшном

  • Все returnUrl и failUrl домены добавлены в портале мерчанта
  • URL вебхука зарегистрирован и возвращает 200 в течение 5 секунд
  • kaspiTransactionId сохраняется в БД (нужен для возвратов)
  • Идентификаторы заказов уникальны и не повторяются
  • Реализован polling как fallback на returnUrl
  • Ошибки Kaspi API логируются с деталями запроса
  • Переключились с testpay.kaspi.kz на pay.kaspi.kz
  • Секреты хранятся в переменных окружения, не в коде

Нужна помощь с интеграцией Kaspi Pay? Напишите нам →


Aunimeda разрабатывает интернет-магазины, мобильные приложения и платёжные интеграции для бизнеса в Казахстане.

Смотрите также: Разработка интернет-магазина в Алматы, Мобильные приложения Алматы, AI-агенты для бизнеса в Алматы

Читайте также

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и командaunimeda
Веб-разработка

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и команд

Supabase - open-source BaaS на PostgreSQL с возможностью самохостинга. Firebase - зрелая Google-платформа. PocketBase - один бинарник для MVP. Сравниваем по модели данных, цене, realtime и соответствию требованиям казахстанского рынка.

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчикаaunimeda
Веб-разработка

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчика

OWASP Top 10 - это стандарт критических рисков безопасности. SQL-инъекции, сломанный контроль доступа, SSRF - каждый пункт с реальной атакой на ваш Node.js/Next.js код и конкретным исправлением. Актуально для проектов на казахстанском рынке.

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшнaunimeda
Веб-разработка

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшн

Bun 1.x стабилен в production. Deno 2.0 поддерживает npm-пакеты. Node.js 22 запускает TypeScript нативно. Реальные бенчмарки производительности, сравнение инструментов и конкретные рекомендации для казахстанских разработчиков.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Казахстане. Бесплатная консультация.

Разработка сайтов

Получить консультацию Все статьи