Самая неприятная заявка в поддержку выглядит так: «Я оплатил, деньги списались, скриншот прилагаю — а заказа нет». Деньги действительно ушли, платёжная система их действительно приняла, а на сайте ничего не произошло. Почти всегда причина одна и та же: сайт узнаёт об оплате не оттуда, откуда следует.

Разберём, как приём платежей устроен на самом деле и что нужно сделать, чтобы ни одна оплата не потерялась.

В сделке участвуют трое, а не двое

Когда клиент нажимает «Оплатить», происходит вот что:

  1. Ваш сайт создаёт заказ и отправляет клиента на страницу оплаты.
  2. Платёжный сервис (Robokassa, ЮKassa, банк) принимает деньги.
  3. Банк клиента подтверждает списание.

И только потом платёжный сервис сообщает вашему сайту: «оплата прошла».

Ключевой момент: сообщений о результате два, и они приходят разными путями.

Что этоКто приводитМожно ли верить
Success URL — страница «спасибо за оплату»Браузер клиентаНет
Вебхук (ResultURL, уведомление)Сервер платёжной системыДа, после проверки подписи

Почему странице «спасибо» верить нельзя

Success URL открывает браузер клиента. А значит:

  • клиент может закрыть вкладку сразу после оплаты — и страница не откроется вовсе, хотя деньги списаны;
  • у клиента может пропасть интернет ровно в этот момент;
  • адрес этой страницы видно в адресной строке. Его можно скопировать и открыть ещё раз. Или отправить другу. Или подобрать, поменяв номер заказа.

Если заказ подтверждается по факту открытия Success URL, вы получите два типа проблем сразу: честные клиенты не получат оплаченное, а нечестные получат неоплаченное.

Правильная роль Success URL — показать человеку «спасибо, платёж принят, заказ №123». Ничего менять в базе на этой странице нельзя.

Вебхук — единственный источник правды

Вебхук (у Robokassa это ResultURL, у ЮKassa — уведомление) приходит с сервера на сервер, минуя браузер. Клиент на него повлиять не может.

Обработчик вебхука должен сделать ровно четыре вещи:

  1. проверить подпись;
  2. найти платёж в своей базе;
  3. отметить его оплаченным и сохранить изменения;
  4. ответить платёжной системе в том формате, который она ждёт.

Разберём каждую — именно здесь и теряются деньги.

Ошибка 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:

сптордопкиас:ь:115E0602.20709:F4528:9m9y2_Fp3aCs5s7wBo0r3dA_82FDDD7521A

Подводный камень: регистр

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).
  • Повторный вебхук по уже оплаченному заказу ничего не дублирует.
  • Уведомления отправляются после сохранения в базу, а лучше — фоном.
  • Сумма из вебхука сверяется с суммой заказа в базе.
  • В логи не попадают персональные данные покупателя и значения подписей.
  • Тестовый режим отключён, боевые пароли подставлены, а прежние — сменены.

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

Коротко

Приём платежей ломается не на «сложной криптографии», а на простых вещах: доверии к браузеру, неучтённом регистре букв, неправильном формате ответа и отсутствии проверки на повтор. Все пять ошибок из этой статьи стоят примерно один рабочий день на исправление — и куда больше, если разбираться уже по жалобам клиентов.

EASYSOCHI — подключение приёма платежей на сайте

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