Документация KAPSULA
Платёжный шлюз для приёма платежей через СБП на сайтах и в ботах. Полное руководство — от регистрации до интеграции и проверки подписи webhook'ов.
О платформе
KAPSULA — платёжный посредник между вами (мерчантом) и платёжной системой. Мы упрощаем интеграцию: вместо того чтобы проходить верификацию у банка-эквайера месяцами, вы регистрируетесь у нас за 5 минут, проходите модерацию и принимаете оплаты.
Сейчас поддерживается:
- СБП (Система Быстрых Платежей) — оплата через QR-код в банковском приложении
- Валюта — только
RUB - Сумма — от 50 ₽ до 15 000 ₽ за один платёж
В планах: банковские карты (когда будут пройдены требования PCI DSS).
https://api.kapsula.pro/v1Быстрый старт
Минимальная интеграция — 5 шагов и около 10 минут.
- Зарегистрируйтесь — kapsula.pro/auth
- Создайте кассу в кабинете → Кассы. Укажите сайт или бота
- Пройдите верификацию (для сайтов — загрузка HTML-файла) и дождитесь модерации
- Получите API-ключ вида
pk_live_...на странице кассы - Сделайте первый запрос:
В ответе придётcurl -X POST https://api.kapsula.pro/v1/payment/create \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "amount": 50000, "order_id": "test-1", "mode": "hosted" }'payment_url— откройте его в браузере и протестируйте оплату с телефона.
Регистрация
Откройте страницу авторизации и заполните форму:
- Email — будет использоваться для входа
- Пароль — минимум 8 символов, должен содержать буквы и цифры
Подтверждение по email сейчас не требуется — после регистрации вы сразу попадаете в кабинет.
Создание кассы
«Касса» — это отдельная точка приёма платежей: один сайт или один бот. У вас может быть сколько угодно касс.
Перейдите в Кассы → Создать кассу. Заполните 2 шага:
Шаг 1. Тип кассы
- 🌐 Сайт — для интернет-магазина или сервиса с сайтом. Потребуется верификация (загрузка файла на сайт)
- 🤖 Бот — для Telegram-ботов. Верификация не требуется, идёт сразу на модерацию
Шаг 2. Реквизиты
- Название — отобразится покупателям на странице оплаты
- URL — адрес сайта или ссылка на бота (
t.me/...) - Описание — что вы продаёте (1–2 предложения для модератора)
Верификация сайта
Чтобы подтвердить что вы владеете сайтом — нужно загрузить наш файл в его корень.
- В кабинете кассы нажмите «Скачать файл верификации» — скачается
kapsula-verify-<токен>.html - Загрузите файл в корень вашего сайта так чтобы он открывался по адресу:
https://ваш-сайт.com/kapsula-verify-<токен>.html - В кабинете нажмите «Проверить» — мы скачаем файл и убедимся что токен совпадает
- После успешной проверки касса автоматически уйдёт на модерацию
pending_moderation.Модерация
После верификации модератор проверяет вашу кассу: законность деятельности, соответствие правилам платёжной системы, наличие политики конфиденциальности и оферты на сайте.
- ⏱ Срок модерации: обычно 1–3 рабочих дня
- ✅ После одобрения статус становится
activeи автоматически генерируются API-ключ и webhook secret - ❌ В случае отказа — придёт уведомление с причиной, можно исправить и подать снова
API-ключи
После одобрения кассы в её карточке появятся два ключа:
| Ключ | Префикс | Назначение |
|---|---|---|
| API-ключ | pk_live_ | Авторизация запросов от вашего сервера к нашему API |
| Webhook secret | whsec_ | Проверка подписи входящих webhook-уведомлений |
Оба ключа можно пересоздать в любой момент — старые сразу перестают работать.
Безопасность
Платёжная система обрабатывает деньги — безопасность критична. Минимальные правила:
1. Хранение ключей
- Не коммитьте API-ключ и webhook secret в git. Используйте переменные окружения (
.envфайл, добавленный в.gitignore) - Не передавайте API-ключ на клиент (в JS-фронтенде, в мобильное приложение). Любой вызов к нашему API делается с вашего сервера
- Ротация: при подозрении на компрометацию сразу пересоздайте ключ через кабинет
2. HTTPS обязателен
- Webhook URL должен быть
https://— мы блокируемhttp://и приватные IP - Возвратные URL (success/fail) тоже должны быть HTTPS
3. Проверка подписи webhook
Каждый webhook от нас подписан HMAC-SHA256. Всегда проверяйте подпись — иначе злоумышленник может прислать вам поддельное «уведомление об оплате» и вы выдадите товар бесплатно.
Подробности с примерами на разных языках — в разделе Подпись webhook.
4. Идемпотентность
Webhook может прийти несколько раз для одного и того же события (например, при сбое сети мы повторим). На вашей стороне используйте payment.id для дедупликации — если уже обработали, не выдавайте товар повторно.
5. Сверяйте сумму
Перед выдачей товара в обработчике webhook'а проверяйте что payment.amount совпадает с ожидаемой суммой заказа. Это защищает от ситуации когда ваш сервис создал заказ на 1000 ₽, а кто-то заплатил 500 ₽.
6. Сверяйте order_id
Используйте поле order_id чтобы привязать платёж к вашему заказу. В webhook'е оно вернётся в payment.order_id. Это защищает от подмены.
Авторизация
Все запросы к публичному API требуют заголовок:
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Если ключ невалиден или касса не активна — вернём 401 или 403.
Жизненный цикл платежа
Режим Hosted (рекомендуется)
Самый простой способ. Покупатель видит страницу оплаты на нашем домене (pay.kapsula.pro/p/...) — мы обеспечиваем UI, QR, обработку статусов.
- Создаёте платёж с
mode: "hosted" - В ответе получаете
payment_url - Перенаправляете покупателя туда (
302 Redirectили открываете в новом окне) - После оплаты мы:
- Шлём webhook на ваш
webhook_url - Перенаправляем покупателя на ваш
success_url(илиfail_urlпри отказе)
- Шлём webhook на ваш
Режим Host (свой UI)
Если нужно полностью кастомное оформление — используйте режим host. Мы вернём вам данные QR, дальше вы рисуете и обрабатываете всё сами.
- Создаёте платёж с
mode: "host" - В ответе получаете
qr.data(PNG в base64) иqr.link(deeplink наqr.nspk.ru) - Рисуете QR на своей странице:
<img src="data:image/png;base64,..." alt="СБП QR"> - Опционально — кнопка «Открыть в банке» с
qr.link(для оплаты с того же телефона) - Опрашиваете
GET /payment/:id/statusкаждые 2–3 секунды или просто ждёте webhook
Создание платежа
Параметры запроса
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
amount | integer | да | Сумма в копейках. От 5000 (50 ₽) до 1500000 (15 000 ₽) |
currency | string | нет | Только RUB (по умолчанию) |
method | string | нет | Только sbp (по умолчанию) |
mode | string | нет | hosted (по умолчанию) или host |
order_id | string | нет | Ваш ID заказа (до 128 символов). Вернётся в webhook'е |
description | string | нет | Описание (до 500 символов). Видит покупатель |
customer_email | string | нет | Email покупателя (для будущих чеков) |
success_url | string | нет | Override для hosted-режима |
fail_url | string | нет | Override для hosted-режима |
Примеры запроса
curl -X POST https://api.kapsula.pro/v1/payment/create \
-H "Authorization: Bearer $KAPSULA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"order_id": "order_12345",
"description": "Подписка Premium на месяц",
"mode": "hosted",
"customer_email": "user@example.com"
}'
// Node.js (fetch — встроен в Node 18+)
const res = await fetch('https://api.kapsula.pro/v1/payment/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KAPSULA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 50000,
order_id: order.id,
description: 'Подписка Premium',
mode: 'hosted',
customer_email: order.customerEmail,
}),
});
if (!res.ok) throw new Error('Kapsula error: ' + await res.text());
const payment = await res.json();
// Сохраните payment.id у себя для последующего матчинга с webhook
await db.orders.update(order.id, { kapsula_payment_id: payment.id });
return res.redirect(payment.payment_url);
<?php
$ch = curl_init('https://api.kapsula.pro/v1/payment/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('KAPSULA_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 50000,
'order_id' => $order_id,
'description' => 'Подписка Premium',
'mode' => 'hosted',
]),
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($code !== 200) {
error_log("Kapsula error: $resp");
die('Payment error');
}
$payment = json_decode($resp, true);
header('Location: ' . $payment['payment_url']);
exit;
import os, requests
def create_kapsula_payment(amount_kop: int, order_id: str, description: str = ''):
r = requests.post(
'https://api.kapsula.pro/v1/payment/create',
headers={
'Authorization': f"Bearer {os.environ['KAPSULA_API_KEY']}",
'Content-Type': 'application/json',
},
json={
'amount': amount_kop,
'order_id': order_id,
'description': description,
'mode': 'hosted',
},
timeout=15,
)
r.raise_for_status()
return r.json()
# использование во Flask:
@app.route('/checkout/<order_id>')
def checkout(order_id):
order = Order.get(order_id)
payment = create_kapsula_payment(int(order.amount * 100), order.id, order.title)
order.kapsula_payment_id = payment['id']
order.save()
return redirect(payment['payment_url'])
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
type CreateReq struct {
Amount int `json:"amount"`
OrderID string `json:"order_id"`
Description string `json:"description"`
Mode string `json:"mode"`
}
type CreateResp struct {
ID string `json:"id"`
PaymentURL string `json:"payment_url"`
}
func createPayment(amount int, orderID string) (*CreateResp, error) {
body, _ := json.Marshal(CreateReq{amount, orderID, "", "hosted"})
req, _ := http.NewRequest("POST", "https://api.kapsula.pro/v1/payment/create", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("KAPSULA_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
if resp.StatusCode != 200 { return nil, fmt.Errorf("kapsula: HTTP %d", resp.StatusCode) }
var p CreateResp
json.NewDecoder(resp.Body).Decode(&p)
return &p, nil
}
Ответ — Hosted
{
"id": "pay_a1b2c3d4e5f6789012345678abcdef01",
"status": "awaiting_confirmation",
"amount": 50000,
"currency": "RUB",
"method": "sbp",
"mode": "hosted",
"payment_url": "https://pay.kapsula.pro/p/pay_a1b2c3d4e5f6789012345678abcdef01",
"created_at": 1714478400000,
"expires_at": 1714480200000
}
Ответ — Host
{
"id": "pay_a1b2c3d4e5f6789012345678abcdef01",
"status": "awaiting_confirmation",
"amount": 50000,
"currency": "RUB",
"method": "sbp",
"mode": "host",
"qr": {
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA...",
"link": "https://qr.nspk.ru/AS10000XXXXXXXXX?type=02&bank=1000..."
},
"qr_pending": false,
"created_at": 1714478400000,
"expires_at": 1714480200000
}
qr_pending: true — QR ещё не пришёл от платёжной системы за 8 секунд (бывает редко). В этом случае дёрните GET /payment/:id через 1–2 секунды.Получение информации о платеже
Полная информация о платеже.
curl https://api.kapsula.pro/v1/payment/pay_a1b2c3d4e5f6... \
-H "Authorization: Bearer $KAPSULA_API_KEY"
Лёгкий запрос для polling — только статус и время оплаты:
{ "status": "success", "paid_at": 1714478560000 }
Статусы платежа
| Статус | Финальный? | Описание |
|---|---|---|
pending | Нет | Создан, ждём ответа от шлюза |
awaiting_confirmation | Нет | QR выдан, ждём оплату |
success | Да | Платёж успешно прошёл |
decline | Да | Отклонён банком |
expired | Да | QR истёк (30 минут не было оплаты) |
error | Да | Внутренняя ошибка |
Подписки: как это работает
Подписка — это регулярные списания через СБП. Покупатель один раз оплачивает и подтверждает в приложении банка согласие на автосписания, после чего деньги списываются автоматически по заданному расписанию. Карты для подписок пока недоступны, работает только СБП.
- Вы создаёте подписку запросом
POST /v1/subscription/createи получаетеpayment_url. - Покупатель открывает ссылку, видит условия (сумма, периодичность, как отключить) и оплачивает первый платёж по QR.
- В момент оплаты банк спрашивает согласие на регулярные списания — счёт привязывается.
- Вам приходит webhook
subscription.activated— открывайте доступ к продукту. - Дальше в каждую дату списания мы сами списываем деньги и шлём
subscription.charged. - Если покупатель отключит подписку — придёт
subscription.canceled, закрывайте доступ.
Подключение подписок
Подписки не включены по умолчанию — их нужно активировать для конкретной кассы.
- Напишите в поддержку, что вам нужны подписки. Мы проверим ваш сценарий и откроем их для кассы. Отдельная проверка связана с требованиями банка-эквайера к регулярным списаниям.
- После активации в кабинете на странице кассы (Кассы → управление кассой → Подписки) появится переключатель. Он включён сразу, вы можете выключить его в любой момент.
- Дальше используйте тот же ключ
pk_live_..., что и для обычных платежей — отдельного ключа для подписок нет.
/cancel или в разделе «Подписки» кабинета.
Если подписки не подключены, POST /v1/subscription/create вернёт 403:
{ "error": "Подписки для этой кассы не подключены. Напишите в поддержку, чтобы их активировали." }
Чтение и отмена подписок работают всегда, даже при выключенном тумблере — вы сможете корректно погасить действующие.
Отдельно возможна временная пауза со стороны платформы — например, при работах
на стороне банка. Тогда create вернёт 503:
{ "error": "Подписки временно недоступны на стороне платформы. Попробуйте позже или свяжитесь с поддержкой." }
Это временное состояние, ваши настройки при этом сохраняются. Обрабатывайте
503 как «повторить позже», а не как отказ.
POST /v1/subscription/create
Создаёт подписку и ссылку на первую оплату.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
amount | integer | да | Сумма одного списания в копейках. 5000–1500000 (50–15000 ₽). Строка не принимается. Ниже 50 ₽ нельзя — это ограничение банка, а не наше: такие платежи отклоняются с ошибкой 101300 Limits exceeded |
period | string | да | day, week, month, quarter, year. Плюс minute и hour — только в тестовом режиме, см. Тестовый режим |
customer_email | string | да | Email покупателя. На него уходят квитанции и ссылка отмены |
interval | integer | нет | 1–100. Например period=week, interval=2 — раз в две недели. По умолчанию 1 |
description | string | нет | Название подписки, видно покупателю. До 500 символов |
order_id | string | нет | Ваш внутренний идентификатор, до 128 символов |
max_retries | integer | нет | Сколько раз повторять неудачное списание, 0–10. По умолчанию 3 |
max_charges | integer | нет | Остановить подписку после N списаний, 1–1000. По умолчанию без ограничения, а для тестовых периодов — 10 |
success_url / fail_url | string | нет | Куда вернуть покупателя после первой оплаты |
mode | string | нет | hosted (по умолчанию) — оплата на нашей странице. host — вы получаете QR данными и рисуете экран сами, см. Свой экран первой оплаты. Режим выдаётся по запросу |
curl -X POST https://api.kapsula.pro/v1/subscription/create \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 25000,
"period": "week",
"interval": 1,
"customer_email": "user@example.com",
"description": "Подписка Премиум",
"order_id": "sub-42"
}'
Ответ:
{
"id": "sub_1f389543072e66d1bf76b0286444811a",
"status": "pending",
"amount": 25000,
"period": "week",
"interval": 1,
"period_label": "раз в неделю",
"payment_url": "https://pay.kapsula.pro/p/pay_a16d...",
"cancel_url": "https://pay.kapsula.pro/s/1e17549ac831..."
}
payment_url — отправьте покупателя туда оплатить.
cancel_url — постоянная ссылка на страницу управления подпиской: покажите её в личном
кабинете покупателя или в письме. Она работает без пароля и регистрации.
GET /v1/subscription/:id
curl https://api.kapsula.pro/v1/subscription/sub_1f38... \
-H "Authorization: Bearer pk_live_..."
| Статус | Описание |
|---|---|
pending | Создана, первая оплата ещё не прошла |
active | Работает, списания идут по расписанию |
past_due | Списание не прошло, повторим через сутки |
canceled | Отключена. Причина в поле cancel_reason |
Поле cancel_reason: customer — отключил покупатель, merchant — отключили вы,
payment_failed — исчерпаны попытки списания, initial_failed — первая оплата не прошла.
GET /v1/subscription/:id/charges
История списаний по подписке. Параметры limit (до 100) и offset.
{
"subscription_id": "sub_1f38...",
"total": 3,
"charges": [
{ "id": "pay_...", "type": "recurring", "status": "success",
"amount": 25000, "paid_at": 1786645967853 }
]
}
type: initial — первая оплата с привязкой счёта, recurring — автосписание.
POST /v1/subscription/:id/cancel
Отключает подписку. Идемпотентно: повторный вызов вернёт тот же результат. Списания прекращаются сразу.
curl -X POST https://api.kapsula.pro/v1/subscription/sub_1f38.../cancel \
-H "Authorization: Bearer pk_live_..."
Webhook подписки
Приходят на тот же webhook_url кассы и подписываются тем же ключом
(заголовок X-Kapsula-Signature, проверка — как в разделе
Проверка подписи).
| Событие | Когда | Что делать |
|---|---|---|
subscription.activated | Первая оплата прошла, счёт привязан | Открыть доступ |
subscription.charged | Прошло очередное списание | Продлить доступ |
subscription.failed | Списание не прошло, будет повтор | Предупредить или ограничить |
subscription.canceled | Подписка отключена | Закрыть доступ |
{
"event": "subscription.charged",
"subscription": {
"id": "sub_1f38...", "order_id": "sub-42", "status": "active",
"amount": 25000, "currency": "RUB", "period": "week", "interval": 1,
"charges_count": 3, "charges_total": 75000,
"next_charge_at": 1787250767853,
"cancel_url": "https://pay.kapsula.pro/s/1e17549ac831..."
},
"payment": { "id": "pay_...", "status": "success", "amount": 25000 },
"timestamp": 1786645967853
}
payment.* не приходит.
Чтобы вы не получали два уведомления об одном событии, по списаниям подписки шлётся
только subscription.*.
Ссылка управления подпиской
У каждой подписки есть постоянная ссылка вида
https://pay.kapsula.pro/s/<токен>. На ней покупатель видит сумму,
периодичность, дату следующего списания, историю платежей и может отключить подписку
в один клик — без пароля и регистрации.
Ссылка не протухает и не меняется за всё время жизни подписки. Её можно спокойно сохранить у себя в базе.
Получить её можно четырьмя способами — берите любой удобный:
| Откуда | Поле |
|---|---|
Ответ POST /v1/subscription/create | cancel_url |
Ответ GET /v1/subscription/:id | cancel_url |
Любой webhook subscription.* | subscription.cancel_url |
| Кабинет KAPSULA, раздел «Подписки» | кнопка «Скопировать ссылку управления» |
Мы уже показываем эту ссылку покупателю: на экране после первой оплаты и в каждом письме — при оформлении, после каждого списания, при проблеме с оплатой и за сутки до следующего списания.
От вас нужно одно: показать её и у себя — в личном кабинете покупателя, в письме или в боте, рядом с описанием подписки. Либо сделать свою кнопку «Отменить» и дёргать по ней
POST /v1/subscription/:id/cancel. Чем очевиднее отмена,
тем меньше споров с банком.
Настройка и оформление страниц подписки
В подписке участвуют две страницы на нашем домене. Ниже — что на них настраивается и как заменить их своими.
Страница первой оплаты
Это обычная страница оплаты pay.kapsula.pro/p/<id>, на которой дополнительно
показан блок с условиями подписки: сколько спишется сейчас, сколько и как часто далее,
и как отключить. Блок обязателен и убрать его нельзя — прозрачные условия до оплаты
требует банк-эквайер.
| Что настраивается | Чем |
|---|---|
| Название подписки на странице и в письмах | description при создании |
| Имя продавца в шапке | название кассы в кабинете KAPSULA |
| Куда вернуть покупателя после оплаты | success_url |
| Куда вернуть при отказе | fail_url |
| Полностью своё оформление экрана | mode: "host", см. ниже |
host — мы отдаём данные QR,
вы рисуете экран сами. Подробности ниже в разделе
Свой экран первой оплаты.
Страница управления подпиской
По умолчанию покупатель попадает на нашу готовую страницу pay.kapsula.pro/s/<токен>.
Там видно статус, сумма, периодичность, дата следующего списания, история платежей и кнопка
отключения. Настраиваются на ней те же description и название кассы, остальное —
наше оформление.
Своя страница вместо нашей
Если хотите держать управление подпиской полностью у себя в дизайне — наша страница не обязательна. Всё, что на ней есть, доступно через API:
| Что нужно на странице | Откуда взять |
|---|---|
| Статус, сумма, период, дата следующего списания | GET /v1/subscription/:id |
| История списаний | GET /v1/subscription/:id/charges |
| Кнопка отключения | POST /v1/subscription/:id/cancel |
// страница подписки в вашем интерфейсе
const sub = await fetch(`https://api.kapsula.pro/v1/subscription/${id}`, {
headers: { Authorization: `Bearer ${API_KEY}` }
}).then(r => r.json())
// sub.status active | past_due | pending | canceled
// sub.amount сумма списания в копейках
// sub.period_label раз в неделю
// sub.next_charge_at дата следующего списания
// кнопка Отключить подписку
await fetch(`https://api.kapsula.pro/v1/subscription/${id}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}` }
})
pk_live_ нельзя отдавать в браузер покупателю. С фронтенда дёргайте свой эндпоинт,
а он уже наш API.И даже со своей страницей отмена должна остаться заметной и работать в один клик — это то, на что смотрит банк при разборе спорных платежей.
Свой экран первой оплаты (режим host)
По умолчанию первая оплата подписки идёт на нашей странице. Если нужно полностью своё
оформление — передайте mode: "host" при создании, и вместо ссылки вы получите
данные QR для привязки счёта. Дальше экран рисуете вы.
Что обязательно показать на своём экране
На нашей странице условия подписки показываются всегда. Когда экран ваш, эта ответственность переходит к вам, а спорные операции по подписке прилетают нам обоим. Поэтому до подтверждения оплаты покупатель должен видеть:
- сумму первого платежа и сумму регулярных списаний;
- периодичность — например «раз в неделю»;
- дату следующего списания;
- как отключить подписку.
Ссылку на отмену мы в любом случае отправим покупателю письмом сами — customer_email
обязателен. Но на экране оплаты условия должны быть до того, как он подтвердит списание.
Создание
curl -X POST https://api.kapsula.pro/v1/subscription/create \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 25000,
"period": "week",
"customer_email": "user@example.com",
"description": "Подписка Премиум",
"mode": "host"
}'
Ответ:
{
"id": "sub_...",
"status": "pending",
"mode": "host",
"amount": 25000,
"period": "week",
"period_label": "раз в неделю",
"payment_id": "pay_...",
"qr": {
"data": "data:image/png;base64,iVBORw0KGgo...",
"link": "https://sub.nspk.ru/AB1S000T0KQ...?type=03"
},
"qr_pending": false,
"cancel_url": "https://pay.kapsula.pro/s/..."
}
| Поле | Что это |
|---|---|
qr.data | Картинка QR в формате data-URI. Вставляется прямо в <img src=> |
qr.link | Ссылка СБП. На мобильном ведите по ней — покупатель не может сканировать свой же экран |
qr_pending | true — QR не успел прийти за 8 секунд. Не ошибка, опросите GET /v1/subscription/:id |
payment_url | В режиме host не возвращается |
sub.nspk.ru с
type=03 и без параметра суммы — это нормально. Обычный платёжный QR идёт на
qr.nspk.ru с type=02. Если видите второй вариант — привязка не зарегистрировалась,
напишите нам.
Если QR не пришёл сразу
QR приезжает к нам callback-ом от банка, обычно за пару секунд. Мы ждём его до 8 секунд
и отдаём в ответе. Если не успел — qr_pending: true, опросите статус:
GET https://api.kapsula.pro/v1/subscription/sub_...
{ "status": "pending", "mode": "host", "qr": { "data": "...", "link": "..." }, "qr_pending": false }
QR отдаётся, только пока подписка ждёт первой оплаты. После оплаты, отмены или истечения
срока поле qr станет null — по нему уже нельзя платить.
Как понять, что оплата прошла
Так же, как в обычном режиме: приходит webhook subscription.activated.
Либо опрашивайте GET /v1/subscription/:id — статус сменится с pending
на active, появится next_charge_at.
QR живёт 30 минут. Если за это время не оплатили, подписка закрывается со статусом
canceled и причиной initial_failed — создавайте новую.
Тестовый режим
Чтобы проверить автосписания, не дожидаясь суток, для кассы можно включить тестовый режим. В нём становятся доступны короткие периоды:
| Период | Что делает |
|---|---|
minute | Списание раз в минуту |
hour | Списание раз в час |
Работают вместе с interval — например period: "minute", interval: 5
даёт списание раз в 5 минут.
curl -X POST https://api.kapsula.pro/v1/subscription/create \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"period": "minute",
"customer_email": "user@example.com",
"description": "Проверка автосписаний"
}'
max_charges. По завершении подписка получает статус
canceled с причиной completed.
Что ещё отличается в тестовом режиме:
- письмо-напоминание за сутки не отправляется — списание всегда ближе, чем через сутки;
- неудачное списание повторяется через 2 минуты, а не через сутки.
Режим включается по запросу в поддержку и выключается после проверки. Без него
period: "minute" вернёт 403. Всё остальное — привязка счёта,
вебхуки, отмена — работает точно так же, как в бою.
101300 Limits exceeded.
Правила и отмена
- Отмена обязана быть очевидной. Это требование банка-эквайера: если покупатель
не может легко отключить списания, он идёт оспаривать платёж в банк, и растёт доля диспутов.
Показывайте ссылку
cancel_urlтам, где покупатель её найдёт. - Условия показываем до оплаты. На нашей странице оплаты покупатель видит сумму, периодичность и порядок отключения ещё до подтверждения платежа.
- Мы предупреждаем письмом за сутки до каждого списания и присылаем квитанцию после. В каждом письме есть ссылка на отключение.
- Неудачные списания. Повторяем раз в сутки, по умолчанию до 3 раз. После этого
подписка закрывается со статусом
canceledи причинойpayment_failed. - Следующая дата отсчитывается от фактического успешного списания, а не от плановой даты.
- Отмена не возвращает деньги за уже оплаченный период. Возврат при необходимости делается отдельно.
Webhook-уведомления
При смене статуса на финальный (success, decline, expired, error) — мы шлём POST на ваш webhook_url (настраивается в кабинете кассы).
Структура события
POST https://your-site.com/your/webhook/path
Content-Type: application/json
X-Kapsula-Signature: 4a8b2d6e9f0a1b3c5d7e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c
{
"event": "payment.success",
"payment": {
"id": "pay_a1b2c3d4e5f6789012345678abcdef01",
"order_id": "order_12345",
"status": "success",
"amount": 50000,
"currency": "RUB",
"method": "sbp",
"paid_at": 1714478560000,
"error_code": null,
"error_message": null,
"created_at": 1714478400000
},
"timestamp": 1714478561234
}
События
payment.success— платёж прошёлpayment.decline— платёж отклонёнpayment.expired— истёк срок QRpayment.error— внутренняя ошибка обработки
Проверка подписи webhook
Алгоритм: HMAC-SHA256(webhook_secret, raw_body). Подпись приходит в hex в заголовке X-Kapsula-Signature.
Важно: подписывается сырое тело запроса как байты, до парсинга JSON. Иначе malformed JSON или whitespace-различия сломают проверку.
const express = require('express');
const crypto = require('crypto');
const app = express();
// ВАЖНО: для проверки подписи нужно raw body, не parsed JSON
app.post('/kapsula/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-kapsula-signature'];
const expected = crypto
.createHmac('sha256', process.env.KAPSULA_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
// timing-safe сравнение
if (!sig || sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Bad signature');
}
const event = JSON.parse(req.body);
handleKapsulaEvent(event);
res.json({ ok: true });
}
);
async function handleKapsulaEvent(event) {
const p = event.payment;
const order = await db.orders.findOne({ kapsula_payment_id: p.id });
if (!order || order.status === 'paid') return; // идемпотентность
if (event.event === 'payment.success' && p.amount === order.amount_kop) {
order.status = 'paid';
await order.save();
await sendOrderConfirmation(order);
}
}
<?php
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_KAPSULA_SIGNATURE'] ?? '';
$secret = getenv('KAPSULA_WEBHOOK_SECRET');
$expected = hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
echo 'Bad signature';
exit;
}
$event = json_decode($body, true);
$p = $event['payment'];
// Идемпотентность + проверка суммы
$order = $pdo->query("SELECT * FROM orders WHERE kapsula_payment_id = '{$p['id']}'")->fetch();
if (!$order || $order['status'] === 'paid') {
echo json_encode(['ok' => true]);
exit;
}
if ($event['event'] === 'payment.success' && $p['amount'] == $order['amount_kop']) {
$pdo->exec("UPDATE orders SET status = 'paid' WHERE id = {$order['id']}");
sendOrderConfirmation($order);
}
echo json_encode(['ok' => true]);
import hmac, hashlib, json, os
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = os.environ['KAPSULA_WEBHOOK_SECRET'].encode()
@app.post('/kapsula/webhook')
def kapsula_webhook():
raw = request.get_data() # сырое тело — НЕ request.json
sig = request.headers.get('X-Kapsula-Signature', '')
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return 'Bad signature', 401
event = json.loads(raw)
p = event['payment']
order = Order.query.filter_by(kapsula_payment_id=p['id']).first()
if not order or order.status == 'paid':
return jsonify(ok=True) # идемпотентность
if event['event'] == 'payment.success' and p['amount'] == order.amount_kop:
order.status = 'paid'
db.session.commit()
send_order_confirmation(order)
return jsonify(ok=True)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Kapsula-Signature")
secret := []byte(os.Getenv("KAPSULA_WEBHOOK_SECRET"))
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "Bad signature", 401)
return
}
var event struct {
Event string `json:"event"`
Payment struct {
ID string `json:"id"`
OrderID string `json:"order_id"`
Amount int `json:"amount"`
Status string `json:"status"`
} `json:"payment"`
}
json.Unmarshal(body, &event)
// обработка...
w.Write([]byte(`{"ok":true}`))
}
Идемпотентность
Webhook может прийти несколько раз для одного и того же события (при retry). Чтобы не выдать товар дважды:
- Сохраняйте у себя
payment.id - В обработчике сначала проверяйте: «уже обработали этот
payment.idс этимstatus?» — если да, верните 200 без действий - Используйте транзакции БД с
SELECT ... FOR UPDATEили unique-индекс
Повторы и сбои
- До 3 попыток с задержкой: сразу, через 30 секунд, через 5 минут
- Считается успехом — HTTP-код
2xx - Таймаут запроса — 10 секунд
- Если все попытки неуспешны — статус доступен через
GET /payment/:id
Сценарий: интеграция на сайт
Типовой flow для интернет-магазина:
- Пользователь нажимает «Оплатить» — ваш сервер создаёт платёж через
POST /payment/createи редиректит покупателя наpayment_url - Покупатель оплачивает на странице KAPSULA
- Мы шлём webhook на ваш
/webhook/kapsula - Ваш обработчик webhook'а помечает заказ как оплаченный, отправляет товар/чек
- Мы редиректим покупателя на ваш
success_url(там вы показываете «Спасибо!»)
success_url — это ненадёжный сигнал. Покупатель мог открыть URL вручную не оплатив. Источник истины — webhook.Сценарий: Telegram-бот
- Пользователь пишет в боте «/buy» — бот вызывает
POST /payment/createсmode: "host" - Бот отправляет покупателю QR-картинку (из
qr.data) и кнопку «Открыть в банке» (сqr.link) - Бот сохраняет
payment.idс привязкой кchat_id - На webhook от KAPSULA — бот уведомляет пользователя «Оплата прошла» и выдаёт услугу
Пример отправки QR в Telegram (Python + aiogram)
import base64, io
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InlineKeyboardButton
@dp.message(Command('buy'))
async def cmd_buy(message):
payment = create_kapsula_payment(50000, f"tg_{message.from_user.id}", "Premium")
qr_b64 = payment['qr']['data'].split(',')[1]
qr_bytes = base64.b64decode(qr_b64)
kb = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text='Открыть в банке', url=payment['qr']['link'])
]])
await message.answer_photo(
BufferedInputFile(qr_bytes, 'qr.png'),
caption=f"Оплатите {500} ₽ через СБП. QR действует 30 минут.",
reply_markup=kb,
)
Готовые модули для CMS
WordPress + WooCommerce
Готовый плагин с поддержкой WooCommerce. Покупатель видит способ оплаты «Оплата через СБП» при оформлении заказа, после оплаты заказ автоматически помечается как «Оплачен» через webhook.
Установка через админку WordPress (рекомендуется)
- Скачайте архив по кнопке выше — получите файл
kapsula-payments.zip. - Откройте админку WordPress →
Плагины → Добавить новый → Загрузить плагин. - Выберите файл
kapsula-payments.zipи нажмите «Установить», затем «Активировать». - В админке появится пункт
Настройки → KAPSULA— откройте его. - Вставьте API-ключ и Webhook secret из карточки кассы (kapsula.pro/shops → ваша касса).
- Скопируйте Webhook URL со страницы настроек плагина.
- Откройте кабинет KAPSULA → Настройки кассы → вставьте этот Webhook URL и сохраните.
- Если используете WooCommerce:
WooCommerce → Настройки → Платежи → KAPSULA→ переключатель Включить. - Готово. На странице оформления заказа появится способ оплаты «Оплата через СБП».
Установка вручную (через FTP)
- Распакуйте zip — получите папку
kapsula-payments. - Загрузите эту папку в
/wp-content/plugins/на вашем сервере. - В админке WordPress →
Плагины— найдите «KAPSULA — Платёжный шлюз» → Активировать. - Дальше как в п. 4-9 выше.
Что делает плагин
- WooCommerce-шлюз — добавляется как стандартный способ оплаты на checkout
- Создание платежа — при оформлении заказа дёргает наш API, получает
payment_urlи редиректит покупателя наpay.kapsula.pro/p/... - Webhook handler — принимает уведомления на
https://ваш-сайт.com/?kapsula_webhook=1, проверяет HMAC-подпись, сверяет сумму, помечает заказ как «Оплачен» - Идемпотентность — если webhook прилетит дважды, заказ не будет помечен оплаченным повторно
- Хук
kapsula_payment_event— для интеграций без WooCommerce. В вашем кодеadd_action('kapsula_payment_event', function($event) { ... })
1C-Битрикс / другие CMS
Готовых модулей для других CMS пока нет. Подключение делается через стандартный API:
- На странице оформления заказа — вызываете
POST /v1/payment/createи делаете редирект наpayment_url - На своём webhook-эндпоинте принимаете POST с заголовком
X-Kapsula-Signature, проверяете подпись, помечаете заказ оплаченным - Примеры кода на PHP, Node.js, Python, Go — в разделе «Создание платежа»
Если нужна помощь с интеграцией — напишите на support@kapsula.pro.
Кабинет: Главная
На главной кабинета отображается:
- Список ваших касс с быстрым доступом
- Сводная статистика по всем кассам
- Последние транзакции
Быстрая оплата
Раздел /quick-pay в кабинете позволяет создавать платёжные ссылки вручную, без вызова API. Удобно для:
- Разовых выставлений счёта клиенту в мессенджере или по почте
- Тестирования интеграции, прежде чем подключать API
- Использования сервиса без своего сайта (например, фрилансерам)
Как это работает:
- Откройте
Быстрая оплатав боковом меню - Выберите кассу (доступны только кассы со статусом
active) - Введите сумму (от 50 до 15 000 ₽), при желании — описание и email клиента
- Нажмите
Создать ссылку— получите URL видаhttps://pay.kapsula.pro/p/pay_... - Скопируйте ссылку и отправьте клиенту любым удобным способом
shop_id. Если у вас несколько касс, оплата по ссылке поступит именно на ту кассу, которая была выбрана при создании.expired, и оплатить будет нельзя — просто создайте новую.Внутренне эндпоинт работает идентично POST /v1/payment/create в hosted-режиме, но авторизация идёт через cookie сессии кабинета, а не через Bearer pk_live_....
Аналитика
На странице конкретной кассы (/shops/:id/manage) есть блок аналитики:
- Всего платежей — за всё время
- Успешных — количество и процент конверсии
- Принято — сумма успешных платежей
- За 24 часа — последние сутки
Транзакции
Список последних 25 платежей по кассе с фильтрацией по статусу. Каждая транзакция показывает:
- ID платежа в KAPSULA и ваш
order_id - Описание и email покупателя
- Сумму, статус, дату создания
Через API: GET /api/shops/:id/payments?status=success&limit=50
Выплаты
Раздел в разработке. Сейчас принятые средства учитываются за вашей кассой; механизм автоматического вывода на расчётный счёт мерчанта — будет добавлен в ближайших обновлениях. Текущий процесс — связь с поддержкой для ручного вывода.
Настройки профиля
Раздел /profile позволяет:
- Сменить email — на новый адрес придёт 4-значный код подтверждения. Email обновляется только после ввода кода. Старый email становится недоступен для входа сразу после смены.
- Сменить пароль — нужно ввести текущий пароль + новый. Код подтверждения приходит на текущий email. Минимум 8 символов, обязательно буквы и цифры.
Если забыли пароль — на странице входа есть ссылка Забыли пароль?. Восстановление в 3 шага: ввод email → код из письма → новый пароль.
Ошибки
Все ошибки возвращаются в формате:
{ "error": "Текст ошибки на русском" }
| Код | Причина |
|---|---|
400 | Невалидные параметры (сумма вне диапазона, неверный email и т.д.) |
401 | API-ключ не передан или неверный |
403 | Касса не активна (на модерации, заблокирована) |
404 | Платёж не найден или принадлежит другой кассе |
429 | Превышен лимит запросов (rate limit) |
502 | Платёжный шлюз недоступен или вернул ошибку |
Лимиты
- Сумма платежа: 50 ₽ – 15 000 ₽ (СБП)
- Время жизни QR: 30 минут
- Метод: только СБП (карты — позже)
- Валюта: только RUB
- Rate limit API: 300 запросов / 15 минут с одного IP
- Webhook timeout: 10 секунд на ответ от вашего сервера
FAQ
webhook_url задан в настройках кассы, (2) URL HTTPS и доступен снаружи (не localhost), (3) ваш сервер отвечает 2xx за 10 секунд, (4) не блокирует ваш фаервол. В крайнем случае — статус всегда можно получить через GET /payment/:id.payment_url / QR.success. Если банк не успел подтвердить — придёт expired и потребуется создать новый платёж.GET /payment/:id (например, для всех заказов в статусе awaiting_confirmation старше N минут).Поддержка
В обращении укажите:
- ID кассы (виден в кабинете)
- ID платежа (если вопрос по конкретному платежу)
- Запрос/ответ от API (без API-ключа в открытом виде!)