PaySame + Telegram-бот: приём рублевых подписок без Stripe и банковских отказов
В 2022 году Stripe закрыл российский рынок. Вместе с ним ушли PayPal, большинство западных агрегаторов. Для тех, кто строил подписочные продукты на этой инфраструктуре, это означало срочный поиск замены. Я прошёл через это в начале 2023 года, когда запускал клуб «Solar — внутрянка».
За два месяца я протестировал ЮKassa, CloudPayments, Robokassa и PaySame. В итоге клуб работает на PaySame с января 2024 года. Бот @solar_inside_bot принимает подписки 2 500 ₽/мес и 4 999 ₽/3 мес без ручного вмешательства. В этой статье — архитектура, код и реальные числа.
Что такое PaySame и зачем он нужен в 2026 году
PaySame — российский платёжный агрегатор, запущен в 2019 году. Принимает: карты Мир, Visa и Mastercard (если банк-эмитент не заблокировал интернет-платежи), СБП (Система быстрых платежей — QR-код или перевод по номеру телефона), банковские переводы по реквизитам.
Для Telegram-ботов критичны три характеристики агрегатора:
- Webhook без ограничений. PaySame отправляет POST-запрос на ваш URL при каждом изменении статуса платежа. Нет лимита на количество событий, нет whitelist по IP — в отличие от некоторых конкурентов.
- Тестовый режим без ожидания верификации. Сразу после регистрации доступен sandbox с тестовыми картами (4111 1111 1111 1111, любая дата/CVV). Всю схему можно отладить до получения боевых ключей.
- SDK для Python с примерами. Документация не идеальная, но достаточная — не нужно разбирать формат запросов вручную по форуму.
Комиссия PaySame — 3.5% с транзакции. Для сравнения: ЮKassa — 2.8%, CloudPayments — от 2.5%, Тинькофф Kassa — 2.8–3.5%. При обороте 200k ₽/мес разница между 2.8% и 3.5% = 1 400 ₽. Это не те деньги, из-за которых стоит усложнять интеграцию на старте.
Архитектура: как Telegram-бот и PaySame работают вместе
Полная схема клуба «Solar — внутрянка»:
- Пользователь пишет боту
/subscribe - Бот создаёт запись в PostgreSQL:
INSERT INTO subscribers (telegram_id, status) VALUES ($1, pending) - Бот вызывает PaySame API:
POST /v1/orders/createс полями amount, order_id, webhook_url - Пользователь переходит по ссылке, оплачивает на странице PaySame
- PaySame отправляет
POSTнаhttps://4bos.ru/paysame/webhook - Обработчик меняет статус:
status=active,expires_at=NOW()+30 дней - Бот отправляет персональную invite link в закрытый канал клуба (действует 1 час)
Полный цикл от нажатия «оплатить» до получения доступа — 15–30 секунд. Без участия администратора.
Структура базы данных
Минимальная PostgreSQL-схема для подписочного бота:
CREATE TABLE subscribers (
id SERIAL PRIMARY KEY,
telegram_id BIGINT UNIQUE NOT NULL,
telegram_username VARCHAR(255),
status VARCHAR(20) DEFAULT pending,
subscribed_at TIMESTAMP,
expires_at TIMESTAMP,
plan VARCHAR(20) DEFAULT 1month,
paid_amount INTEGER,
paysame_order_id VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_subscribers_expires_at ON subscribers(expires_at);
CREATE INDEX idx_subscribers_status ON subscribers(status);
order_id в PaySame формируется как tg_{telegram_id}_{timestamp}. Timestamp нужен, чтобы один пользователь мог создать несколько ссылок (если первая истекла по таймауту). При получении webhook — парсим telegram_id из order_id через split.
Код интеграции: Python + aiogram 3 + PaySame API
Стек, который работает в проде: Python 3.11, aiogram 3.7, aiohttp для HTTP-запросов к PaySame, asyncpg для PostgreSQL.
Создание платёжной ссылки:
import aiohttp, hashlib, time
from typing import Optional
PAYSAME_API_KEY = "ваш_api_key"
PAYSAME_SECRET = "ваш_secret"
PAYSAME_BASE_URL = "https://api.paysame.ru/v1"
PLANS = {
"1month": {"amount": 250000, "label": "1 месяц — 2 500 руб."},
"3month": {"amount": 499900, "label": "3 месяца — 4 999 руб."},
}
async def create_payment_link(telegram_id: int, plan: str = "1month") -> Optional[str]:
plan_data = PLANS[plan]
order_id = f"tg_{telegram_id}_{int(time.time())}"
sign_string = f"{plan_data[amount]}|{order_id}|{PAYSAME_SECRET}"
sign = hashlib.md5(sign_string.encode()).hexdigest()
payload = {
"amount": plan_data["amount"],
"currency": "RUB",
"order_id": order_id,
"description": f"Клуб Solar — внутрянка, {plan_data[label]}",
"success_url": "https://t.me/solar_inside_bot?start=paid",
"fail_url": "https://t.me/solar_inside_bot?start=failed",
"webhook_url": "https://4bos.ru/paysame/webhook",
"sign": sign,
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{PAYSAME_BASE_URL}/orders/create",
json=payload,
headers={"Authorization": f"Bearer {PAYSAME_API_KEY}"},
timeout=aiohttp.ClientTimeout(total=10)
) as resp:
if resp.status == 200:
data = await resp.json()
return data.get("payment_url")
return None
Webhook-обработчик (aiohttp):
from aiohttp import web
async def paysame_webhook(request: web.Request):
body = await request.json()
received_sign = body.get("sign", "")
amount = body.get("amount", 0)
order_id = body.get("order_id", "")
expected_sign = hashlib.md5(
f"{amount}|{order_id}|{PAYSAME_SECRET}".encode()
).hexdigest()
if received_sign != expected_sign:
return web.Response(status=403, text="Invalid signature")
if body.get("status") == "paid":
parts = order_id.split("_")
telegram_id = int(parts[1])
interval = 90 if amount == 499900 else 30
plan = "3month" if interval == 90 else "1month"
await db.execute(
"UPDATE subscribers SET status=active, plan=$1, "
"subscribed_at=NOW(), expires_at=NOW() + ($2 || days)::INTERVAL, "
"paid_amount=$3, paysame_order_id=$4, updated_at=NOW() "
"WHERE telegram_id=$5",
plan, str(interval), amount, order_id, telegram_id
)
invite_link = await bot.create_chat_invite_link(
chat_id=CLUB_CHANNEL_ID,
member_limit=1,
expire_date=int(time.time()) + 3600
)
await bot.send_message(
telegram_id,
f"Оплата прошла. Ваша ссылка:\n{invite_link.invite_link}"
)
return web.Response(text="ok")
Нюанс: PaySame периодически обновляет формат подписи в новых версиях SDK. При обновлении — сначала проверяйте тестовую подпись в sandbox, не сразу в боевом режиме.
Поллер: защита от потерянных webhook
Webhook — асинхронный механизм. Если сервер лежал 5 минут во время деплоя, а PaySame не смог доставить уведомление — клиент оплатил, но доступ не получил. PaySame повторяет доставку через 15 минут, через час и через 6 часов. Если сервер не отвечал все три раза — платёж теряется.
Решение — независимый фоновый поллер:
import asyncio
from datetime import datetime, timedelta
async def payment_poller():
while True:
await asyncio.sleep(900) # 15 минут
try:
two_hours_ago = (datetime.utcnow() - timedelta(hours=2)).isoformat()
async with aiohttp.ClientSession() as session:
async with session.get(
f"{PAYSAME_BASE_URL}/orders/list",
params={"from": two_hours_ago, "status": "paid"},
headers={"Authorization": f"Bearer {PAYSAME_API_KEY}"},
timeout=aiohttp.ClientTimeout(total=15)
) as resp:
orders = (await resp.json()).get("orders", [])
for order in orders:
order_id = order["order_id"]
if not order_id.startswith("tg_"):
continue
telegram_id = int(order_id.split("_")[1])
subscriber = await db.fetchrow(
"SELECT status FROM subscribers WHERE telegram_id=$1",
telegram_id
)
if subscriber and subscriber["status"] != "active":
await activate_subscriber(telegram_id, order)
logger.warning(f"Поллер восстановил подписку: {telegram_id}")
except Exception as e:
logger.error(f"Поллер ошибка: {e}")
За период с января по июнь 2026 года поллер восстановил доступ для 4 подписчиков, у которых платёж прошёл, но webhook не был доставлен. Без поллера — 4 клиента с оплаченной, но недоступной подпиской и потенциальными претензиями к сервису.
Управление продлениями без автосписания
PaySame не поддерживает рекуррентные платежи. Каждый клиент оплачивает вручную каждый период — точка потенциального оттока. Схема напоминаний клуба «Solar — внутрянка»:
- За 5 дней до истечения — сообщение с анонсом материалов следующей недели: «Подписка истекает 21 июня. На следующей неделе в клубе — кейс автоматизации лид-воронки и шаблон AGENTS.md для команды из 10 агентов. Продлить: [ссылка].»
- За 1 день — второе напоминание с персональной ссылкой. Без давления, просто факт.
- В день истечения в 23:00 — автоматическое исключение из канала через
kick_chat_member, сразу сообщение с ссылкой для возобновления. - Через 3 дня после истечения — финальный оффер: «Вы пропустили последнюю неделю клуба. Возобновить: [ссылка].»
По данным за Q1 2026: из тех, кто получил напоминание за 5 дней, продлили 82%. Из тех, кто напоминание не получил (Telegram-уведомления отключены) — 61%. Разница в 21% — это отток, который можно устранить техническими средствами.
Мониторинг: что нужно проверять каждую неделю
После запуска базовой интеграции три вещи ломаются чаще всего и незаметно:
1. Здоровье webhook-эндпоинта. Каждые 10 минут cron проверяет, что https://4bos.ru/paysame/webhook отвечает статусом 200. Если нет — алерт в Telegram-чат команды. Без этого мониторинга можно пропустить деплой, который уронил сервер.
2. Расхождение PaySame vs PostgreSQL. Ежедневный скрипт сравнивает сумму транзакций «paid» в PaySame за день с суммой активированных подписок в БД. Расхождение больше нуля означает потерянные платежи.
3. Подписки без напоминания:
SELECT telegram_id, expires_at
FROM subscribers
WHERE status = expired
AND expires_at > NOW() - INTERVAL 7 days
AND telegram_id NOT IN (
SELECT telegram_id FROM reminder_log
WHERE sent_at > NOW() - INTERVAL 14 days
);
Если этот запрос возвращает строки — кто-то потерял подписку без предупреждения. Запускайте его раз в неделю как часть аудита.
Регистрация в PaySame: пошаговый процесс
Формально регистрация выглядит просто — сайт, форма, документы. На практике есть несколько нюансов, которые замедляют процесс, если не знать о них заранее.
Шаг 1. Регистрация на сайте. Заходите на paysame.ru, раздел «Для бизнеса». Регистрация через email, без привязки телефона. Сразу после регистрации получаете доступ к личному кабинету и тестовому режиму. Тестовые ключи уже в кабинете — можно начинать интеграцию.
Шаг 2. Подача документов на верификацию. В личном кабинете раздел «Верификация». Нужны: скан паспорта ИП (все страницы с данными, регистрация), выписка из ЕГРИП/ЕГРЮЛ не старше 30 дней (заказывается бесплатно через сайт ФНС за 1-2 дня), описание продукта в свободной форме («подписка на закрытое Telegram-сообщество, ежемесячная оплата от физических лиц, цифровой продукт»). Срок верификации — 3–5 рабочих дней. В периоды высокой нагрузки (начало/конец квартала) может растянуться до 7 дней.
Шаг 3. Настройка webhook URL и success/fail URL. Это нужно сделать до подачи документов, потому что PaySame требует указать их при регистрации. Менять webhook URL после верификации — отдельная заявка в поддержку с ожиданием 1–3 рабочих дня. Указывайте финальные production URL сразу. Для тестирования используйте ngrok или аналог, а webhook URL — отдельный тестовый эндпоинт.
Шаг 4. Тестирование в sandbox. Тестовые карты: 4111 1111 1111 1111 (успех), 4222 2222 2222 2222 (отказ). Срок — любой в будущем. CVV — любые 3 цифры. Сумма — любая в рублях. Webhook приходит в течение 2–5 секунд после тестовой транзакции. Проверяйте подпись в первую очередь — именно тут часто расходится формат строки для MD5.
Шаг 5. Переключение на боевой режим. После верификации в личном кабинете появляются production API-ключи. Меняете ключи в конфиге, проводите первую боевую транзакцию на минимальную сумму (100 ₽ через реальную карту), проверяете webhook — и система готова.
Работа с СБП: чем отличается от оплаты картой
СБП (Система быстрых платежей) — это мгновенный перевод по номеру телефона или QR-коду. Для Telegram-ботов СБП критично важна: часть аудитории не хочет вводить данные карты в форме на незнакомом сайте, но готова сделать перевод через СБП в знакомом банковском приложении.
PaySame поддерживает СБП в рамках того же API — никаких дополнительных эндпоинтов. Разница в UX: на странице оплаты PaySame клиент видит две вкладки — «Карта» и «СБП». Переключается сам. Ваш webhook получает одинаковый формат ответа в обоих случаях, только поле payment_method будет отличаться: "card" или "sbp".
Конверсия по методам оплаты в клубе «Solar — внутрянка» за Q1 2026: карта — 73% транзакций, СБП — 27%. При этом отказов у СБП-транзакций нет вообще — если клиент выбрал СБП, он почти всегда завершает оплату. У карт — 3–4% отказов из-за лимитов и блокировок. Это хороший аргумент, чтобы явно продвигать СБП в интерфейсе бота: «Оплатить через СБП — быстро и без ввода данных карты».
Безопасность: что нужно проверять в production
Webhook-эндпоинт принимает POST-запросы из интернета. Три обязательных меры безопасности:
Верификация подписи — обязательна. PaySame подписывает каждый webhook MD5-хешем из полей amount, order_id и вашего секрета. Без проверки подписи любой может отправить поддельный webhook и открыть доступ без оплаты. Код верификации показан выше — не пропускайте эту проверку даже в тестовом режиме.
Защита от replay-атак. Один и тот же webhook PaySame может прийти несколько раз (при сетевых ошибках). Сохраняйте paysame_order_id в таблице и проверяйте уникальность перед активацией:
existing = await db.fetchrow(
"SELECT id FROM subscribers WHERE paysame_order_id=$1",
order_id
)
if existing:
return web.Response(text="already processed")
# Иначе — активируем
Логирование всех входящих webhook. Пишите в отдельную таблицу webhook_log каждый входящий запрос с телом и статусом обработки. Это единственный способ расследовать инциденты типа «клиент говорит что заплатил, но доступа нет». Без лога — гадаете.
CREATE TABLE webhook_log (
id SERIAL PRIMARY KEY,
received_at TIMESTAMP DEFAULT NOW(),
order_id VARCHAR(100),
status VARCHAR(20),
amount INTEGER,
raw_body JSONB,
processed BOOLEAN DEFAULT FALSE
);
Эта таблица за 6 месяцев работы клуба накопила 847 записей — из них 4 потребовали ручного расследования. Без лога эти 4 инцидента остались бы неразрешёнными.
AB-тестирование: какой UX продаёт лучше
После запуска базовой интеграции есть смысл протестировать два варианта UX в боте — это занимает 2-3 недели и даёт данные для принятия решения.
Вариант А: одна кнопка «Подписаться», внутри выбор тарифа. Пользователь жмёт одну кнопку, бот показывает два тарифа, он выбирает, получает ссылку. Три шага до оплаты.
Вариант Б: две кнопки сразу — «2 500/мес» и «4 999/3 мес». Пользователь видит оба тарифа в первом сообщении без дополнительных экранов. Два шага до оплаты.
По результатам теста в клубе (январь–февраль 2026, 214 уникальных пользователей, разделены 50/50): Вариант Б дал конверсию 74% vs 67% у Варианта А. Разница статистически значима при таком объёме. Вариант Б работает лучше — меньше трения между желанием и оплатой.
Тест был простым: боту добавили флаг ab_group в таблице subscribers, рандомно присваивали A или B при первом контакте, разные обработчики для каждой группы. Считали конверсию через SQL раз в неделю.
Интеграция с несколькими Telegram-каналами: клуб с разными тарифами и доступами
Если продукт предполагает несколько уровней доступа — например, базовый канал и премиум-канал с дополнительным контентом — PaySame позволяет реализовать это без дополнительной инфраструктуры. Один агрегатор, разные суммы платежей, разные действия webhook-обработчика.
Схема для двухуровневого клуба:
CLUB_PLANS = {
"basic_1m": {
"amount": 250000,
"label": "Базовый — 2 500 руб./мес",
"channels": [CLUB_BASIC_CHANNEL_ID],
"interval_days": 30,
},
"pro_1m": {
"amount": 490000,
"label": "Pro — 4 900 руб./мес",
"channels": [CLUB_BASIC_CHANNEL_ID, CLUB_PRO_CHANNEL_ID],
"interval_days": 30,
},
}
async def activate_channels(telegram_id: int, plan_key: str):
plan = CLUB_PLANS[plan_key]
for channel_id in plan["channels"]:
invite = await bot.create_chat_invite_link(
chat_id=channel_id,
member_limit=1,
expire_date=int(time.time()) + 3600
)
await bot.send_message(
telegram_id,
f"Ваша ссылка для входа:\n{invite.invite_link}"
)
При такой структуре webhook-обработчик определяет тариф по сумме платежа (amount) и вызывает activate_channels с нужным набором каналов. Один webhook URL — все тарифы.
Важный нюанс: при даунгрейде (клиент перешёл с Pro на Basic) нужно исключить его из Pro-канала. PaySame не сигнализирует о смене тарифа — это логика вашей системы. В таблице subscribers храните plan из предыдущего периода и при активации нового — сравниваете, нужно ли удалять из каналов, которые не входят в новый тариф.
Для клуба «Solar — внутрянка» двухуровневую модель пока не запускали — работаем с одним каналом и двумя временными тарифами (1 месяц и 3 месяца). Но структура кода заложена с расчётом на масштаб.
Как PaySame работает с самозанятыми
Самозанятые — отдельный случай. PaySame не подключает самозанятых напрямую: у самозанятого нет расчётного счёта ИП, а PaySame требует выплаты на р/с юрлица или ИП.
Два рабочих варианта для самозанятых:
- Через партнёрскую схему агрегатора. Ряд агрегаторов (ЮKassa «Касса для самозанятых», некоторые посредники) позволяют самозанятым принимать платежи — чек автоматически формируется в «Мой налог», выплаты идут на личный счёт. PaySame в этом режиме не работает, но такая схема стоит дополнительной интеграции.
- Открыть ИП. Для стабильного подписочного бизнеса с оборотом от 50k ₽/мес — выгоднее ИП на УСН 6%: меньше ограничений, стандартная интеграция с PaySame, расчётный счёт. Открытие ИП онлайн через Госуслуги — 1–3 рабочих дня, без госпошлины.
«Мой налог» как чековая система сохраняется при любом варианте — самозанятый обязан выбивать чек на каждую продажу физлицу.
Когда PaySame — не лучший выбор
Три ситуации, где нужен другой агрегатор:
Нужны автосписания. Если бизнес-модель — привязать карту один раз и списывать автоматически без участия клиента — PaySame не подходит. CloudPayments или ЮKassa с модулем рекуррентных платежей, тариф от 5 000 ₽/мес.
Клиенты за пределами РФ. PaySame — только рубли, только российские методы оплаты. Для международных клиентов: USDT TRC20 через crypto-gateway или Stripe через нероссийское юрлицо.
Оборот от 1M ₽/мес. Разница между 2.5% (CloudPayments) и 3.5% (PaySame) при миллионном обороте = 10 000 ₽/мес. На таком масштабе считайте тарифы индивидуально — у ЮKassa и CloudPayments есть переговорные условия.
Для старта с нуля и оборота до 500k ₽/мес — PaySame закрывает задачу без лишних сложностей.
Итоги и следующий шаг
Схема работает в клубе «Solar — внутрянка» с января 2024 года: Telegram-бот на aiogram 3 → PaySame webhook → PostgreSQL → автоматический доступ в закрытый канал. Поллер как страховка от потерянных webhooks. Напоминания о продлении через systemd-таймер с еженедельным SQL-аудитом.
Из практических выводов: используйте telegram_id как основной идентификатор в order_id, не телефон и не email. Так проще трассировать конкретный платёж в логах и не хранить лишние персональные данные. Подпись webhook — тестируйте первой, это самое частое место ошибки при интеграции PaySame.
Полный код бота — payment_poller.py, webhook-обработчик, планировщик напоминаний, SQL-схема и конфиги systemd — в клубе «Solar — внутрянка». Там же разбор аналитики подписок: откуда приходят лиды, на каком шаге воронки теряются и как считать LTV подписчика. Подробнее о финансовой отчётности — в статье «Автоматический мониторинг финансов».
Бери и адаптируй: https://4bos.ru/inside/, от 2 500 ₽/мес.
— Solar OS.