Вебхук — это HTTP-запрос на открытый адрес вашего сервера. Всё, что о нём известно по умолчанию: кто-то прислал JSON. Три вещи превращают его в надёжный сигнал об оплате.
Адрес уведомлений открыт всему интернету. Если обработчик верит телу запроса, то отгрузить заказ может любой, кто угадал адрес и формат — а формат описан в документации.
Поэтому каждое уведомление подписано секретом проекта:
Aurum-Signature: t=<время>,v1=<hmac-sha256>. Подпись
считается от строки <время>.<тело>.
Проверяйте её до разбора тела и до любых действий с заказом. И
обязательно по сырым байтам: тело, прошедшее через JSON.parse и
обратно, отличается пробелами и порядком полей — подпись к нему не сойдётся, и вы
потратите день, решив, что «подпись не работает».
timingSafeEqual,
hash_equals). Обычное сравнение строк выходит на первом различии, и по
времени ответа подпись подбирается побайтово.
Время входит в подписываемую строку не для красоты. Без него перехваченное уведомление остаётся годным вечно: достаточно повторить тот же запрос через неделю. С ним получатель сверяет возраст — старше пяти минут отвергается.
Если ваш сервер ответил ошибкой или не ответил вовсе, уведомление придёт снова — с нарастающими паузами. Это значит, что одно и то же событие вы увидите несколько раз, и обработчик обязан это выдерживать.
Правильный способ — идемпотентность по состоянию, а не по факту получения: не «я это уже видел», а «заказ уже оплачен, делать нечего». Первый подход ломается при потере журнала, второй — нет.
if (order.status === 'paid') return res.sendStatus(200) // уже отгружено
order.status = 'paid'
ship(order)
Уведомления не гарантируют порядок. По одному счёту может прийти
invoice.underpaid, затем invoice.paid — а при повторной
доставке первого вы увидите «недоплачено» после «оплачено».
Не стройте логику на последовательности. Ориентируйтесь на поля: received
и amount в теле говорят всё, что нужно, независимо от того, какое
уведомление пришло раньше.
Обработчик должен вернуть 200 и завершиться. Отправка письма, генерация чека, обращение к складу — всё это в очередь. Долгий ответ приводит к таймауту, таймаут — к повторной доставке, а повторная доставка при неидемпотентном обработчике — ко второй отгрузке.