URL для уведомлений от платёжной системы
Технический адрес (webhook / Result URL / Notification URL), а не страница для покупателя.
…
Сайт статический и без сервера, поэтому эта страница ничего не обрабатывает по-настоящему.
Указывать её в настройках платёжной системы пока нет смысла — ниже описано, что должно появиться
на этом месте, когда у магазина будет бэкенд.
Зачем нужен отдельный адрес для вебхука
Success URL и Fail URL (страницы «Оплата прошла успешно» и «Оплата не прошла») открываются в браузере покупателя — он может закрыть вкладку, потерять связь или просто не дождаться редиректа. Поэтому они не считаются надёжным подтверждением оплаты. Вебхук платёжная система шлёт своим сервером напрямую на сервер магазина, в обход браузера — это и есть окончательное подтверждение, на которое можно полагаться.
Что должен делать настоящий обработчик
- Принимать POST-запрос по этому адресу (обычно
application/x-www-form-urlencodedили JSON — зависит от платёжной системы). - Проверять подпись запроса секретным ключом магазина — так сервер убеждается, что запрос действительно от платёжной системы, а не от постороннего.
- Находить заказ по его номеру из запроса и сверять сумму платежа с суммой заказа.
- Помечать заказ оплаченным в своей базе данных — не в
sessionStorageбраузера, он серверу недоступен. - Отвечать телом, которое ждёт платёжная система (например, Robokassa ждёт строго
OK{номер заказа}, у других систем — своё). Неверный ответ платёжная система расценит как сбой и повторит запрос позже. - Быть готовым к повторным уведомлениям об одном и том же заказе — обрабатывать их так, чтобы повтор ничего не ломал.
Пример обработчика (иллюстрация, не часть этого сайта)
Ниже — набросок на Python в духе протокола Robokassa (подпись — MD5 от суммы, номера заказа и секретного слова). У других систем (ЮKassa, CloudPayments, PayOp и т.д.) формат подписи и ответа другой, но принцип совпадает.
from flask import Flask, request
import hashlib
app = Flask(__name__)
SECRET = "..." # секретный ключ из личного кабинета платёжной системы
@app.route("/webhook", methods=["POST"])
def payment_webhook():
data = request.form
order_id = data["InvId"]
amount = data["OutSum"]
signature = data["SignatureValue"]
expected = hashlib.md5(f"{amount}:{order_id}:{SECRET}".encode()).hexdigest()
if signature.lower() != expected.lower():
return "bad signature", 400
order = db.get_order(order_id)
if not order:
return "order not found", 404
if order.status != "paid": # идемпотентность: не обрабатывать повторно
order.status = "paid"
db.save(order)
return f"OK{order_id}" # формат ответа, который ждёт Robokassa