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
- Visit kaspi.kz/merchantapi
- Complete the application using:
- Company BIN or Individual Entrepreneur IIN
- A business bank account in Kazakhstan
- Verify your email address and phone number.
- Wait for approval (typically 1–3 business days).
- After approval you'll receive:
TradePointIdApiKey- 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
returnUrlandfailUrldomains have been added to the Merchant Portal. - The webhook URL is registered and consistently returns HTTP 200 within 5 seconds.
-
kaspiTransactionIdis stored in your database for refunds and reconciliation. - Every
orderIdis globally unique. - Polling is implemented as a fallback on the
returnUrlpage. - All Kaspi API errors are logged together with request details.
- Production environment uses
https://pay.kaspi.kzinstead 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.