AboutBlogContact
Web DevelopmentApril 6, 2026 11 min read 1Updated: July 1, 2026

Kaspi Pay API Integration in 2026: The Complete Developer Guide

AunimedaAunimeda
📋 Table of Contents

In 2026, Kaspi Pay is no longer just a payment service—it's the financial infrastructure of Kazakhstan. With more than 14 million active users and over 90% of the country's bankable population using the platform, Kaspi Gold has become the number one payment card, while Kaspi.kz remains the largest online marketplace in Kazakhstan.

If your product targets customers in Kazakhstan, integrating Kaspi Pay is no longer optional—it's essential.

In 2026, Kaspi introduced Merchant API v2, featuring a new request-signing mechanism, improved webhook support, and installment payments (Kaspi Installments). This guide covers everything you need to know.


Which Payment Method Should You Choose?

Method Best For How It Works
eCommerce API (Redirect) Websites, SPAs Redirects customers to Kaspi's hosted payment page
Payment Link WhatsApp, Telegram, SMS, Email Customer taps a link and pays in the Kaspi app
QR Code Retail stores, invoices, POS displays Customer scans the QR code using the Kaspi app
Deep Link Native iOS / Android apps Opens the Kaspi app directly
Kaspi Installments E-commerce, B2C Customers pay in 3, 6, or 12 monthly installments

2026 Recommendation: For e-commerce projects, offer both the eCommerce API and Kaspi Installments. Installment payments typically improve conversion rates for purchases above 30,000 KZT.


Merchant Registration

  1. Visit kaspi.kz/merchantapi
  2. Complete the application using:
    • Company BIN or Individual Entrepreneur IIN
    • A business bank account in Kazakhstan
  3. Verify your email address and phone number.
  4. Wait for approval (typically 1–3 business days).
  5. After approval you'll receive:
    • TradePointId
    • ApiKey
    • Access to the sandbox environment

Environments

Sandbox:    https://testpay.kaspi.kz/api/v2
Production: https://pay.kaspi.kz/api/v2

In the sandbox environment, payment confirmations must be completed using the Kaspi test application. Contact your merchant support manager to obtain access.


Request Signing (API v2)

Merchant API v2 changed the signing algorithm. Instead of SHA256, all requests are now signed using HMAC-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');
}


Step 1: Creating a Payment Order

// 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)), // Amount in KZT (integer)
    description: order.description,
    failUrl: order.failUrl,
    orderId: order.orderId, // Your unique order 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: 10000,
    }
  );

  // data.paymentUrl — Redirect the customer here
  // data.orderId — Kaspi-side order ID (store it in your database)
  return data;
}

// Checkout endpoint
app.post('/api/checkout', async (req, res) => {
  const { amount, cart } = req.body;
  const orderId = `ORD-${Date.now()}-${Math.random()
    .toString(36)
    .slice(2, 7)}`;

  // Save order with "pending" status
  await db.orders.create({
    orderId,
    amount,
    cart,
    status: 'pending',
    createdAt: new Date(),
  });

  try {
    const kaspi = await createKaspiOrder({
      amount,
      orderId,
      description: `Order ${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: 'Failed to create Kaspi payment.',
    });
  }
});

Step 2: Processing Webhooks

Kaspi sends a POST request every time a payment status changes. Configure your webhook endpoint in the Kaspi Merchant Portal.

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',
      });
    }

    // Verify webhook signature
    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 expects HTTP 200, otherwise it retries
    res.status(200).json({
      received: true,
    });
  }
);

Step 3: Checking Payment Status (Polling on returnUrl)

Webhooks may occasionally be delayed. Always verify the payment status on your payment success page as an additional safeguard.

async function getKaspiOrderStatus(kaspiOrderId) {
  const { data } = await axios.get(
    `${KASPI_BASE_URL}/orders/${kaspiOrderId}/status`,
    {
      headers: {
        Authorization: `Bearer ${KASPI_API_KEY}`,
      },
    }
  );

  // Returns:
  // '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('/');
  }

  // Never trust the URL parameter alone.
  // Always verify the payment directly with Kaspi.
  const kaspiStatus = await getKaspiOrderStatus(order.kaspiOrderId);

  if (kaspiStatus === 'APPROVED') {
    if (order.status !== 'paid') {
      // Webhook hasn't arrived yet—mark the order as paid.
      await db.orders.update(
        { orderId },
        {
          status: 'paid',
          paidAt: new Date(),
        }
      );
    }

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

  if (kaspiStatus === 'PENDING') {
    // Payment is still being processed.
    return res.render('pending', {
      order,
      pollInterval: 3000,
    });
  }

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

Kaspi Installments (0-0-3 / 0-0-6 / 0-0-12)

Kaspi Installments is one of the strongest conversion drivers for online stores. Customers pay over 3, 6, or 12 months with zero interest, while merchants receive the full payment immediately (minus the agreed commission).

// Create an order with installment support
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,

    // Enable installment options
    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}`,
      },
    }
  );

  // paymentUrl allows customers to choose
  // either a one-time payment or installments.
  return data;
}

Installment payment webhooks include additional fields:

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

Mobile Integration: 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);

  /// Creates an order on your backend and launches Kaspi
  Future<PaymentResult> startPayment({
    required double amount,
    required String orderId,
    required String description,
  }) async {
    // 1. Create an order on your server
    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('Failed to create payment order.');
    }

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

    // 2. Open the Kaspi app (or browser if unavailable)
    final uri = Uri.parse(paymentUrl);

    if (await canLaunchUrl(uri)) {
      await launchUrl(
        uri,
        mode: LaunchMode.externalApplication,
      );

      return PaymentResult.pending(orderId);
    } else {
      // Fallback to in-app browser
      await launchUrl(
        uri,
        mode: LaunchMode.inAppBrowserView,
      );

      return PaymentResult.pending(orderId);
    }
  }

  /// Checks the payment status when returning to the app
  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;
    // Returns: 'paid' | 'pending' | 'failed'
  }
}

// Example usage in CheckoutScreen

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: 'Mobile application order',
    );

    if (result.isPending) {
      // The application moves to the background.
      // When the user returns, verify the payment status.
      // Consider using AppLifecycleObserver
      // or universal links.
    }
  }
}

Detecting Return from the Kaspi App

// Listen for AppLifecycleState changes

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();
    }

    // If status == 'pending', continue waiting.
  }
}

QR Payments (For Retail Stores and Invoices)

// Generate a Kaspi QR code
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-encoded PNG QR image (display it at the checkout)
  // data.qrToken - Token used for polling the payment status
  return data;
}

// Poll QR payment status
// (every 3 seconds × 60 attempts = 3 minutes)
async function waitForQRPayment(qrToken) {
  for (let attempt = 0; attempt < 60; attempt++) {
    await new Promise((resolve) => setTimeout(resolve, 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;
  // Example:
  // { status: 'REFUNDED', refundId: '...' }
}

Partial refunds are fully supported. Simply specify an amount lower than the original transaction amount.

When handling payment disputes or reconciliation, you may also need to store the kaspiTransactionId received in webhook notifications.


Common API Errors

Error Code Cause Solution
INVALID_SIGNATURE Incorrect field order or signing algorithm Use HMAC-SHA256 and sort all request parameters alphabetically before signing.
ORDER_ALREADY_EXISTS Duplicate orderId Generate unique IDs (UUID or Date.now() with a random suffix).
TRADE_POINT_NOT_FOUND Invalid TradePointId Verify your .env configuration and ensure there are no extra spaces.
AMOUNT_TOO_SMALL Payment amount is below 100 KZT Validate the minimum amount on the client before sending the request.
INVALID_RETURN_URL URL is not whitelisted Add every return URL/domain in the Kaspi Merchant Portal.
INSTALLMENT_NOT_ALLOWED Installment payments are not enabled Contact your Kaspi merchant manager to activate the feature.

Production Readiness Checklist

  • All returnUrl and failUrl domains have been added to the Merchant Portal.
  • The webhook URL is registered and consistently returns HTTP 200 within 5 seconds.
  • kaspiTransactionId is stored in your database for refunds and reconciliation.
  • Every orderId is globally unique.
  • Polling is implemented as a fallback on the returnUrl page.
  • All Kaspi API errors are logged together with request details.
  • Production environment uses https://pay.kaspi.kz instead of the sandbox endpoint.
  • API keys and secrets are stored in environment variables rather than in source code.

Need Help Integrating Kaspi Pay?

Whether you're building an e-commerce platform, mobile application, marketplace, or enterprise payment solution, professional integration can save weeks of development time and help avoid costly production issues.


Frequently Asked Questions

How long does Kaspi Pay integration take?

For a developer familiar with payment integrations, roughly 1–2 weeks of work: payment initiation, handling returnUrl/failUrl, the webhook, and polling as a fallback. The longest pole is merchant registration on Kaspi's side — start it in parallel and build against the testpay.kaspi.kz sandbox.

What do I need to start integrating?

A Kaspi merchant agreement and TradePointId, your returnUrl/failUrl domains whitelisted in the Merchant Portal, a registered webhook URL that returns HTTP 200 within 5 seconds, and storage of kaspiTransactionId in your database — you'll need it for refunds and reconciliation.

Is there a sandbox environment?

Yes — testpay.kaspi.kz. Test every path (success, cancellation, duplicate webhooks) there, and only switch to https://pay.kaspi.kz for production. Keep API keys and secrets in environment variables, never in source code.

Does Kaspi support installments and refunds?

Installments are enabled separately through your Kaspi merchant manager (an INSTALLMENT_NOT_ALLOWED error means the feature isn't active). Refunds are issued against the stored kaspiTransactionId, so always persist it on every successful payment.


Aunimeda — custom web applications, mobile apps, enterprise software, and secure payment integrations (Kaspi Pay, Visa/Mastercard, Halyk), with teams in Los Angeles, Bishkek, and Almaty since 2010. We build for businesses across Kazakhstan, Central Asia, and international markets.

Read Also

SSR vs CSR vs SSG vs ISR: How to Choose a Rendering Strategy in 2026aunimeda
Web Development

SSR vs CSR vs SSG vs ISR: How to Choose a Rendering Strategy in 2026

Server-side, client-side, static and incremental rendering each win in different situations. A practical 2026 decision guide — with the SEO, performance and cost trade-offs of every approach and a per-page-type cheat sheet.

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speedaunimeda
Web Development

How to Build an MVP in 2026: A Founder's Guide to Scope, Cost, and Speed

What an MVP actually is (and isn't), how to scope it correctly, how much it costs in 2026, and how to choose the right development approach. Practical advice for founders and product teams.

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026aunimeda
Web Development

Outsource Software Development to Kyrgyzstan: A Practical Guide for 2026

Why businesses outsource software development to Kyrgyzstan in 2026, how to evaluate and hire a development team in Bishkek, and what the engagement process actually looks like.

Need IT development for your business?

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

Web Development

Get Consultation All articles