Документация · KAPSULA
Подключить

Документация KAPSULA

Платёжный шлюз для приёма платежей через СБП на сайтах и в ботах. Полное руководство — от регистрации до интеграции и проверки подписи webhook'ов.

О платформе

KAPSULA — платёжный посредник между вами (мерчантом) и платёжной системой. Мы упрощаем интеграцию: вместо того чтобы проходить верификацию у банка-эквайера месяцами, вы регистрируетесь у нас за 5 минут, проходите модерацию и принимаете оплаты.

Сейчас поддерживается:

В планах: банковские карты (когда будут пройдены требования PCI DSS).

💡 Базовый URL API: https://api.kapsula.pro/v1

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

Минимальная интеграция — 5 шагов и около 10 минут.

  1. Зарегистрируйтесьkapsula.pro/auth
  2. Создайте кассу в кабинете → Кассы. Укажите сайт или бота
  3. Пройдите верификацию (для сайтов — загрузка HTML-файла) и дождитесь модерации
  4. Получите API-ключ вида pk_live_... на странице кассы
  5. Сделайте первый запрос:
    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 сейчас не требуется — после регистрации вы сразу попадаете в кабинет.

Создание кассы

«Касса» — это отдельная точка приёма платежей: один сайт или один бот. У вас может быть сколько угодно касс.

Перейдите в Кассы → Создать кассу. Заполните 2 шага:

Шаг 1. Тип кассы

Шаг 2. Реквизиты

Верификация сайта

Чтобы подтвердить что вы владеете сайтом — нужно загрузить наш файл в его корень.

  1. В кабинете кассы нажмите «Скачать файл верификации» — скачается kapsula-verify-<токен>.html
  2. Загрузите файл в корень вашего сайта так чтобы он открывался по адресу: https://ваш-сайт.com/kapsula-verify-<токен>.html
  3. В кабинете нажмите «Проверить» — мы скачаем файл и убедимся что токен совпадает
  4. После успешной проверки касса автоматически уйдёт на модерацию
⚠ Для ботов верификация не требуется — статус сразу pending_moderation.

Модерация

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

API-ключи

После одобрения кассы в её карточке появятся два ключа:

КлючПрефиксНазначение
API-ключpk_live_Авторизация запросов от вашего сервера к нашему API
Webhook secretwhsec_Проверка подписи входящих webhook-уведомлений

Оба ключа можно пересоздать в любой момент — старые сразу перестают работать.

Безопасность

Платёжная система обрабатывает деньги — безопасность критична. Минимальные правила:

1. Хранение ключей

2. 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.

Жизненный цикл платежа

[Ваш сервер] [KAPSULA] [6Tech / СБП] │ │ │ │ POST /payment/create │ │ ├─────────────────────────────────>│ │ │ │ POST /v1/payment/pay │ │ ├─────────────────────────────>│ │ │<──── request_uuid ───────────┤ │ │<──── callback (QR данные) ───┤ │<─── 200 + payment_url / qr ──────┤ │ │ │ │ │ [Покупатель сканирует QR] │ │ │ │ │ │<──── callback (Success) ─────┤ │<─── webhook payment.success ─────┤ │ │ │ │ ▼ ▼ ▼

Режим Hosted (рекомендуется)

Самый простой способ. Покупатель видит страницу оплаты на нашем домене (pay.kapsula.pro/p/...) — мы обеспечиваем UI, QR, обработку статусов.

  1. Создаёте платёж с mode: "hosted"
  2. В ответе получаете payment_url
  3. Перенаправляете покупателя туда (302 Redirect или открываете в новом окне)
  4. После оплаты мы:
    • Шлём webhook на ваш webhook_url
    • Перенаправляем покупателя на ваш success_url (или fail_url при отказе)
✅ Hosted-режим не требует реализации UI оплаты на вашей стороне — идеально для быстрого старта.

Режим Host (свой UI)

Если нужно полностью кастомное оформление — используйте режим host. Мы вернём вам данные QR, дальше вы рисуете и обрабатываете всё сами.

  1. Создаёте платёж с mode: "host"
  2. В ответе получаете qr.data (PNG в base64) и qr.link (deeplink на qr.nspk.ru)
  3. Рисуете QR на своей странице:
    <img src="data:image/png;base64,..." alt="СБП QR">
  4. Опционально — кнопка «Открыть в банке» с qr.link (для оплаты с того же телефона)
  5. Опрашиваете GET /payment/:id/status каждые 2–3 секунды или просто ждёте webhook

Создание платежа

POST /api/v1/payment/create

Параметры запроса

ПараметрТипОбяз.Описание
amountintegerдаСумма в копейках. От 5000 (50 ₽) до 1500000 (15 000 ₽)
currencystringнетТолько RUB (по умолчанию)
methodstringнетТолько sbp (по умолчанию)
modestringнетhosted (по умолчанию) или host
order_idstringнетВаш ID заказа (до 128 символов). Вернётся в webhook'е
descriptionstringнетОписание (до 500 символов). Видит покупатель
customer_emailstringнетEmail покупателя (для будущих чеков)
success_urlstringнетOverride для hosted-режима
fail_urlstringнет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 секунды.

Получение информации о платеже

GET /api/v1/payment/:id

Полная информация о платеже.

curl https://api.kapsula.pro/v1/payment/pay_a1b2c3d4e5f6... \
  -H "Authorization: Bearer $KAPSULA_API_KEY"
GET /api/v1/payment/:id/status

Лёгкий запрос для polling — только статус и время оплаты:

{ "status": "success", "paid_at": 1714478560000 }

Статусы платежа

СтатусФинальный?Описание
pendingНетСоздан, ждём ответа от шлюза
awaiting_confirmationНетQR выдан, ждём оплату
successДаПлатёж успешно прошёл
declineДаОтклонён банком
expiredДаQR истёк (30 минут не было оплаты)
errorДаВнутренняя ошибка

Подписки: как это работает

Подписка — это регулярные списания через СБП. Покупатель один раз оплачивает и подтверждает в приложении банка согласие на автосписания, после чего деньги списываются автоматически по заданному расписанию. Карты для подписок пока недоступны, работает только СБП.

  1. Вы создаёте подписку запросом POST /v1/subscription/create и получаете payment_url.
  2. Покупатель открывает ссылку, видит условия (сумма, периодичность, как отключить) и оплачивает первый платёж по QR.
  3. В момент оплаты банк спрашивает согласие на регулярные списания — счёт привязывается.
  4. Вам приходит webhook subscription.activated — открывайте доступ к продукту.
  5. Дальше в каждую дату списания мы сами списываем деньги и шлём subscription.charged.
  6. Если покупатель отключит подписку — придёт subscription.canceled, закрывайте доступ.

Подключение подписок

Подписки не включены по умолчанию — их нужно активировать для конкретной кассы.

  1. Напишите в поддержку, что вам нужны подписки. Мы проверим ваш сценарий и откроем их для кассы. Отдельная проверка связана с требованиями банка-эквайера к регулярным списаниям.
  2. После активации в кабинете на странице кассы (Кассы → управление кассой → Подписки) появится переключатель. Он включён сразу, вы можете выключить его в любой момент.
  3. Дальше используйте тот же ключ pk_live_..., что и для обычных платежей — отдельного ключа для подписок нет.
Что делает переключатель. Выключенный тумблер блокирует только создание новых подписок. Уже действующие продолжают списываться, чтобы клиенты, оплатившие период, не потеряли доступ. Чтобы остановить конкретную подписку, отмените её через /cancel или в разделе «Подписки» кабинета.

Если подписки не подключены, POST /v1/subscription/create вернёт 403:

{ "error": "Подписки для этой кассы не подключены. Напишите в поддержку, чтобы их активировали." }

Чтение и отмена подписок работают всегда, даже при выключенном тумблере — вы сможете корректно погасить действующие.

Отдельно возможна временная пауза со стороны платформы — например, при работах на стороне банка. Тогда create вернёт 503:

{ "error": "Подписки временно недоступны на стороне платформы. Попробуйте позже или свяжитесь с поддержкой." }

Это временное состояние, ваши настройки при этом сохраняются. Обрабатывайте 503 как «повторить позже», а не как отказ.

POST /v1/subscription/create

Создаёт подписку и ссылку на первую оплату.

ПолеТипОбязательноОписание
amountintegerдаСумма одного списания в копейках. 5000–1500000 (50–15000 ₽). Строка не принимается. Ниже 50 ₽ нельзя — это ограничение банка, а не наше: такие платежи отклоняются с ошибкой 101300 Limits exceeded
periodstringдаday, week, month, quarter, year. Плюс minute и hour — только в тестовом режиме, см. Тестовый режим
customer_emailstringдаEmail покупателя. На него уходят квитанции и ссылка отмены
intervalintegerнет1–100. Например period=week, interval=2 — раз в две недели. По умолчанию 1
descriptionstringнетНазвание подписки, видно покупателю. До 500 символов
order_idstringнетВаш внутренний идентификатор, до 128 символов
max_retriesintegerнетСколько раз повторять неудачное списание, 0–10. По умолчанию 3
max_chargesintegerнетОстановить подписку после N списаний, 1–1000. По умолчанию без ограничения, а для тестовых периодов — 10
success_url / fail_urlstringнетКуда вернуть покупателя после первой оплаты
modestringнет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/createcancel_url
Ответ GET /v1/subscription/:idcancel_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_pendingtrue — QR не успел прийти за 8 секунд. Не ошибка, опросите GET /v1/subscription/:id
payment_urlВ режиме host не возвращается
QR привязки отличается от обычного. Он ведёт на 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": "Проверка автосписаний"
  }'
Подписка сама остановится после 10 списаний. Это защита от забытого теста: при 50 ₽ за списание раз в минуту счёт теряет 3000 ₽ в час. Своё значение можно задать в max_charges. По завершении подписка получает статус canceled с причиной completed.

Что ещё отличается в тестовом режиме:

Режим включается по запросу в поддержку и выключается после проверки. Без него period: "minute" вернёт 403. Всё остальное — привязка счёта, вебхуки, отмена — работает точно так же, как в бою.

Минимум 50 ₽ действует и в тестовом режиме. Порог стоит на стороне банка, обойти его мы не можем. Меньшие суммы отклоняются с ошибкой 101300 Limits exceeded.

Правила и отмена

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
}

События

Проверка подписи webhook

Обязательно проверяйте подпись. Без проверки злоумышленник может прислать поддельный 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). Чтобы не выдать товар дважды:

Повторы и сбои

💡 Делайте обработчик webhook'а быстрым — отвечайте 200 сразу, тяжёлую работу делайте в фоне (очередь, async). Иначе таймаут.

Сценарий: интеграция на сайт

Типовой flow для интернет-магазина:

  1. Пользователь нажимает «Оплатить» — ваш сервер создаёт платёж через POST /payment/create и редиректит покупателя на payment_url
  2. Покупатель оплачивает на странице KAPSULA
  3. Мы шлём webhook на ваш /webhook/kapsula
  4. Ваш обработчик webhook'а помечает заказ как оплаченный, отправляет товар/чек
  5. Мы редиректим покупателя на ваш success_url (там вы показываете «Спасибо!»)
⚠ Не выдавайте товар на странице success_url — это ненадёжный сигнал. Покупатель мог открыть URL вручную не оплатив. Источник истины — webhook.

Сценарий: Telegram-бот

  1. Пользователь пишет в боте «/buy» — бот вызывает POST /payment/create с mode: "host"
  2. Бот отправляет покупателю QR-картинку (из qr.data) и кнопку «Открыть в банке» (с qr.link)
  3. Бот сохраняет payment.id с привязкой к chat_id
  4. На 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.

📦
kapsula-payments.zip
Плагин для WordPress 5.6+ с интеграцией WooCommerce. Версия 1.0.0
⬇ Скачать плагин

Установка через админку WordPress (рекомендуется)

  1. Скачайте архив по кнопке выше — получите файл kapsula-payments.zip.
  2. Откройте админку WordPressПлагины → Добавить новый → Загрузить плагин.
  3. Выберите файл kapsula-payments.zip и нажмите «Установить», затем «Активировать».
  4. В админке появится пункт Настройки → KAPSULA — откройте его.
  5. Вставьте API-ключ и Webhook secret из карточки кассы (kapsula.pro/shops → ваша касса).
  6. Скопируйте Webhook URL со страницы настроек плагина.
  7. Откройте кабинет KAPSULA → Настройки кассы → вставьте этот Webhook URL и сохраните.
  8. Если используете WooCommerce: WooCommerce → Настройки → Платежи → KAPSULA → переключатель Включить.
  9. Готово. На странице оформления заказа появится способ оплаты «Оплата через СБП».

Установка вручную (через FTP)

  1. Распакуйте zip — получите папку kapsula-payments.
  2. Загрузите эту папку в /wp-content/plugins/ на вашем сервере.
  3. В админке WordPress → Плагины — найдите «KAPSULA — Платёжный шлюз»Активировать.
  4. Дальше как в п. 4-9 выше.

Что делает плагин

💡 Минимальная сумма заказа для СБП — 50 ₽, максимальная — 15 000 ₽. Если в корзине меньше 50 ₽ — способ оплаты KAPSULAй будет недоступен.

1C-Битрикс / другие CMS

Готовых модулей для других CMS пока нет. Подключение делается через стандартный API:

Если нужна помощь с интеграцией — напишите на support@kapsula.pro.

Кабинет: Главная

На главной кабинета отображается:

Быстрая оплата

Раздел /quick-pay в кабинете позволяет создавать платёжные ссылки вручную, без вызова API. Удобно для:

Как это работает:

  1. Откройте Быстрая оплата в боковом меню
  2. Выберите кассу (доступны только кассы со статусом active)
  3. Введите сумму (от 50 до 15 000 ₽), при желании — описание и email клиента
  4. Нажмите Создать ссылку — получите URL вида https://pay.kapsula.pro/p/pay_...
  5. Скопируйте ссылку и отправьте клиенту любым удобным способом
Изоляция платежей: платёж жёстко привязан к выбранной кассе через shop_id. Если у вас несколько касс, оплата по ссылке поступит именно на ту кассу, которая была выбрана при создании.
Срок действия: ссылка живёт 30 минут (ограничение СБП на стороне 6Tech). По истечении статус платежа меняется на expired, и оплатить будет нельзя — просто создайте новую.

Внутренне эндпоинт работает идентично POST /v1/payment/create в hosted-режиме, но авторизация идёт через cookie сессии кабинета, а не через Bearer pk_live_....

Аналитика

На странице конкретной кассы (/shops/:id/manage) есть блок аналитики:

Транзакции

Список последних 25 платежей по кассе с фильтрацией по статусу. Каждая транзакция показывает:

Через API: GET /api/shops/:id/payments?status=success&limit=50

Выплаты

Раздел в разработке. Сейчас принятые средства учитываются за вашей кассой; механизм автоматического вывода на расчётный счёт мерчанта — будет добавлен в ближайших обновлениях. Текущий процесс — связь с поддержкой для ручного вывода.

Настройки профиля

Раздел /profile позволяет:

Если забыли пароль — на странице входа есть ссылка Забыли пароль?. Восстановление в 3 шага: ввод email → код из письма → новый пароль.

Безопасность: код действителен 10 минут, максимум 5 неверных попыток, минимум 60 секунд между запросами кода. После сброса пароля на текущую сессию выдаётся новый JWT.

Ошибки

Все ошибки возвращаются в формате:

{ "error": "Текст ошибки на русском" }
КодПричина
400Невалидные параметры (сумма вне диапазона, неверный email и т.д.)
401API-ключ не передан или неверный
403Касса не активна (на модерации, заблокирована)
404Платёж не найден или принадлежит другой кассе
429Превышен лимит запросов (rate limit)
502Платёжный шлюз недоступен или вернул ошибку

Лимиты

FAQ

Можно ли тестировать без реальных платежей?
Сейчас нет — sandbox-окружение в разработке. Тестировать можно с реальной картой на минимальной сумме (50 ₽), потом запросить возврат через поддержку.
Какая комиссия?
Текущая ставка комиссии видна в кабинете для каждой кассы. По умолчанию — 2% от суммы платежа. Уточняйте у поддержки если ваш профиль предполагает льготные условия.
Что делать если webhook не пришёл?
Проверьте: (1) webhook_url задан в настройках кассы, (2) URL HTTPS и доступен снаружи (не localhost), (3) ваш сервер отвечает 2xx за 10 секунд, (4) не блокирует ваш фаервол. В крайнем случае — статус всегда можно получить через GET /payment/:id.
Можно ли изменить сумму платежа после создания?
Нет. Создайте новый платёж и используйте новый payment_url / QR.
Что если QR истёк а покупатель его уже отсканировал?
Если оплата была завершена в банке до истечения — мы получим callback и установим статус success. Если банк не успел подтвердить — придёт expired и потребуется создать новый платёж.
Поддерживаются ли возвраты (refund)?
Сейчас возврат делается через поддержку вручную. API для refund будет добавлен в ближайших версиях.
Можно ли использовать один API-ключ для нескольких сайтов?
Технически да, но мы рекомендуем создавать отдельную кассу для каждого сайта/проекта — это даёт раздельную аналитику, упрощает модерацию и компрометация одного ключа не затрагивает остальные.
Что если мой сайт DDoS'ят и мы не успеваем отвечать на webhook?
Мы повторим webhook 3 раза (через 0с / 30с / 5мин). Если все попытки неуспешны — поднимайте сервис и подтягивайте статусы через GET /payment/:id (например, для всех заказов в статусе awaiting_confirmation старше N минут).

Поддержка

📧 support@kapsula.pro

В обращении укажите:

Удачи в подключении! Если что-то непонятно — пишите, доработаем документацию.