Как подключить yespay
От регистрации до первого реального платежа — примерно час работы бэкенд-разработчика. Ниже весь путь: ключи, кассир Kaspi, создание счёта, вебхук и проверка перед выходом в бой.
Что вы подключаете
yespay — прослойка между вашим приложением и Kaspi Pay. Вы дёргаете наш REST API, мы выставляем счёт в Kaspi от имени вашего кассира, покупатель платит в приложении Kaspi, деньги приходят напрямую на ваш счёт Kaspi Business. yespay денег не касается — мы отдаём вам подписанное событие о том, что счёт оплачен.
Что нужно иметь до старта
Действующий Kaspi Business (ИП или ТОО) и сотрудник с ролью «Кассир» в нём. Для тестового режима не нужно ничего: тестовый ключ выдаётся сразу при регистрации и работает без кассира.
Быстрый старт
Зарегистрируйтесь и возьмите ключи
В кабинете откройте раздел «API-ключи». У каждого мерчанта два независимых набора ключей:
| Ключ | Режим | Что делает |
|---|---|---|
sk_test_… | test | Работает на мок-провайдере. Реального Kaspi и реальных денег нет. |
sk_live_… | live | Выставляет настоящие счета через подключённого кассира Kaspi. |
Секретный ключ показывается один раз
Мы храним только его хеш и не можем восстановить ключ. Потеряли — выпускайте новый и отзывайте старый. Секретный ключ живёт только на сервере: во фронтенде, в мобильном приложении и в репозитории ему не место.
Создайте первый платёж в тестовом режиме
Сумма — целое число в тенге, не в тиынах: 14990 означает 14 990 ₸. Дробные суммы отклоняются, потому что поведение Kaspi на дробных значениях не подтверждено.
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"
}'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
Один ключ — один счёт. Повтор запроса с тем же ключом вернёт тот же платёж, а не создаст второй. Разумный выбор — идентификатор заказа в вашей системе. Подробности: идемпотентность.
Поднимите обработчик вебхуков
Вебхук — основной способ узнать об оплате. Создайте эндпоинт в кабинете (раздел «Вебхуки») или через POST /v1/webhooks; в ответ вы один раз получите подписной секрет whsec_….
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 — оно доставляется по той же схеме, что и боевое.
Опрос статуса — запасной вариант
Вебхуки — основной канал, но если ваш обработчик лежал, статус всегда можно прочитать напрямую. Опрашивайте не чаще раза в 3–5 секунд и прекращайте, когда статус стал финальным.
curl https://api.getyespay.com/v1/payments/pay_3Nq8xVb2... \
-H "Authorization: Bearer sk_test_..."Полная история переходов платежа — GET /v1/payments/:id/events. Там же виден сырой статус провайдера: это первое, что стоит приложить к обращению в поддержку.
Подключение Kaspi
Боевой режим требует один раз подключить кассира. Мы работаем правами штатной роли «Кассир» в вашем Kaspi Business — никаких доверенностей и никакого доступа к деньгам сверх выставления счетов.
- В кабинете откройте Интеграции → Kaspi и нажмите «Подключить кассира».
- Введите номер сотрудника с ролью «Кассир» в формате
+7 7XX XXX XX XX. Это должен быть телефон, к которому у вас есть доступ прямо сейчас: на него придёт SMS. - Введите код из SMS. Код нигде не сохраняется, не пишется в логи и не возвращается по API — он обменивается на сессию и уничтожается.
- Выберите организацию, если их у аккаунта несколько. После этого кассир получит статус 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_blocked | Kaspi заблокировал доступ этому сотруднику. Решается на стороне Kaspi Business, не у нас. |
provider_unavailable | Kaspi недоступен либо у вашего сервера сбиты часы. Подпись 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-документация. Если интеграцию пишет ИИ-агент, отдайте ему готовые промты.
