Оплата внутри бота — это не «прикрутить кнопку», а связка из эквайринга, фискализации, статусов заказа, обработки возвратов и юридического оформления. В мессенджере MAX (российский продукт VK Tech) экосистема платежей моложе, чем у западных конкурентов, и продавцу важно понимать, какие способы реально работают по состоянию на 2026 год, а где придётся доделывать обвязку самому. В этой статье разберём способы приёма оплаты в боте MAX, архитектуру платёжного потока, идемпотентность, фискализацию по 54-ФЗ, рекуррентные списания и типичные антипаттерны, которые ломают деньги в проде.
Способы приёма оплаты в боте MAX
В боте MAX в 2026 году доступно несколько принципиально разных способов принять деньги — выбор определяется не столько комиссией, сколько UX и юридической нагрузкой:
- Нативные платежи MAX Bot API — встроенная форма оплаты в чате через подключённый эквайринг. По состоянию на 2026 год возможности развиваются: VK Tech постепенно расширяет список поддерживаемых провайдеров. Сценарий близок к тому, что предлагают другие мессенджеры — кнопка «Оплатить» прямо в карточке бота, реальная валюта (RUB), а сам платёж проходит через подключённый шлюз.
- Ссылка на платёжную форму эквайера. Бот генерирует одноразовую ссылку (ЮKassa, CloudPayments, Tinkoff, Robokassa, СберПэй), пользователь оплачивает на странице провайдера, бот получает webhook с результатом и активирует услугу.
- СБП по QR. Подходит для офлайн-сценариев и для случаев, где пользователь в любом случае открывает банковское приложение. QR можно сформировать через провайдера или напрямую через банк-партнёра.
- Постоплата и счёт на оплату. Бот отправляет PDF-счёт юрлицу, оплата идёт через расчётный счёт. Удобно для B2B — там оплата по картам в боте редко имеет смысл из-за лимитов и бухгалтерских требований.
- Крипто-платежи. Для узких ниш (международные клиенты, специфичные SaaS) — через шлюзы вроде CryptoCloud или Cryptomus. Фискализация и юридическая часть сложнее, но иногда это единственный способ принять оплату от клиента за пределами РФ.
Для большинства B2C-сценариев оптимально нативные платежи или ссылка на эквайринг — это привычный для пользователя UX и автоматическая фискализация на стороне провайдера.
Сравнительная таблица шлюзов для бота MAX
| Шлюз | Комиссия | СБП | Рекуррент | Чеки 54-ФЗ | Особенности |
|---|---|---|---|---|---|
| ЮKassa | 2.8–3.5% карты, 0.7% СБП | да | да (save_payment_method) | да, из коробки | универсально, удобный API, тестовая среда |
| CloudPayments | 2.5–3.2% | да | да (subscription API) | да | хороший рекуррент, кастомные виджеты |
| Tinkoff Касса | 2.5–3.2% | да | да (Recurrent: Y) | да | выплата T+1, сильный антифрод |
| Robokassa | 3–5% | да | да | да (через АТОЛ) | простое подключение для микро-бизнеса |
| СБП напрямую (банк) | 0.4–0.7% | да | нет | требует своей кассы | минимальная комиссия, сложнее обвязка |
| Крипто-шлюз | 0.5–1.5% | нет | нет | нет (вне РФ-периметра) | для международных клиентов |
Грубый ориентир: если оборот менее 500 000 ₽/мес — берите ЮKassa или CloudPayments, не тратьте время на оптимизацию комиссий. Если оборот больше — добавляйте СБП напрямую через банк, экономия на 1–2 п.п. комиссии быстро окупает доработки.
Архитектура платёжного потока
Минимальная схема надёжной оплаты в боте MAX:
handler → создание заказа в БД (status=pending)
→ создание invoice через API провайдера (Idempotency-Key)
→ отправка ссылки/inline-payment пользователю
→ пользователь оплачивает
провайдер → webhook на ваш сервер (HTTPS, подпись)
→ валидация подписи + проверка суммы
→ обновление статуса заказа (status=paid)
→ активация услуги/выдача товара
→ отправка фискального чека
→ уведомление пользователю в боте
Ключевой принцип — статус заказа меняется только по webhook от провайдера, а не по событию из мессенджера или клика пользователя. Иначе один сетевой сбой превращает оплаченный заказ в «висящий», а пользователь уже видит списание в банке.
Второй принцип — активация услуги после записи факта платежа в БД, не до. В одной транзакции: записали платёж, активировали подписку. Иначе при падении сервера между этими шагами получите услугу без оплаты в учёте — пользователь будет рад, бухгалтер нет.
Создание invoice через ЮKassa
Базовый код создания платежа на Python с requests:
import requests
from uuid import uuid4
YOOKASSA_SHOP_ID = "123456"
YOOKASSA_SECRET_KEY = "live_xxx"
def create_payment(order_id: str, amount_kopecks: int, description: str, return_url: str) -> dict:
idempotence_key = f"order-{order_id}-{uuid4()}"
resp = requests.post(
"https://api.yookassa.ru/v3/payments",
auth=(YOOKASSA_SHOP_ID, YOOKASSA_SECRET_KEY),
headers={
"Idempotence-Key": idempotence_key,
"Content-Type": "application/json",
},
json={
"amount": {
"value": f"{amount_kopecks / 100:.2f}",
"currency": "RUB",
},
"capture": True,
"confirmation": {
"type": "redirect",
"return_url": return_url,
},
"description": description,
"metadata": {
"order_id": order_id,
},
"receipt": build_receipt(order_id, amount_kopecks),
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
Важные моменты:
Idempotence-Keyобязателен — если запрос ретраится из-за сетевой ошибки, провайдер вернёт исходный платёж, а не создаст второй.metadata.order_id— ваш внутренний идентификатор. По нему позже найдёте заказ в БД при webhook.amount.value— строка с двумя знаками после запятой ("499.00"), а не число. Это требование API ЮKassa, частая ошибка.receipt— секция фискализации (см. ниже). Если её не передать, чек придётся выбивать через свою кассу отдельно.
Webhook handler на FastAPI
Webhook — самая критичная часть платёжной интеграции. Рабочий шаблон на FastAPI:
from fastapi import APIRouter, Request, HTTPException
from ipaddress import ip_address, ip_network
router = APIRouter()
YOOKASSA_IPS = [
ip_network("185.71.76.0/27"),
ip_network("185.71.77.0/27"),
ip_network("77.75.153.0/25"),
ip_network("77.75.156.11/32"),
ip_network("77.75.156.35/32"),
ip_network("77.75.154.128/25"),
ip_network("2a02:5180::/32"),
]
def is_yookassa_ip(client_ip: str) -> bool:
addr = ip_address(client_ip)
return any(addr in net for net in YOOKASSA_IPS)
@router.post("/webhooks/yookassa")
async def yookassa_webhook(request: Request):
client_ip = request.headers.get("X-Forwarded-For", request.client.host).split(",")[0].strip()
if not is_yookassa_ip(client_ip):
raise HTTPException(status_code=403, detail="forbidden")
body = await request.json()
event = body.get("event")
payment = body.get("object", {})
payment_id = payment.get("id")
order_id = payment.get("metadata", {}).get("order_id")
if not payment_id or not order_id:
raise HTTPException(status_code=400, detail="missing fields")
if event == "payment.succeeded":
await handle_payment_succeeded(order_id, payment_id, payment)
elif event == "payment.canceled":
await handle_payment_canceled(order_id, payment_id, payment)
elif event == "refund.succeeded":
await handle_refund_succeeded(order_id, payment_id, payment)
return {"ok": True}
ЮKassa не подписывает webhook'и HMAC — она использует whitelist IP-адресов. У других провайдеров (CloudPayments, Tinkoff) подпись через HMAC-SHA256 обязательна — без проверки злоумышленник пришлёт поддельный «успешный платёж» и получит услугу бесплатно.
Идемпотентность webhook и защита от двойных списаний
Webhook может прилететь дважды (а то и трижды): таймаут на вашей стороне, ретрай на стороне провайдера, дубль из-за переадресации. Дедуп нужен на двух уровнях.
Уровень 1: middleware на уникальность события.
from sqlalchemy import select
from datetime import datetime
async def handle_payment_succeeded(order_id: str, payment_id: str, payload: dict):
async with db.transaction():
existing = await db.fetch_one(
select(Payment).where(Payment.provider_payment_id == payment_id)
)
if existing and existing.status == "paid":
return
await db.execute(
insert(Payment).values(
order_id=order_id,
provider_payment_id=payment_id,
amount_kopecks=int(float(payload["amount"]["value"]) * 100),
currency=payload["amount"]["currency"],
status="paid",
paid_at=datetime.utcnow(),
raw_payload=payload,
)
)
await db.execute(
update(Order).where(Order.id == order_id).values(status="paid")
)
await activate_service(order_id)
await notify_user(order_id, status="paid")
Уровень 2: уникальный индекс в БД. На колонке payments.provider_payment_id поставьте UNIQUE — даже если кодовая логика проморгает дубль, БД не даст вставить второй раз и кинет IntegrityError, который обработчик превратит в 200 OK для провайдера.
Уровень 3: проверка суммы. Никогда не доверяйте сумме из webhook буквально — сравнивайте с тем, что выставили на сервере по order_id. Если суммы не совпадают (расхождение даже на копейку) — логируйте инцидент и не активируйте услугу.
Фискализация по 54-ФЗ
Для всех платежей физлицам в РФ обязателен онлайн-чек через ОФД. Варианты:
- Чек через провайдера. ЮKassa, CloudPayments и Tinkoff формируют чек автоматически, если в платёж передать секцию
receipt. Это самый простой путь. - Своя облачная касса. АТОЛ Онлайн, OFD.ru, Бизнес.Ру, ferma.ofd.ru — отдельная интеграция с фискальным накопителем по протоколу ФФД 1.2. Нужна, если хотите контролировать формирование чеков самостоятельно или интегрироваться с собственной товароучётной системой.
Структура секции receipt для ЮKassa:
def build_receipt(order_id: str, amount_kopecks: int) -> dict:
return {
"customer": {
"email": get_customer_email(order_id),
},
"items": [
{
"description": "Подписка Pro на месяц",
"quantity": "1.00",
"amount": {
"value": f"{amount_kopecks / 100:.2f}",
"currency": "RUB",
},
"vat_code": 1,
"payment_subject": "service",
"payment_mode": "full_prepayment",
}
],
"tax_system_code": 2,
}
Что значат поля:
vat_code— НДС:1без НДС,2НДС 0%,3НДС 10%,4НДС 20%,5НДС 10/110,6НДС 20/120.payment_subject— предмет расчёта:commodity(товар),service(услуга),payment(платёж),intellectual_activity(РИД).payment_mode— способ расчёта:full_prepayment(полная предоплата),partial_prepayment,advance,full_payment(полный расчёт).tax_system_code— система налогообложения:1ОСН,2УСН доходы,3УСН доходы−расходы,5ЕСХН,6ПСН.customer.email— обязательно для отправки чека покупателю по 54-ФЗ. Если нет email, можноphone.
Своя касса через АТОЛ Онлайн
Если используете не провайдерскую фискализацию, а АТОЛ Онлайн, поток другой — двухшаговый:
import requests
ATOL_LOGIN = "your_login"
ATOL_PASSWORD = "your_password"
ATOL_GROUP_CODE = "your_group_code"
def atol_get_token() -> str:
resp = requests.post(
"https://online.atol.ru/possystem/v4/getToken",
json={"login": ATOL_LOGIN, "pass": ATOL_PASSWORD},
timeout=10,
)
return resp.json()["token"]
def atol_send_receipt(order_id: str, email: str, items: list, total_kopecks: int) -> str:
token = atol_get_token()
external_id = f"order-{order_id}"
payload = {
"external_id": external_id,
"receipt": {
"client": {"email": email},
"company": {
"email": "shop@example.ru",
"sno": "usn_income",
"inn": "7707083893",
"payment_address": "https://example.ru",
},
"items": items,
"payments": [
{"type": 1, "sum": total_kopecks / 100}
],
"total": total_kopecks / 100,
},
"timestamp": datetime.now().strftime("%d.%m.%Y %H:%M:%S"),
}
resp = requests.post(
f"https://online.atol.ru/possystem/v4/{ATOL_GROUP_CODE}/sell",
headers={"Token": token},
json=payload,
timeout=15,
)
return resp.json()["uuid"]
После отправки чека АТОЛ возвращает uuid, по которому через несколько секунд через /report/{uuid} забираете финальный фискальный документ с номером чека и QR-кодом для покупателя.
Webhook-события и их обработка
Минимальный набор событий, которые нужно обрабатывать:
| Событие | Когда приходит | Что делать |
|---|---|---|
payment.waiting_for_capture | пользователь авторизовал, но не списано | капчурить через /payments/{id}/capture или авто-capture |
payment.succeeded | средства списаны | активировать услугу, выбить чек, уведомить |
payment.canceled | банк отказал, истёк таймаут | пометить заказ failed, предложить повтор |
refund.succeeded | возврат прошёл | отозвать услугу, выбить чек коррекции, уведомить |
deal.closed | (для маркетплейсов) сделка закрыта | финализировать расчёты с продавцом |
Для большинства простых сценариев (предоплата) достаточно payment.succeeded + payment.canceled + refund.succeeded. Для отложенного капча (бронирования, такси) добавьте waiting_for_capture.
Возвраты и частичные возвраты
Возврат — через API провайдера, не через мессенджер. Полный возврат ЮKassa:
def refund_payment(payment_id: str, amount_kopecks: int, reason: str) -> dict:
resp = requests.post(
"https://api.yookassa.ru/v3/refunds",
auth=(YOOKASSA_SHOP_ID, YOOKASSA_SECRET_KEY),
headers={
"Idempotence-Key": str(uuid4()),
"Content-Type": "application/json",
},
json={
"amount": {
"value": f"{amount_kopecks / 100:.2f}",
"currency": "RUB",
},
"payment_id": payment_id,
"description": reason,
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
Частичный возврат — указываете сумму меньше изначальной. Можно делать несколько частичных возвратов подряд, пока сумма всех возвратов не достигнет суммы платежа.
При возврате провайдер автоматически выбьет чек коррекции в ОФД (если был передан receipt при изначальном платеже) — это важно для 54-ФЗ. Если делаете возврат через свою кассу (АТОЛ), фискальный документ типа «возврат прихода» формируете отдельным запросом.
Срок добровольного возврата по закону «О защите прав потребителей» — 10 дней на банковский счёт после заявления покупателя. Чарджбеки (споры через банк) разбираются 30–45 дней через переписку с провайдером.
Рекуррентные платежи и подписки
В боте MAX нативного механизма подписок нет — реализуете на стороне сервера. Архитектура:
- Первый платёж с сохранением метода. При создании платежа передаёте
save_payment_method: true:payload = { "amount": {"value": "499.00", "currency": "RUB"}, "save_payment_method": True, "capture": True, "confirmation": {"type": "redirect", "return_url": return_url}, "metadata": {"order_id": order_id, "subscription_id": sub_id}, } - Сохранение токена карты. В webhook
payment.succeededприходитpayment_method.id— сохраняете в БД, привязываете к подписке. Сама карта остаётся у провайдера, у вас только токен. - Списание по cron. Каждый месяц по расписанию создаёте платёж с
payment_method_idи безconfirmation— деньги списываются автоматически:payload = { "amount": {"value": "499.00", "currency": "RUB"}, "payment_method_id": stored_method_id, "capture": True, "description": "Продление подписки Pro", "metadata": {"subscription_id": sub_id, "period": "2026-05"}, } - Обработка неудач. Если статус
canceled(нет средств, истёк срок карты) — даёте grace-period 3 дня, шлёте напоминание в бот с кнопкой обновить карту.
Юридически нужны: оферта на рекуррент с явным упоминанием суммы и периодичности, отдельное согласие пользователя галочкой перед первой оплатой, хранение факта акцепта в БД (user_id, offer_version, accepted_at, ip) для разрешения споров.
Безопасность платежей
Базовые правила, которые часто нарушают:
- Не храните данные карт у себя. PCI DSS — отдельная сертификация ценой в миллионы рублей. Всегда работайте через токены провайдера (
payment_method_id). - Проверяйте подписи webhook. ЮKassa — IP whitelist, CloudPayments — HMAC-SHA256 в заголовке
Content-HMAC, Tinkoff — SHA-256 от конкатенации полей. Без проверки = бесплатные подписки для злоумышленников. - HTTPS обязательно. Webhook-эндпоинт только по TLS с валидным сертификатом. HTTP-эндпоинты провайдеры не вызывают вообще.
- Не доверяйте данным от клиента. Сумма, состав корзины, скидки — всё считается на сервере. Клиент присылает только
order_idилиcart_id. - Логирование без чувствительных полей. В логи можно
payment_id,order_id,amount,status. Нельзя —secret_key, тело webhook целиком, email клиента в открытом виде, токены карт. - Rate limiting на API. Создание заказов и инициация платежей — типичная цель для скриптовых атак. Лимит 10 запросов в минуту с одного
user_idплюс капча на подозрительные паттерны.
Тестовые карты для разработки
У всех провайдеров есть sandbox с тестовыми картами. Несколько ходовых:
- ЮKassa тест:
5555 5555 5555 4444(Mastercard, успех),5555 5555 5555 4477(отказ),4111 1111 1111 1026(Visa, требует 3-D Secure). - Tinkoff тест:
4300 0000 0000 0777(успех),5000 0000 0000 0009(отказ от банка). - CloudPayments тест:
4242 4242 4242 4242(успех),4012 8888 8888 1881(отказ).
CVC и срок действия — любые валидные. На приёмке прогоняйте не только happy path, но и: отказ банка, таймаут запроса, дублирующий webhook, частичный возврат, оплата того же order_id дважды, истечение confirmation_url.
Метрики платёжной воронки
Что мерить, чтобы понимать, где теряются деньги:
- Conversion от инициации до оплаты —
payment.succeeded / payment.created. Норма для эквайринга 60–80%, для СБП 75–90%. Падение ниже — повод смотреть UX оплаты и выбор провайдера. - Refund rate —
refunds / payments. Нормально 1–3%; выше 5% — проблема либо с продуктом, либо с антифродом. - Средний чек (AOV) — общая выручка / число заказов. Используется для сегментации и для определения, нужен ли вам upsell на этапе чекаута.
- Time-to-payment — медианное время от создания заказа до
payment.succeeded. Если больше 5 минут — пользователь откладывает оплату, и значит надо слать напоминание через бот через 10–15 минут. - Failed-by-bank rate —
payment.canceled с reason=bank_rejected. Если выше 10%, играйте 3-D Secure-настройками или меняйте антифрод-правила.
152-ФЗ и обработка персональных данных
Приём оплаты — это автоматически обработка ПДн. Что нужно:
- Согласие на обработку ПДн перед сбором email, телефона, имени. Отдельная страница
/consentна сайте + чекбокс перед первой оплатой. - Минимизация данных в логах. Email — только в БД с шифрованием на уровне колонки или Application Layer Encryption. В логи попадают
user_idиorder_id, не email и не телефон. - Не передавать ПДн в провайдер сверх необходимого. В
metadataплатежа не пишите ФИО, паспорт, адрес доставки — только идентификаторы. - Уведомление РКН. Для приёма оплаты с ПДн нужно подать уведомление об обработке ПДн в Роскомнадзор. Если данные передаются в Telegram-бот для уведомлений — отдельное уведомление о трансграничной передаче.
- Хранение факта акцепта. В БД:
user_id,consent_version,accepted_at,ip,user_agent. Это доказательство в случае спора.
Антипаттерны, которые ломают деньги
Грабли, на которые регулярно наступают:
- Хранение карт самостоятельно без PCI DSS. Запрещено законодательно, штрафы и блокировка эквайером. Только токены провайдера.
- Доверие к статусу платежа из клиента. Сценарий: клиент отправляет POST
/order/paidпосле редиректа с формы оплаты, бэкенд верит и активирует услугу. Любой школьник с DevTools получает услугу бесплатно. Статус — только из webhook от провайдера. - Игнор идемпотентности. Webhook прилетает дважды → подписка продлевается на 2 месяца сразу → пользователь возмущён → возврат + ручная сверка. Идемпотентность на уровне БД (уникальный индекс) — обязательна.
- Отсутствие обработки
payment.canceled. Заказ висит вpendingбесконечно, пользователь не понимает, что делать. Нужно: пометить failed, прислать уведомление в бот с кнопкой «Повторить оплату». - Отсутствие фискализации. Продажа без чека = штраф ФНС от 30 000 ₽ за каждое нарушение. Подключайте
receiptв платёж сразу, не «потом доделаем». - Webhook-эндпоинт без авторизации. Открытый POST-эндпоинт принимает любые JSON-ы как успешные платежи. IP-whitelist или HMAC-подпись — критично с первого дня.
- Тяжёлая логика синхронно в webhook. Webhook должен ответить 200 за 5–10 секунд, иначе провайдер ретраит. Отправку email, выбивку чека, обновление CRM — в фоновую очередь (Celery, RQ, ARQ), в webhook только запись в БД.
Тестовый режим и приёмка
У всех приличных эквайеров есть sandbox: тестовые карты, симуляция отказов, ускоренные таймауты. Чек-лист приёмки:
- Успешная оплата картой → активация услуги → чек на email.
- Отказ банка → корректное сообщение в бот, кнопка «Повторить».
- Таймаут оплаты → заказ переведён в
expired, кнопка создать новый. - Дубль webhook → второй раз не активирует услугу повторно.
- Полный возврат → услуга деактивирована, чек возврата выбит.
- Частичный возврат → корректно скорректирован остаток.
- Рекуррентное списание → новый платёж создан, услуга продлена.
- Истёк срок карты при рекурренте → пользователь уведомлён, grace-period работает.
Без прогона негативных сценариев первое же ребро в проде будет ваше — и оно почти всегда происходит ночью в выходной.
Итого
Подключение оплаты в боте MAX — это интеграция бот ↔ провайдер ↔ касса с правильной обработкой webhook, идемпотентностью, фискализацией по 54-ФЗ и юридическим оформлением. Технически проект занимает от недели на простой эквайринг до месяца на сценарии с возвратами, рекуррентными подписками и собственной кассой. Главное — не экономить на тестировании негативных сценариев, обработке дублей webhook и проверке подписей. Деньги пользователя — это самое больное место продукта, и любая ошибка стоит дороже, чем разработка нормальной инфраструктуры с первого дня.
Частые вопросы
Какие способы оплаты можно подключить в боте MAX?
Пять основных. Нативные платежи MAX Bot API — встроенная форма оплаты в чате через подключённый эквайринг (возможности развиваются по состоянию на 2026 год). Ссылка на платёжную форму эквайера (ЮKassa, CloudPayments, Tinkoff, Robokassa, СберПэй) — бот генерирует одноразовую ссылку, пользователь оплачивает на странице, бот получает webhook. СБП по QR — для офлайн-сценариев или быстрых платежей через банковское приложение. Постоплата или счёт PDF — оплата вне платформы, удобно для B2B. Крипто-шлюзы (CryptoCloud, Cryptomus) — для международных клиентов в узких нишах. Для B2C обычно нативные платежи или ссылка эквайера, для B2B — счёт.
Сколько стоит подключить оплату в боте MAX?
От недели работы на простой эквайринг (создание заказа, ссылка на оплату, webhook, обновление статуса) до месяца на сценарии с возвратами, рекуррентными подписками и подключением своей кассы. Стоимость интеграции — от 30 000 ₽ за базовую схему до 150 000 ₽ за полноценную с фискализацией, частичными возвратами и автосписаниями. Плюс комиссии провайдера (2–3% за карты, 0,4–0,7% за СБП напрямую), фиксированная стоимость облачной кассы (1500–5000 ₽/мес для АТОЛ Онлайн и аналогов). Сравнивать провайдеров стоит не только по комиссии, но и по качеству API, наличию тестовой среды и встроенной фискализации.
Нужна ли касса для приёма оплаты в боте MAX по 54-ФЗ?
Да, обязательно. Для российского бизнеса любой приём оплаты от физлица — это автоматически фискализация по 54-ФЗ. Варианты: провайдер сам формирует чеки (ЮKassa, CloudPayments, Tinkoff умеют — это самый простой путь, передаёте секцию receipt в платёж), или подключается отдельная облачная касса (АТОЛ Онлайн, OFD.ru, Бизнес.Ру, ferma.ofd.ru). В чек входят: позиции с описанием, количеством, ценой и НДС, признак расчёта (полная предоплата, аванс, полный расчёт), предмет расчёта (товар, услуга), система налогообложения, email или телефон покупателя. Продажа без чека — штраф ФНС от 30 000 ₽ за каждое нарушение.
Как обеспечить идемпотентность платежей в боте MAX?
Три уровня защиты от дублей. Первый — Idempotency-Key в API провайдера: уникальный UUID в заголовке POST /payments, повторный запрос с тем же ключом вернёт исходный платёж, не создаст второй. Второй — уникальный индекс на колонке payments.provider_payment_id в БД: даже если кодовая логика проморгает дубль webhook, БД не даст вставить второй раз и кинет IntegrityError. Третий — проверка event_id или хеша тела webhook перед обработкой. Webhook от провайдера может прилететь дважды (таймаут на вашей стороне, ретрай провайдера) — без идемпотентности подписка продлевается на 2 месяца сразу и пользователь возмущён. Идемпотентность нужна с первого дня, не «потом доделаем».
Как защитить webhook оплаты в боте MAX от подделки?
Два механизма в зависимости от провайдера. ЮKassa использует IP-whitelist — проверяете, что webhook пришёл с IP-адреса из официального списка ЮKassa (185.71.76.0/27 и другие подсети). CloudPayments и Tinkoff используют HMAC-подпись: провайдер кладёт в заголовок (Content-HMAC для CloudPayments) HMAC-SHA256 от тела запроса с secret-ключом, вы пересчитываете и сравниваете. Без проверки злоумышленник пришлёт поддельный webhook вида {"event": "payment.succeeded", "object": {"metadata": {"order_id": "X"}}} и получит услугу бесплатно. HTTPS обязателен — провайдеры не вызывают HTTP-эндпоинты вообще. Дополнительно — rate limiting на webhook-роут и алёрты на резкий рост подозрительных запросов.
Как сделать рекуррентные платежи и подписки в боте MAX?
В боте MAX нативного механизма подписок нет — реализуется на стороне сервера. Архитектура: при первом платеже передаёте провайдеру флаг сохранения метода (save_payment_method: true для ЮKassa, Recurrent: Y для Tinkoff), получаете payment_method_id (токен карты, сама карта остаётся у провайдера). По cron каждый месяц создаёте платёж с payment_method_id без confirmation — деньги списываются автоматически. При статусе succeeded продлеваете подписку в БД, отправляете уведомление в бот. При canceled (нет средств, истёк срок карты) даёте grace-period 3 дня и шлёте напоминание с кнопкой обновить карту через новый invoice с save_payment_method. Юридически нужны: оферта на рекуррент с явным упоминанием суммы и периодичности, отдельное согласие галочкой перед первой оплатой, хранение факта акцепта в БД (user_id, offer_version, accepted_at, ip).
Какие антипаттерны при приёме оплаты в боте MAX встречаются чаще всего?
Семь типичных грабель. Хранение данных карт самостоятельно без PCI DSS-сертификации — запрещено законом, всегда токены провайдера. Доверие к статусу платежа из клиента (POST /order/paid с фронта вместо webhook) — даёт услугу бесплатно любому со скриптом. Игнор идемпотентности webhook — двойное продление подписок и возмущённые пользователи. Отсутствие обработки payment.canceled — заказы висят в pending бесконечно. Отсутствие фискализации по 54-ФЗ — штрафы ФНС от 30 000 ₽ за каждое нарушение. Открытый webhook-эндпоинт без IP-whitelist или HMAC-подписи — поддельные платежи. Тяжёлая логика синхронно в webhook (отправка email, выбивка чека, обновление CRM) — провайдер таймаутит и ретраит, фоновые задачи через Celery или ARQ обязательны.