AboutBlogContact
DevelopmentApril 6, 2026 6 min read 156

Kaspi Pay API Integration Guide for Web and Mobile Apps (2026)

AunimedaAunimeda
📋 Table of Contents

Kaspi Pay API Integration Guide for Web and Mobile Apps (2026)

Kaspi Pay is the dominant payment method in Kazakhstan - used by over 13 million active users (85%+ of the adult population). If you're building anything for the Kazakh market, Kaspi Pay integration is not optional. This is the most comprehensive English-language guide to integrating it.


Overview of Kaspi Pay Payment Methods

Kaspi offers several payment flows depending on your use case:

Method Use Case User Action
QR Code In-store, invoices User scans QR in Kaspi app
Deep Link Mobile apps Redirects to Kaspi app
Payment Link Web, WhatsApp, SMS User taps link → Kaspi app
eCommerce API Web checkout Redirect to Kaspi payment page

For most web/mobile integrations: use the eCommerce API (redirect flow) or Payment Link.


Getting API Access

  1. Register as a merchant at kaspi.kz/merchantapi
  2. Pass business verification (BIN/IIN, bank account in KZ)
  3. Receive: TradePointId, API credentials, test environment access

Test environment: https://testpay.kaspi.kz
Production: https://pay.kaspi.kz


Payment Flow (eCommerce API)

1. Your server creates an order → sends to Kaspi API
2. Kaspi returns a payment URL
3. You redirect the user to that URL (or open in WebView on mobile)
4. User pays in Kaspi app / web interface
5. Kaspi sends webhook to your server
6. Your server confirms payment → updates order

Step 1: Create a Payment Order

// Node.js example
const axios = require('axios');
const crypto = require('crypto');

const KASPI_TRADE_POINT_ID = process.env.KASPI_TRADE_POINT_ID;
const KASPI_API_KEY = process.env.KASPI_API_KEY;
const BASE_URL = 'https://pay.kaspi.kz'; // or testpay.kaspi.kz

async function createKaspiOrder(orderData) {
  const payload = {
    amount: orderData.amount,          // In tenge, integer (e.g. 5000 = 5000 KZT)
    orderId: orderData.orderId,        // Your unique order ID
    returnUrl: orderData.returnUrl,    // Redirect after payment
    failUrl: orderData.failUrl,        // Redirect on failure
    tradePointId: KASPI_TRADE_POINT_ID,
    description: orderData.description,
  };

  // Generate signature
  const signatureString = `${payload.amount}${payload.orderId}${KASPI_API_KEY}`;
  const signature = crypto
    .createHash('sha256')
    .update(signatureString)
    .digest('hex');

  const response = await axios.post(
    `${BASE_URL}/api/v1/orders/create`,
    { ...payload, signature },
    {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${KASPI_API_KEY}`,
      },
    }
  );

  return response.data; // Contains { paymentUrl, orderId }
}

// Usage in your checkout handler
app.post('/checkout', async (req, res) => {
  const { cartTotal, orderId } = req.body;

  const kaspiOrder = await createKaspiOrder({
    amount: Math.round(cartTotal),
    orderId: `ORDER-${orderId}`,
    returnUrl: `https://yoursite.kz/payment/success?orderId=${orderId}`,
    failUrl: `https://yoursite.kz/payment/fail?orderId=${orderId}`,
    description: `Order #${orderId}`,
  });

  // Redirect user to Kaspi payment page
  res.json({ paymentUrl: kaspiOrder.paymentUrl });
});

Step 2: Handle the Webhook

Kaspi sends a POST request to your webhook URL when payment status changes.

// Webhook handler
app.post('/webhooks/kaspi', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = JSON.parse(req.body);

  // Verify signature
  const expectedSignature = crypto
    .createHash('sha256')
    .update(`${payload.orderId}${payload.amount}${KASPI_API_KEY}`)
    .digest('hex');

  if (payload.signature !== expectedSignature) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  switch (payload.status) {
    case 'APPROVED':
      await db.orders.update({
        where: { kaspiOrderId: payload.orderId },
        data: { 
          status: 'paid',
          paidAt: new Date(),
          kaspiTransactionId: payload.transactionId,
        }
      });
      await fulfillOrder(payload.orderId);
      break;

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

  // Always respond 200 to acknowledge receipt
  res.status(200).json({ received: true });
});

Step 3: Verify Payment Status (Polling Fallback)

Don't rely on webhooks alone - they can fail. Check status on your success/return URL:

async function checkKaspiOrderStatus(kaspiOrderId) {
  const response = await axios.get(
    `${BASE_URL}/api/v1/orders/${kaspiOrderId}/status`,
    {
      headers: { 'Authorization': `Bearer ${KASPI_API_KEY}` }
    }
  );

  return response.data.status; // 'APPROVED' | 'PENDING' | 'DECLINED' | 'CANCELLED'
}

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

  // Always verify - don't trust the URL parameter alone
  const kaspiStatus = await checkKaspiOrderStatus(order.kaspiOrderId);

  if (kaspiStatus === 'APPROVED') {
    res.render('success', { order });
  } else {
    res.render('pending', { order }); // Payment may still be processing
  }
});

Mobile Integration: Flutter Deep Link

For mobile apps, use deep links to open the Kaspi app directly:

// pubspec.yaml
dependencies:
  url_launcher: ^6.2.0

// payment_service.dart
import 'package:url_launcher/url_launcher.dart';

class KaspiPaymentService {
  /// Opens Kaspi app for payment. Returns to your app via deeplink callback.
  Future<void> openKaspiPayment(String paymentUrl) async {
    final uri = Uri.parse(paymentUrl);
    
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    } else {
      // Kaspi app not installed - open web fallback
      await launchUrl(
        Uri.parse(paymentUrl),
        mode: LaunchMode.inAppWebView,
      );
    }
  }
}

Handle return deep link in your app:

// In AndroidManifest.xml
// <intent-filter>
//   <action android:name="android.intent.action.VIEW" />
//   <data android:scheme="yourapp" android:host="payment" />
// </intent-filter>

// In your Flutter app
class PaymentResultScreen extends StatefulWidget {
  @override
  void initState() {
    super.initState();
    // Parse result from deep link
    _handleIncomingLink();
  }

  void _handleIncomingLink() {
    // yourapp://payment?status=success&orderId=ORDER-123
    final uri = Uri.parse(widget.deepLink);
    final status = uri.queryParameters['status'];
    final orderId = uri.queryParameters['orderId'];
    
    if (status == 'success') {
      // Verify server-side before showing success screen
      _verifyPayment(orderId);
    }
  }
}

QR Code Payment Flow

For physical retail or invoice-based payments:

async function generateKaspiQR(amount, orderId) {
  const response = await axios.post(`${BASE_URL}/api/v1/qr/create`, {
    amount,
    orderId,
    tradePointId: KASPI_TRADE_POINT_ID,
    signature: generateSignature(amount, orderId),
  });

  // response.data.qrCode - base64 encoded QR image
  // response.data.qrToken - token to check payment status
  return response.data;
}

// Poll for QR payment status (user hasn't confirmed yet)
async function pollQRStatus(qrToken, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    await sleep(3000); // Poll every 3 seconds
    
    const status = await axios.get(`${BASE_URL}/api/v1/qr/${qrToken}/status`, {
      headers: { Authorization: `Bearer ${KASPI_API_KEY}` }
    });

    if (status.data.status === 'APPROVED') return 'paid';
    if (status.data.status === 'DECLINED') return 'failed';
    // 'PENDING' - continue polling
  }
  return 'timeout';
}

Common Errors and Solutions

Error Code Meaning Fix
INVALID_SIGNATURE Signature mismatch Check field order in signature string
ORDER_ALREADY_EXISTS Duplicate orderId Use unique IDs (UUID or timestamp)
TRADE_POINT_NOT_FOUND Wrong TradePointId Verify env variable
AMOUNT_TOO_SMALL Below minimum (100 KZT) Validate before sending
INVALID_RETURN_URL URL not whitelisted Register return URLs in merchant portal

Testing

Use test card in Kaspi test environment:

  • Any amount accepted in test mode
  • Use testpay.kaspi.kz endpoint
  • Kaspi test app available separately (request from Kaspi merchant support)

Notes for Production

  • Register all returnUrl and failUrl domains in the Kaspi merchant portal before going live
  • Store kaspiTransactionId from webhook - needed for refunds
  • Refund API: POST /api/v1/orders/{orderId}/refund with amount and signature
  • Rate limits: 100 requests/minute per TradePointId

Need help integrating Kaspi Pay? Contact Aunimeda →

Read Also

Telegram Bot vs WhatsApp Bot: Which to Build for CIS Markets (2026)aunimeda
Development

Telegram Bot vs WhatsApp Bot: Which to Build for CIS Markets (2026)

Detailed comparison of Telegram and WhatsApp bots for Russian, Kazakh, and Kyrgyz markets. Audience data, technical capabilities, costs, and when to build each.

2GIS Maps Flutter Integration Guide: Maps for CIS Apps (2026)aunimeda
Development

2GIS Maps Flutter Integration Guide: Maps for CIS Apps (2026)

How to integrate 2GIS Maps SDK into your Flutter app. Map display, markers, routing, search, and geolocation - with full code examples for iOS and Android.

Telegram Mini App with Payments: Complete Developer Tutorial (2026)aunimeda
Development

Telegram Mini App with Payments: Complete Developer Tutorial (2026)

Build a Telegram Mini App with in-app payments from scratch. Covers Telegram WebApp API, bot setup, React frontend, Node.js backend, and Telegram Payments integration.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles