Как подключить yespay

От регистрации до первого реального платежа — примерно час работы бэкенд-разработчика. Ниже весь путь: ключи, кассир Kaspi, создание счёта, вебхук и проверка перед выходом в бой.

Что вы подключаете

yespay — прослойка между вашим приложением и Kaspi Pay. Вы дёргаете наш REST API, мы выставляем счёт в Kaspi от имени вашего кассира, покупатель платит в приложении Kaspi, деньги приходят напрямую на ваш счёт Kaspi Business. yespay денег не касается — мы отдаём вам подписанное событие о том, что счёт оплачен.

Что нужно иметь до старта

Действующий Kaspi Business (ИП или ТОО) и сотрудник с ролью «Кассир» в нём. Для тестового режима не нужно ничего: тестовый ключ выдаётся сразу при регистрации и работает без кассира.

Быстрый старт

1

Зарегистрируйтесь и возьмите ключи

В кабинете откройте раздел «API-ключи». У каждого мерчанта два независимых набора ключей:

КлючРежимЧто делает
sk_test_…testРаботает на мок-провайдере. Реального Kaspi и реальных денег нет.
sk_live_…liveВыставляет настоящие счета через подключённого кассира Kaspi.

Секретный ключ показывается один раз

Мы храним только его хеш и не можем восстановить ключ. Потеряли — выпускайте новый и отзывайте старый. Секретный ключ живёт только на сервере: во фронтенде, в мобильном приложении и в репозитории ему не место.

2

Создайте первый платёж в тестовом режиме

Сумма — целое число в тенге, не в тиынах: 14990 означает 14 990 ₸. Дробные суммы отклоняются, потому что поведение Kaspi на дробных значениях не подтверждено.

bash
curl -X POST https://api.getyespay.com/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-1001" \
  -d '{
    "amount": 14990,
    "currency": "KZT",
    "customer": { "phone": "+77001234567" },
    "order_id": "ORDER-1001",
    "description": "Заказ №1001"
  }'
json
HTTP/1.1 201 Created

{
  "id": "pay_3Nq8xVb2...",
  "object": "payment",
  "status": "pending",
  "amount": 14990,
  "currency": "KZT",
  "order_id": "ORDER-1001",
  "customer": { "phone": "+77001234567" },
  "provider": "kaspi",
  "method": "remote_invoice",
  "receipt_url": null,
  "qr": null,
  "livemode": false,
  "refunded_amount": 0,
  "expires_at": "2026-09-06T09:15:00.000Z",
  "paid_at": null,
  "created_at": "2026-09-06T09:00:00.000Z",
  "metadata": null
}

Ответ приходит сразу со статусом pending: счёт создан и ждёт покупателя. Финальный статус вы узнаете из вебхука или опросом — шаги 3 и 4.

Всегда шлите Idempotency-Key

Один ключ — один счёт. Повтор запроса с тем же ключом вернёт тот же платёж, а не создаст второй. Разумный выбор — идентификатор заказа в вашей системе. Подробности: идемпотентность.

3

Поднимите обработчик вебхуков

Вебхук — основной способ узнать об оплате. Создайте эндпоинт в кабинете (раздел «Вебхуки») или через POST /v1/webhooks; в ответ вы один раз получите подписной секрет whsec_….

javascript
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.YESPAY_WEBHOOK_SECRET;

// ВАЖНО: подпись считается по СЫРОМУ телу. JSON.parse -> JSON.stringify
// меняет байты и ломает проверку.
app.post('/webhooks/yespay', express.raw({ type: 'application/json' }), async (req, res) => {
  const header = req.get('X-Yespay-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const timestamp = Number(parts.t);
  const raw = req.body.toString('utf8');

  // 1. Возраст события — защита от переигрывания старой записи.
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return res.status(400).send('stale');
  }

  // 2. Подпись — сравнение в постоянном времени.
  const expected = crypto.createHmac('sha256', SECRET).update(timestamp + '.' + raw).digest('hex');
  const ok =
    parts.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!ok) return res.status(400).send('bad signature');

  const event = JSON.parse(raw);

  // 3. Дедупликация: повтор того же X-Yespay-Event-Id — не второй платёж.
  if (await alreadyProcessed(req.get('X-Yespay-Event-Id'))) return res.status(200).send('ok');

  if (event.type === 'payment.paid') {
    await markOrderPaid(event.data.order_id, event.data.id, event.data.amount);
  }

  // 4. Отвечайте 2xx быстро; тяжёлую работу — в очередь.
  res.status(200).send('ok');
});
  • URL должен быть публичным https://. Приватные адреса (localhost, 10.x, 192.168.x, 169.254.x) отклоняются и при создании эндпоинта, и на каждой доставке — это защита от SSRF.
  • Ответьте 2xx в течение 10 секунд. Любой другой ответ или таймаут означает повторную доставку: до 6 попыток с задержкой от 5 секунд до часа.
  • Повторы неизбежны, поэтому обработчик обязан быть идемпотентным. Ключ дедупликации — заголовок X-Yespay-Event-Id: он стабилен между попытками.

Пока не сделано ни одного платежа, проверить подпись можно тестовым событием: POST /v1/webhooks/test — оно доставляется по той же схеме, что и боевое.

4

Опрос статуса — запасной вариант

Вебхуки — основной канал, но если ваш обработчик лежал, статус всегда можно прочитать напрямую. Опрашивайте не чаще раза в 3–5 секунд и прекращайте, когда статус стал финальным.

bash
curl https://api.getyespay.com/v1/payments/pay_3Nq8xVb2... \
  -H "Authorization: Bearer sk_test_..."

Полная история переходов платежа — GET /v1/payments/:id/events. Там же виден сырой статус провайдера: это первое, что стоит приложить к обращению в поддержку.

Подключение Kaspi

Боевой режим требует один раз подключить кассира. Мы работаем правами штатной роли «Кассир» в вашем Kaspi Business — никаких доверенностей и никакого доступа к деньгам сверх выставления счетов.

  1. В кабинете откройте Интеграции → Kaspi и нажмите «Подключить кассира».
  2. Введите номер сотрудника с ролью «Кассир» в формате +7 7XX XXX XX XX. Это должен быть телефон, к которому у вас есть доступ прямо сейчас: на него придёт SMS.
  3. Введите код из SMS. Код нигде не сохраняется, не пишется в логи и не возвращается по API — он обменивается на сессию и уничтожается.
  4. Выберите организацию, если их у аккаунта несколько. После этого кассир получит статус connected, и ключи sk_live_… начнут работать.

Kaspi разрешает одну активную сессию на устройство

Если тот же кассир заново войдёт в приложение Kaspi на телефоне, нашу сессию может вытеснить. Тогда создание платежей начнёт отвечать ошибкой payment_account_reauth_required, а вы получите событие payment_account.reauth_required — нужно повторить подключение, SMS придёт снова. Практическое следствие: подключайте отдельного сотрудника-кассира, а не личный номер владельца.

Что делать при ошибке подключения

Что видитеЧто это значит
Номер не принадлежит ни одному оператору РКНомер введён не как казахстанский мобильный. Проверьте префикс: 700–709, 747, 750, 751, 760–764, 771, 775–778.
payment_account_blockedKaspi заблокировал доступ этому сотруднику. Решается на стороне Kaspi Business, не у нас.
provider_unavailableKaspi недоступен либо у вашего сервера сбиты часы. Подпись Kaspi привязана ко времени: расхождение больше 15 секунд ломает вход.

Тестовый режим и тестовые номера

С ключом sk_test_… работает мок-провайдер: настоящий Kaspi не вызывается, SMS не уходит, деньги не двигаются. Сценарий платежа выбирается по двум последним цифрам номера покупателя — так можно прогнать все ветки, включая те, которые в бою вы вряд ли увидите в удобный момент.

Номер покупателяЧто произойдётЧерез сколько
+7 700 123 45 00платёж уходит в failed≈5 секунд
+7 700 123 45 11провайдер отвечает нераспознанным статусом → unknown≈5 секунд
+7 700 123 45 77навсегда остаётся в pendingникогда
+7 700 123 45 99платёж уходит в expired≈20 секунд
любой другойплатёж уходит в paid≈8 секунд

Обязательно прогоните ветку unknown

Статус unknown означает «мы не смогли определить исход». Это не отказ. Код, который трактует unknown как неуспех и отпускает товар или возвращает деньги, однажды сделает это по оплаченному заказу. Правильная реакция — придержать заказ и дождаться следующего события или ручной проверки.

Чек-лист перед выходом в бой

  • Секретный ключ только на сервере. Не в бандле фронтенда, не в мобильном приложении, не в git.
  • Idempotency-Key на каждом создании платежа и возврата. Иначе ретрай вашей очереди — это второй счёт покупателю.
  • Подпись вебхука проверяется по сырому телу, сравнивается в постоянном времени, а событие старше 5 минут отклоняется.
  • Обработчик вебхука идемпотентен по X-Yespay-Event-Id и отвечает 2xx быстрее 10 секунд.
  • Ветки failed, expired, unknown и «навсегда pending» проверены на тестовых номерах выше.
  • Сумма считается на сервере, а не приходит из браузера, и передаётся целым числом тенге.
  • Заказ помечается оплаченным по вебхуку или по API, а не по факту возврата покупателя на страницу.
  • Часы сервера синхронизированы по NTP. Дрейф ломает и подпись Kaspi, и проверку возраста вебхука.
  • Есть реакция на reauth. Событие payment_account.reauth_required должно доходить до живого человека, а не в лог.
  • Боевой платёж проверен на маленькой сумме и возвращён через POST /v1/refunds.

Дальше

Полный справочник по эндпоинтам, статусам и кодам ошибок — API-документация. Если интеграцию пишет ИИ-агент, отдайте ему готовые промты.