Прямой ответ
Вебхук платёжного провайдера — HTTP-запрос с уведомлением об изменении состояния платежа. Провайдер ожидает быстрый ответ и повторяет доставку при ошибке, поэтому обработка строится вокруг трёх требований: проверка подлинности, идемпотентность и короткий ответ.
Проверка подписи выполняется до разбора полезной нагрузки:
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: bytes) -> bool:
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Сравнение выполняется функцией постоянного времени, поскольку обычное сравнение строк раскрывает совпадение по времени ответа.
Обработчик сохраняет событие и отвечает немедленно:
@app.post("/webhooks/payments")
async def payment_webhook(request: Request):
raw = await request.body()
if not verify(raw, request.headers["X-Signature"], WEBHOOK_SECRET):
raise HTTPException(status_code=400)
event = json.loads(raw)
stored = await store_event(event) # идемпотентная запись
if stored:
await enqueue_for_processing(event["id"])
return {"received": True}
Три свойства определяют надёжность приёма.
| Требование | Нарушение | Последствие |
|---|---|---|
| Проверка подписи | Пропущена | Подделка успешной оплаты |
| Идемпотентность | Отсутствует | Двойное применение события |
| Быстрый ответ | Долгая обработка | Повторная доставка, дубли |
| Учёт порядка | Не учтён | Устаревшее состояние перезаписывает актуальное |
Провайдер повторяет доставку при отсутствии ответа в течение нескольких секунд, поэтому долгая обработка внутри обработчика приводит к потоку дублей. Очередь задач отделяет приём от обработки; об очередях и гарантиях — в статье про надёжность доставки.
Порядок доставки и устаревшие события
Провайдер не гарантирует доставку уведомлений в порядке возникновения, поэтому событие с более ранним состоянием может прийти после события с более поздним.
Три приёма устраняют перезапись актуального состояния устаревшим.
Метка времени события. Состояние применяется только если метка времени события новее сохранённой. Событие с более ранней меткой фиксируется в журнале, но не меняет состояние.
Версия состояния. Номер версии увеличивается при каждом изменении, и применение выполняется только для большей версии.
Явная машина состояний. Переход выполняется только по допустимому правилу: из состояния «ожидает оплаты» возможен переход в «оплачен» либо «отменён», а обратный переход запрещён; о переходах — в статье про состояния платежа.
Практическое следствие: уведомление не устанавливает состояние напрямую, а запускает проверку актуального состояния у провайдера. Такой подход устраняет зависимость от порядка доставки, поскольку источником становится ответ провайдера, а не последовательность уведомлений.
Отдельный случай — повторная доставка того же события. Провайдер использует идентификатор события, поэтому уникальное ограничение по идентификатору отклоняет дубли на уровне базы:
CREATE TABLE webhook_events (
event_id text PRIMARY KEY,
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz
);
Практика: обработка платёжных уведомлений
1. Проверяйте подпись до разбора данных. Разбор непроверенной полезной нагрузки выполняется над данными неизвестного отправителя.
2. Сохраняйте событие до обработки. Запись в таблицу событий с уникальным идентификатором делает повторную доставку безопасной.
3. Отвечайте немедленно. Обработка выполняется в фоновой задаче, а ответ провайдеру возвращается после сохранения события.
4. Используйте постоянное по времени сравнение подписи. Обычное сравнение строк раскрывает информацию о совпадении через время ответа.
5. Не применяйте устаревшие события. Метка времени либо версия состояния предотвращает перезапись актуального состояния.
6. Обрабатывайте повторяющиеся ошибки отдельно. Событие, которое не удаётся обработать несколько раз, помещается в очередь недоставленных сообщений; о разборе ошибок — в статье про dead letter queue.
7. Ведите журнал принятых событий. Журнал позволяет восстановить последовательность при разборе расхождений.
8. Проверяйте состояние у провайдера. Уведомление — сигнал к проверке, а не источник истины; о схеме интеграции — в статье про интеграцию платёжного шлюза.
Рекомендации:
- Разделяйте приём и обработку. Приём отвечает провайдеру, обработка выполняется отдельно, поэтому сбой обработки не приводит к повторной доставке.
- Не возвращайте ошибку при успешном сохранении. Ответ с ошибкой после сохранения события провоцирует дубли, которые отклоняются уникальным ограничением, но создают лишний трафик.
- Учитывайте задержку уведомлений. Состояние в интерфейсе обновляется по проверке у провайдера, а не по ожиданию уведомления.
- Проверяйте идемпотентность обработчика. Повторное применение события не должно приводить к повторному выполнению действия.
- Мониторьте очередь необработанных событий. Рост числа несохранённых либо необработанных событий указывает на сбой обработки.
Практический сценарий: заказ помечен оплаченным дважды
Контекст: после успешной оплаты заказ получил два уведомления о подтверждении оплаты, и действие после оплаты выполнилось дважды.
- Диагностика. В журнале событий оказались две записи с разными идентификаторами событий, но одним идентификатором платежа. Обработчик выполнял действие при каждом уведомлении без проверки ранее выполненного.
- Причина. Провайдер отправил повторное уведомление после изменения состава полей, а обработчик не проверял, было ли действие по платежу выполнено ранее. Идемпотентность обеспечивалась только на уровне записи события, но не на уровне действия.
- Действия. (а) Добавили проверку состояния платежа перед выполнением действия: действие выполняется только при перехводе из «ожидает оплаты» в «оплачен»; (б) реализовали переход в отдельной транзакции с условным обновлением, чтобы параллельные уведомления не выполнили действие одновременно; (в) отменили второе выполненное действие; (г) добавили метрику числа событий на платёж для обнаружения аномальных повторов; (д) включили проверку состояния у провайдера вместо применения состояния из уведомления.
- Результат. Повторное уведомление не приводит к повторному действию, поскольку переход выполняется условно и только один раз.
Вывод: идемпотентность требуется не только для записи события, но и для действия после оплаты. Условный переход состояния в транзакции исключает двойное выполнение при повторных уведомлениях, а проверка состояния у провайдера устраняет зависимость от порядка и состава доставки.
FAQ
Что возвращать провайдеру при ошибке обработки? Успешный ответ, если событие сохранено. Обработка выполняется в фоновой задаче, а ответ с ошибкой провоцирует повторную доставку.
Нужно ли проверять подпись, если адрес обработчика неизвестен? Да. Отсутствие публичного адреса не заменяет проверку подлинности, поскольку запрос может быть отправлен любым отправителем.
Как обрабатывать события, пришедшие не по порядку? Применять состояние только при более новой метке времени либо версии, а актуальное состояние получать запросом к провайдеру.
Сколько раз провайдер повторяет доставку? Зависит от провайдера и настраивается политикой повторов. Обработчик должен быть идемпотентным независимо от числа повторов.
Источники: Stripe webhooks, PayPal webhooks, Python hmac — ссылки в шапке.