О насБлогКонтакты
Бизнес и продукт12 июня 2015 г. 5 мин 494Обновлено: 10 июня 2026 г.

Как интегрировать Kaspi Pay в интернет-магазин на PHP (2015)

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

Как интегрировать Kaspi Pay в интернет-магазин на PHP (2015)

Коротко: Зарегистрируйтесь как мерчант в Kaspi Bank, получите merchantId и apiKey. Создайте платёжный запрос через REST API (POST JSON), перенаправьте клиента на payment_url, обработайте webhook на вашем сервере после оплаты. Проверяйте подпись HMAC-SHA256 в каждом webhook.


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

В 2015 году для подключения Kaspi Pay требовалось:

  1. ТОО или ИП с расчётным счётом в Kaspi Bank
  2. Договор на эквайринг (офисное посещение, 3-5 рабочих дней)
  3. Получить merchantId, apiKey, тестовые данные

Комиссия: 1.2-1.8% в зависимости от оборота.


Класс для работы с Kaspi Pay API

<?php
// payment/KaspiPay.php

class KaspiPay {
    private string $merchantId;
    private string $apiKey;
    private string $apiUrl;
    private bool   $testMode;

    public function __construct(string $merchantId, string $apiKey, bool $testMode = false) {
        $this->merchantId = $merchantId;
        $this->apiKey     = $apiKey;
        $this->testMode   = $testMode;
        $this->apiUrl     = $testMode
            ? 'https://api-test.kaspi.kz/pay/v1/'
            : 'https://api.kaspi.kz/pay/v1/';
    }

    /**
     * Создать платёжный запрос
     * Возвращает URL для перенаправления клиента
     */
    public function createPayment(array $order): array {
        $payload = [
            'merchantId'   => $this->merchantId,
            'orderId'      => (string)$order['id'],
            'amount'       => (int)($order['total'] * 100),  // В тиынах
            'currency'     => 'KZT',
            'description'  => 'Заказ #' . $order['id'],
            'successUrl'   => 'https://myshop.kz/order/' . $order['id'] . '/success',
            'failUrl'      => 'https://myshop.kz/order/' . $order['id'] . '/fail',
            'callbackUrl'  => 'https://myshop.kz/payment/kaspi/callback',
            'customerPhone'=> $order['phone'] ?? '',
            'expiresAt'    => date('c', strtotime('+30 minutes')),
        ];

        $payload['signature'] = $this->sign($payload);

        $response = $this->request('POST', 'payments', $payload);

        return [
            'payment_id'  => $response['paymentId'],
            'payment_url' => $response['paymentUrl'],
            'expires_at'  => $response['expiresAt'],
        ];
    }

    /**
     * Проверить статус платежа
     */
    public function getPaymentStatus(string $paymentId): array {
        $params = [
            'merchantId' => $this->merchantId,
            'timestamp'  => time(),
        ];
        $params['signature'] = $this->sign($params);

        return $this->request('GET', 'payments/' . $paymentId . '?' . http_build_query($params));
    }

    /**
     * Верификация подписи входящего webhook
     */
    public function verifyWebhook(array $data, string $receivedSignature): bool {
        $expectedSig = $this->sign($data);
        return hash_equals($expectedSig, $receivedSignature);
    }

    private function sign(array $data): string {
        // Подпись: HMAC-SHA256 от JSON + apiKey
        ksort($data);
        $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
        return hash_hmac('sha256', $payload, $this->apiKey);
    }

    private function request(string $method, string $endpoint, array $data = []): array {
        $url = $this->apiUrl . $endpoint;
        $ch  = curl_init($url);

        $headers = [
            'Content-Type: application/json',
            'X-Merchant-Id: ' . $this->merchantId,
            'X-Timestamp: ' . time(),
        ];

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_HTTPHEADER     => $headers,
        ]);

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $decoded = json_decode($response, true);

        if ($httpCode >= 400 || isset($decoded['error'])) {
            throw new RuntimeException(
                'Kaspi Pay API error: ' . ($decoded['error']['message'] ?? "HTTP $httpCode")
            );
        }

        return $decoded;
    }
}

Контроллер: инициация оплаты

<?php
// PaymentController.php

class PaymentController {
    public function initKaspi(int $orderId): void {
        $order = OrderRepository::findForUser($orderId, currentUserId());

        if (!$order || $order['status'] !== 'pending_payment') {
            redirect('/cart?error=order_not_found');
            return;
        }

        $kaspi = new KaspiPay(KASPI_MERCHANT_ID, KASPI_API_KEY, IS_PRODUCTION ? false : true);

        try {
            $payment = $kaspi->createPayment($order);

            // Сохранить payment_id для последующей сверки
            OrderRepository::update($orderId, [
                'payment_provider' => 'kaspi',
                'payment_id'       => $payment['payment_id'],
                'status'           => 'payment_initiated',
            ]);

            // Перенаправить на страницу Kaspi
            header('Location: ' . $payment['payment_url']);
            exit;

        } catch (Exception $e) {
            error_log('Kaspi payment error: ' . $e->getMessage());
            redirect('/order/' . $orderId . '?error=payment_init_failed');
        }
    }
}

Обработчик Webhook

<?php
// public/payment/kaspi/callback.php

// Kaspi отправляет POST с JSON
$rawBody = file_get_contents('php://input');
$data    = json_decode($rawBody, true);
$sig     = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Логировать все webhook для отладки
file_put_contents(
    '/var/log/kaspi_webhooks.log',
    date('Y-m-d H:i:s') . ' ' . $rawBody . "\n",
    FILE_APPEND
);

$kaspi = new KaspiPay(KASPI_MERCHANT_ID, KASPI_API_KEY);

// Проверить подпись
if (!$kaspi->verifyWebhook($data, $sig)) {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'message' => 'Invalid signature']);
    exit;
}

$orderId   = $data['orderId'] ?? null;
$paymentId = $data['paymentId'] ?? null;
$status    = $data['status'] ?? null;
$amount    = $data['amount'] ?? 0;  // В тиынах

if (!$orderId || !$paymentId) {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'message' => 'Missing fields']);
    exit;
}

$order = OrderRepository::find((int)$orderId);

if (!$order) {
    // Возвращаем 200 даже если заказ не найден - иначе Kaspi будет повторять webhook
    echo json_encode(['status' => 'ok']);
    exit;
}

if ($status === 'PAID' && $order['status'] === 'payment_initiated') {
    // Дополнительная проверка суммы
    $expectedTiyn = (int)($order['total'] * 100);
    if (abs($expectedTiyn - (int)$amount) > 1) {  // Допуск 1 тиын (округление)
        error_log("Kaspi amount mismatch: expected $expectedTiyn, got $amount for order $orderId");
        echo json_encode(['status' => 'ok']);  // Не 400 - иначе повторные webhook
        exit;
    }

    OrderRepository::markAsPaid($orderId, [
        'payment_provider'      => 'kaspi',
        'payment_transaction_id'=> $paymentId,
        'paid_amount'           => $amount / 100,  // Тиыны → тенге
        'paid_at'               => date('Y-m-d H:i:s'),
    ]);

    // Уведомить менеджера и клиента
    NotificationService::orderPaid($order);
}

// Всегда возвращаем 200 с JSON - Kaspi ожидает именно это
http_response_code(200);
echo json_encode(['status' => 'ok']);

Проверка статуса оплаты (polling для страницы успеха)

<?php
// Для страницы /order/{id}/success - проверяем что оплата действительно прошла
// Webhook может прийти позже чем пользователь вернулся на сайт

function waitForPayment(int $orderId, int $maxWaitSeconds = 10): bool {
    $kaspi = new KaspiPay(KASPI_MERCHANT_ID, KASPI_API_KEY);
    $start = time();

    while (time() - $start < $maxWaitSeconds) {
        $order = OrderRepository::find($orderId);

        if ($order['status'] === 'paid') {
            return true;  // Webhook уже обработан
        }

        // Спросить у Kaspi напрямую
        $paymentId = $order['payment_id'] ?? null;
        if ($paymentId) {
            $payment = $kaspi->getPaymentStatus($paymentId);
            if ($payment['status'] === 'PAID') {
                OrderRepository::markAsPaid($orderId, [/* ... */]);
                return true;
            }
        }

        sleep(1);
    }

    return false;
}

Почему Kaspi Pay вытеснил конкурентов в 2015

В 2015 году у Kaspi Bank было 3+ миллиона активных пользователей мобильного банка - больше чем у всех других банков Казахстана вместе. Для интернет-магазина, ориентированного на казахстанскую аудиторию, Kaspi Pay обеспечивал наибольший охват.

Конверсия на нашем тестовом проекте:

  • Без Kaspi Pay: 22% успешных оплат (только VISA/MC)
  • С Kaspi Pay: 41% успешных оплат (добавились Kaspi-пользователи)

Kaspi Pay оказался важнее всех остальных способов оплаты вместе взятых.

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

AI в e-commerce Казахстана: персонализация, рекомендации и как не отстать от Kaspi в 2026aunimeda
Бизнес и продукт

AI в e-commerce Казахстана: персонализация, рекомендации и как не отстать от Kaspi в 2026

Kaspi.kz использует AI-персонализацию уже несколько лет. Как независимые интернет-магазины Казахстана могут применить те же принципы без бюджета tech-гиганта? Разбираем доступные AI-инструменты для казахстанского e-commerce.

Как запустить интернет-магазин в Алматы с нуля - пошаговый гайдaunimeda
Бизнес и продукт

Как запустить интернет-магазин в Алматы с нуля - пошаговый гайд

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

IT аутсорсинг в Казахстане - почему компании выбирают Алматыaunimeda
Бизнес и продукт

IT аутсорсинг в Казахстане - почему компании выбирают Алматы

Почему казахстанский и международный бизнес выбирает IT аутсорсинг в Алматы. Преимущества, риски и как правильно выстроить работу с аутсорс-командой.

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

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

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