Webhook для AI API нужно принимать как недоверенный внешний запрос: проверить HMAC-подпись по исходному телу, ограничить допустимый возраст события и обработать уникальный event ID только один раз. Такая схема защищает асинхронные map, crawl и исследовательские задания от подмены, повторной доставки и двойного запуска бизнес-логики.
Зачем webhook, если статус можно опрашивать
Долгая операция не должна удерживать пользовательское HTTP-соединение. Клиент создаёт задание, получает идентификатор и продолжает работу. Когда результат готов, сервис отправляет событие на заранее заданный URL. Это уменьшает число запросов статуса и позволяет сразу продолжить внутренний процесс: сохранить результат, обновить карточку или поставить следующий шаг в очередь.
Polling всё равно полезен как резервный путь. Webhook сообщает о событии, но не обязан быть единственным источником истины. Если доставка задержалась или ваш endpoint был недоступен, приложение может получить состояние через GET /v1/jobs/{id} либо прочитать журнал доставок зарегистрированного endpoint.
В LLMTOKENAPI для простого map или crawl можно передать webhookUrl. Для нескольких типов задач удобнее создать постоянный webhook endpoint и подписать его только на нужные события, например job.completed и job.failed. Актуальные маршруты и scopes перечислены в документации API.
Сначала сохраните секрет и сырой body
Секрет webhook возвращается при создании endpoint и должен храниться так же внимательно, как API-ключ. Не записывайте его в репозиторий, браузерный код или обычные логи. При компрометации перевыпустите секрет и обновите принимающую сторону.
Для проверки подписи нужен именно исходный набор байтов HTTP-тела. Если сначала разобрать JSON, а потом сериализовать объект заново, изменятся пробелы или порядок полей, и корректная подпись перестанет совпадать. Поэтому обработчик должен сначала прочитать raw body, проверить запрос и только затем выполнять JSON.parse.
Руководство GitHub по проверке webhook использует тот же базовый принцип: вычислить HMAC от полученного тела с секретом и сравнить результат безопасным способом. Для LLMTOKENAPI подписываемая строка имеет форму timestamp.eventId.body, а служебные значения приходят в заголовках:
x-llmtokenapi-event-id— стабильный идентификатор события;x-llmtokenapi-timestamp— время создания подписи;x-llmtokenapi-signature— подпись HMAC SHA-256 с версиейv1.
Проверяйте подпись до разбора события
Надёжный обработчик выполняет проверки в фиксированном порядке:
- Убеждается, что все три заголовка присутствуют и имеют ожидаемый формат.
- Проверяет, что timestamp попадает в небольшое допустимое окно относительно серверного времени.
- Собирает строку
timestamp.eventId.rawBodyбез преобразования тела. - Вычисляет HMAC SHA-256 с сохранённым секретом.
- Сравнивает подписи функцией постоянного времени.
- Проверяет event ID в хранилище обработанных событий.
- Только после этого разбирает JSON и запускает бизнес-логику.
Обычное сравнение строк через === не подходит для криптографической проверки. Используйте timingSafeEqual в Node.js или эквивалент вашей платформы. Перед сравнением убедитесь, что буферы одинаковой длины, иначе многие реализации выбросят исключение.
Защититесь от повторной доставки
Даже правильно подписанное событие может прийти повторно. Причина обычно безобидна: принимающий сервер успел обработать запрос, но ответ потерялся, поэтому отправитель повторил доставку. Если обработчик второй раз создаст заказ, письмо или платную задачу, подпись не спасёт от дубля.
Храните eventId в таблице с уникальным индексом. В одной транзакции попробуйте зафиксировать ID и выполнить изменение состояния. Если запись уже существует, верните успешный ответ без повторной работы. Такой подход превращает повторную доставку в нормальный сценарий, а не в инцидент.
Проверка возраста подписи решает другую задачу: не позволяет бесконечно воспроизводить старый корректный запрос. Практическое окно зависит от вашей очереди и сетевой задержки; выбирайте его явно и следите за синхронизацией времени на сервере. Сочетание timestamp и уникального event ID защищает лучше, чем любой из этих механизмов по отдельности.
Отвечайте быстро, тяжёлую работу выполняйте в очереди
Webhook endpoint должен проверить запрос, надёжно сохранить событие и быстро вернуть код 2xx. Извлечение большого результата, отправку писем или обновление поискового индекса лучше передать внутреннему worker. Иначе принимающая сторона может превысить таймаут, а отправитель начнёт повторять уже выполняющуюся работу.
Разделите статусы «событие принято» и «бизнес-операция завершена». Первый означает, что payload сохранён и больше не потеряется. Второй обновляется worker после фактической обработки. Для диагностики сохраняйте event ID, тип события, время получения, результат проверки и внутренний request ID, но не секрет и не чувствительное содержимое пользовательских данных.
Подготовьте endpoint к ошибкам и ротации
Перед production проверьте положительный и отрицательный сценарии. Корректная подпись должна приниматься; изменённое тело, старый timestamp и неверная подпись — отклоняться. Два запроса с одним event ID должны менять состояние только один раз. Временно недоступная база должна приводить к повторяемой ошибке, а не к потере события.
Постоянный webhook endpoint в LLMTOKENAPI можно протестировать, перевыпустить его секрет, посмотреть доставки и повторить конкретную неудачную доставку. Эти операции требуют отдельных scopes webhooks:read и webhooks:write, поэтому ключ для управления webhook не нужно использовать в обычном пользовательском трафике.
Итоговый webhook для AI API состоит не из одного публичного URL. Это проверка raw body, HMAC SHA-256, короткое окно времени, уникальность event ID, быстрая фиксация события и фоновая обработка. С такой схемой повторная доставка становится безопасной, а асинхронные задания — наблюдаемыми и предсказуемыми.



