Самая неприятная заявка в поддержку выглядит так: «Я оплатил, деньги списались, скриншот прилагаю — а заказа нет». Деньги действительно ушли, платёжная система их действительно приняла, а на сайте ничего не произошло. Почти всегда причина одна и та же: сайт узнаёт об оплате не оттуда, откуда следует.
Разберём, как приём платежей устроен на самом деле и что нужно сделать, чтобы ни одна оплата не потерялась.
В сделке участвуют трое, а не двое
Когда клиент нажимает «Оплатить», происходит вот что:
- Ваш сайт создаёт заказ и отправляет клиента на страницу оплаты.
- Платёжный сервис (Robokassa, ЮKassa, банк) принимает деньги.
- Банк клиента подтверждает списание.
И только потом платёжный сервис сообщает вашему сайту: «оплата прошла».
Ключевой момент: сообщений о результате два, и они приходят разными путями.
| Что это | Кто приводит | Можно ли верить |
|---|---|---|
| Success URL — страница «спасибо за оплату» | Браузер клиента | Нет |
| Вебхук (ResultURL, уведомление) | Сервер платёжной системы | Да, после проверки подписи |
Почему странице «спасибо» верить нельзя
Success URL открывает браузер клиента. А значит:
- клиент может закрыть вкладку сразу после оплаты — и страница не откроется вовсе, хотя деньги списаны;
- у клиента может пропасть интернет ровно в этот момент;
- адрес этой страницы видно в адресной строке. Его можно скопировать и открыть ещё раз. Или отправить другу. Или подобрать, поменяв номер заказа.
Если заказ подтверждается по факту открытия Success URL, вы получите два типа проблем сразу: честные клиенты не получат оплаченное, а нечестные получат неоплаченное.
Правильная роль Success URL — показать человеку «спасибо, платёж принят, заказ №123». Ничего менять в базе на этой странице нельзя.
Вебхук — единственный источник правды
Вебхук (у Robokassa это ResultURL, у ЮKassa — уведомление) приходит с сервера на сервер, минуя браузер. Клиент на него повлиять не может.
Обработчик вебхука должен сделать ровно четыре вещи:
- проверить подпись;
- найти платёж в своей базе;
- отметить его оплаченным и сохранить изменения;
- ответить платёжной системе в том формате, который она ждёт.
Разберём каждую — именно здесь и теряются деньги.
Ошибка 1. Не проверять подпись
Адрес вебхука рано или поздно станет известен: он есть в настройках личного кабинета, в логах, в документации подрядчика. Если обработчик верит любому запросу, «оплатить» заказ сможет кто угодно обычным curl.
Подпись — это контрольная сумма от суммы, номера заказа и вашего секретного пароля. Пароль знают только вы и платёжная система, поэтому подделать подпись нельзя.
У Robokassa для входящего вебхука подпись считается так:
import hashlib
def signature(out_sum: str, inv_id: str, password_2: str) -> str:
sign_str = f"{out_sum}:{inv_id}:{password_2}"
return hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()
Подставим реальные значения — сумма 1500.00, заказ №42, пароль my_password_2:
Подводный камень: регистр
Robokassa присылает подпись строчными буквами, а hexdigest().upper() возвращает прописные. Наивное сравнение пришло == посчитали вернёт False на каждом платеже, и обработчик отвергнет все оплаты подряд.
# неправильно: сравнивает "1e6227..." с "1E6227..."
if request_signature == signature(out_sum, inv_id, password_2):
...
# правильно
if request_signature.upper() == signature(out_sum, inv_id, password_2):
...
Если вы передаёте платёжной системе свои дополнительные параметры (у Robokassa они начинаются с Shp_), их тоже надо добавить в подписываемую строку — отсортированными по имени, иначе подпись не сойдётся.
Ошибка 2. Отвечать «200 ОК» на неверную подпись
Логика «подпись не сошлась — молча вернём 200, чтобы не ругалось» встречается на удивление часто. Она опасна вдвойне: вы не узнаете ни о попытке подделки, ни о собственной ошибке в расчёте подписи. Платёжная система будет считать, что уведомление доставлено, и повторять его не станет.
Не сошлась подпись — верните ошибку и обязательно запишите это в лог.
Ошибка 3. Отвечать не в том формате
Платёжные системы по-разному понимают «я принял уведомление»:
- Robokassa ждёт в теле ответа обычный текст
OK42, где 42 — номер заказа. Не JSON, не HTML. Если ответить{"status": "ok"}, Robokassa сочтёт уведомление недоставленным и будет слать его снова и снова. - ЮKassa смотрит только на код 200, тело ей безразлично.
Мелочь, из-за которой обработчик «работает», но платежи всё равно висят неподтверждёнными.
Ошибка 4. Долгий ответ
Типичный обработчик заодно шлёт уведомление менеджеру — письмо или сообщение в мессенджер. Если сделать это до сохранения платежа в базу, получится так: почтовый сервер задумался на 20 секунд, платёж всё это время не зафиксирован, платёжная система не дождалась ответа и прислала уведомление повторно.
Порядок должен быть таким:
payment.status = "succeeded"
await db.commit() # сначала сохранили
await notify_manager(...) # потом уведомили
А ещё лучше — отправлять уведомление фоновой задачей, чтобы ответ платёжной системе уходил сразу.
Ошибка 5. Вебхук придёт дважды — и это норма
Повторная доставка не поломка, а штатное поведение: сеть моргнула, ответ не дошёл, система повторяет. Ваш обработчик обязан быть готов к тому, что один и тот же платёж придёт два, три, пять раз.
Проверка занимает одну строку:
if payment.status == "succeeded":
# Уже обработан: ничего не меняем, но отвечаем успехом,
# иначе система будет повторять уведомление бесконечно.
return f"OK{inv_id}"
Без неё повторный вебхук второй раз спишет остаток со склада, второй раз отправит письмо клиенту и второй раз начислит бонусы.
Дополнительный рубеж: белый список адресов
Вебхук приходит с известных серверов платёжной системы. Их адреса можно пустить, а остальным отказать прямо на nginx, не доводя запрос до приложения. У Robokassa это 185.59.216.65 и 185.59.217.65 — актуальный список всегда есть в её документации.
location ~* webhook {
allow 185.59.216.65;
allow 185.59.217.65;
deny all;
proxy_pass http://backend:8000;
}
Это не замена проверке подписи, а дополнение к ней: подпись защищает от подделки, белый список — от лишнего шума в логах и от перебора.
Важно: если перед вашим nginx стоит ещё один прокси или CDN, то в $remote_addr окажется адрес прокси, а не платёжной системы, и фильтр отсечёт всё подряд. Тогда нужен модуль real_ip с доверенными подсетями.
Чек по 54-ФЗ
Если вы принимаете оплату от физических лиц, покупателю нужно выдать кассовый чек. Своя онлайн-касса для этого не обязательна: и Robokassa, и ЮKassa умеют пробивать чек за вас, если передать им состав заказа — наименование, количество, цену и ставку НДС — в параметре Receipt при создании платежа.
Забыть про чек легко: платежи проходят, деньги приходят, всё выглядит рабочим. Вопросы появляются позже и уже не к разработчику.
Что проверить перед боевым запуском
- Заказ подтверждается только по вебхуку, страница «спасибо» ничего не меняет в базе.
- Подпись проверяется, сравнение не зависит от регистра.
- На неверную подпись обработчик отвечает ошибкой и пишет в лог.
- Формат ответа совпадает с требованиями системы (
OK<номер>для Robokassa). - Повторный вебхук по уже оплаченному заказу ничего не дублирует.
- Уведомления отправляются после сохранения в базу, а лучше — фоном.
- Сумма из вебхука сверяется с суммой заказа в базе.
- В логи не попадают персональные данные покупателя и значения подписей.
- Тестовый режим отключён, боевые пароли подставлены, а прежние — сменены.
Последний пункт важнее, чем кажется: тестовые пароли часто успевают побывать в переписке, в скриншотах и в истории репозитория.
Коротко
Приём платежей ломается не на «сложной криптографии», а на простых вещах: доверии к браузеру, неучтённом регистре букв, неправильном формате ответа и отсутствии проверки на повтор. Все пять ошибок из этой статьи стоят примерно один рабочий день на исправление — и куда больше, если разбираться уже по жалобам клиентов.

Нужно подключить оплату на сайте или проверить, всё ли в порядке с существующей? Посмотрите, что мы делаем с сайтами и веб-приложениями, или напишите нам — разберёмся вместе.


