URL для уведомлений от платёжной системы

Технический адрес (webhook / Result URL / Notification URL), а не страница для покупателя.

…
Сайт статический и без сервера, поэтому эта страница ничего не обрабатывает по-настоящему. Указывать её в настройках платёжной системы пока нет смысла — ниже описано, что должно появиться на этом месте, когда у магазина будет бэкенд.

Зачем нужен отдельный адрес для вебхука

Success URL и Fail URL (страницы «Оплата прошла успешно» и «Оплата не прошла») открываются в браузере покупателя — он может закрыть вкладку, потерять связь или просто не дождаться редиректа. Поэтому они не считаются надёжным подтверждением оплаты. Вебхук платёжная система шлёт своим сервером напрямую на сервер магазина, в обход браузера — это и есть окончательное подтверждение, на которое можно полагаться.

Что должен делать настоящий обработчик

  1. Принимать POST-запрос по этому адресу (обычно application/x-www-form-urlencoded или JSON — зависит от платёжной системы).
  2. Проверять подпись запроса секретным ключом магазина — так сервер убеждается, что запрос действительно от платёжной системы, а не от постороннего.
  3. Находить заказ по его номеру из запроса и сверять сумму платежа с суммой заказа.
  4. Помечать заказ оплаченным в своей базе данных — не в sessionStorage браузера, он серверу недоступен.
  5. Отвечать телом, которое ждёт платёжная система (например, Robokassa ждёт строго OK{номер заказа}, у других систем — своё). Неверный ответ платёжная система расценит как сбой и повторит запрос позже.
  6. Быть готовым к повторным уведомлениям об одном и том же заказе — обрабатывать их так, чтобы повтор ничего не ломал.

Пример обработчика (иллюстрация, не часть этого сайта)

Ниже — набросок на 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

← Вернуться в каталог