Повторы webhook в платёжных системах
Как Qvard повторяет webhook платежа, что считается неудачной доставкой и как собрать обработчик, который переживает повторы, дубли и простой, не выполняя заказ дважды.

Webhook платежа это сообщение, которое нельзя потерять и на которое нельзя среагировать дважды. Сети падают, серверы перезапускаются, поэтому отправитель повторяет доставку; именно поэтому обработчик нужно писать с расчётом на повторы. В материале описано, как повторяет Qvard, что он считает ошибкой и как построить вокруг этого обработчик и процедуру восстановления.
Когда отправляется webhook
Qvard отправляет webhook, когда платёж завершён. Это POST на callback URL, сохранённый в вашем проекте, с небольшим JSON, где ваш id (идентификатор, который вы передали при создании платежа) и status, с подписью в заголовке X-Qvard-Signature.
Неполный, отклонённый или ещё ожидающий платёж webhook о завершении не порождает. Если о таких платежах нужно знать, смотрите их в кабинете; не ждите сообщения, которое не отправляется.
Что считается доставленным
Qvard считает доставку успешной, только когда ваша точка отвечает ровно HTTP 200. Всё остальное ошибка:
- любой другой код ответа, включая
201,202,204и все4xxи5xx; - таймаут, потому что сервер был медленным или недоступным;
- редирект: Qvard не следует за
301и302, поэтому правило, перенаправляющееhttpнаhttpsили добавляющее слеш в конце, ломает доставку.
Эта точность намеренная. 204 No Content выглядит вполне приличным ответом и всё равно считается неудачной доставкой. Возвращайте 200, при желании с пустым телом.
Как работают повторы
После неудачной доставки Qvard пробует снова. Всего пять попыток примерно за шесть минут. Отсюда два следствия:
- Короткий простой покрыт. Если сервер перезапускается или деплой занимает минуту-две, одна из следующих попыток пройдёт, и вы ничего не потеряете.
- Долгий простой нет. После последней попытки Qvard останавливается. Если сервер лежал дольше этих шести минут, уведомление само не придёт; отправьте его по платежу ещё раз из кабинета.
Из-за повторов обработчик иногда увидит одно и то же событие несколько раз, например когда сервер сделал работу, а ответ потерялся по дороге. Пример ниже построен с расчётом на это: обновление меняет только ещё не оплаченный заказ, выполнение ставится в очередь, только если строка изменилась, и ответ в любом случае 200.
import os
from fastapi import FastAPI, Request, Response
from qvard import Qvard
app = FastAPI()
SECRET = os.environ["QVARD_SECRET_KEY"]
qvard = Qvard(SECRET)
@app.post("/webhooks/qvard")
async def qvard_webhook(request: Request):
try:
body = qvard.webhooks.construct_event(await request.body(), request.headers.get("X-Qvard-Signature", ""), SECRET)
except ValueError:
return Response(status_code=400)
order_id, status = body.get("id"), body.get("status")
# Idempotent: the UPDATE changes a row only for an order that is not paid yet.
if status == "completed":
changed = await db.execute(
"UPDATE orders SET state = 'paid' WHERE id = :id AND state <> 'paid'", {"id": order_id}
)
if changed:
await queue.enqueue("fulfil", order_id) # slow work after the answer
return Response(status_code=200) # exactly 200, also for repeats and unknown ordersЧто обязан делать обработчик
Четыре правила закрывают почти все проблемы:
- Проверка подписи. Проверяйте заголовок
X-Qvard-Signature(HMAC-SHA256 вашим секретным ключом по сырому телу) за постоянное время и иначе отвечайте400. Делайте это до того, как действовать по платежу. См. как защитить webhook криптоплатежей. - Идемпотентность. Делайте смену состояния условной, «пометить оплаченным если ещё не оплачен», лучше одним запросом к базе, чтобы две параллельные доставки не победили обе.
- Быстрый ответ. Проверить, обновить, поставить в очередь, ответить ровно 200. Письма, генерацию файлов и вызовы других систем делайте из очереди, чтобы медленный смежный сервис не превратился в таймаут webhook.
- Отвечайте 200 на то, что никогда не обработаете. Неизвестный заказ или повтор тоже должны возвращать 200, иначе Qvard продолжит повторять сообщение, с которым вы уже справились или справитесь никогда.
Сбои, похожие на баги Qvard
Большинство жалоб «webhook не пришёл» имеют причину на принимающей стороне:
- Файрвол или WAF, блокирующий запросы с незнакомых адресов, либо страница «скоро открытие» или обслуживания.
- Редирект с
httpнаhttps, сwwwна голый домен или на адрес со слешем. - Ошибка фреймворка после того, как ответ уже решён, например исключение в middleware, превращающее ваш 200 в 500.
- Медленный обработчик, который делает работу до ответа и упирается в таймаут.
- Кэш или CDN перед маршрутом.
- Неверный адрес, сохранённый в проекте, или опечатка в маршруте.
Проверьте точку снаружи вашей сети запросом с валидным заголовком X-Qvard-Signature и смотрите в журнале доступа сервера код ответа, а не только факт прихода запроса.
Процедура восстановления при пропущенных уведомлениях
Повторы покрывают короткие простои; для остального добавьте страховку.
- Логируйте каждую доставку:
id,status, ваше решение (применено, повтор, неизвестный заказ, ключ отклонён) и время. Пропуск в логе так вы замечаете разрыв. - Сверяйтесь по расписанию. Раз в сутки сравнивайте заказы, которые всё ещё считаете неоплаченными, с платежами и балансом в кабинете Qvard. Всё, что в кабинете завершено, а у вас не оплачено, это пропущенное уведомление.
- Отправляйте повторно из кабинета по таким платежам и давайте идемпотентному обработчику их применить.
- Настройте оповещение, когда серия запросов получает ошибку, чтобы узнать о сломанной точке за минуты, а не в конце дня.
Остальная интеграция, включая создание платежа, в материалах как принимать криптоплатежи на сайте и API для криптоплатежей.
FAQ
Сколько раз Qvard повторяет webhook?
Всего пять попыток примерно за шесть минут. После этого Qvard останавливается, а уведомление по платежу можно отправить повторно из кабинета.
Какой код ответа должна вернуть моя точка?
Ровно 200. Любой другой статус, включая 204, таймаут или редирект, считается неудачной доставкой и повторяется.
Может ли один и тот же webhook прийти дважды?
Да, например когда сервер обработал первую доставку, но ответ потерялся. Делайте обработчик идемпотентным и на повторы всё равно отвечайте 200.
Приходит ли webhook при неполном или отклонённом платеже?
Нет. Webhook отправляется, когда платёж завершён. Остальные платежи проверяйте в кабинете.
