AACSearch / руководство
Вебхуки
Получайте подписанные события AACSearch на своём эндпоинте — подписка, проверка HMAC-подписи, повторные попытки и диагностика неудачных доставок.
Вебхук — это HTTPS-эндпоинт, который вы регистрируете в AACSearch. Когда в
организации что-то происходит — завершается эксперимент, меняется конфигурация
хостинга, создаётся обращение в поддержку — AACSearch отправляет на этот
эндпоинт подписанный POST с JSON-телом события.
Вебхуки работают по принципу push, а не polling. Они не содержат API-ключ: получатель подтверждает авторство запроса проверкой HMAC-подписи.
Вебхуки и вебхук коннектора — это разное
У коннектора есть собственный входящий вебхук
(POST /connectors/{connectorId}/webhook), который загружает данные каталога
в AACSearch. Вебхуки на этой странице работают в обратную сторону: AACSearch
уведомляет вашу систему о событиях в вашей организации.
Прежде чем начать
- Организация, в которой вы owner или admin. Список может читать любой участник, а создание, редактирование, ротация секрета и удаление требуют одной из этих двух ролей.
- Публичный HTTPS-эндпоинт, принимающий
POSTи быстро отвечающий2xx. - Возможность прочитать тело запроса до разбора JSON — для проверки подписи нужны точные байты.
Что вы получите
Эндпоинт, который проверяет каждую доставку и подтверждает её, а также представление неудачных доставок для алертинга.
Шаги
1. Создайте эндпоинт
Откройте Вебхуки в консоли (/dashboard/webhooks) и нажмите
Новый вебхук. Укажите:
- имя (до 120 символов),
- URL эндпоинта (до 2048 символов, HTTP или HTTPS),
- события — отдельные события или Все события.
Скопируйте секрет подписи сейчас
Секрет whsec_… показывается один раз — в карточке «Вебхук создан».
Если вы его потеряли, выполните ротацию на странице эндпоинта.
Ожидаемый результат: эндпоинт появился в списке, включён, статус — «Доставок пока нет».
Проверка: нажмите Отправить тест. Будет создана реальная доставка
события webhook.test. Эндпоинт должен получить её в течение нескольких
секунд, а статус строки после Обновить — стать успешным.
«Все события» — это живая подписка
Выбор Все события записывает шаблон *, поэтому эндпоинт получает и те
события, которые появятся в платформе позже. Три события —
experiment.winner_detected, quality.regression и event.replayed — не
имеют группы в списке выбора, поэтому их можно подписать только через
Все события.
2. Прочитайте доставку
Каждый запрос — это JSON-POST со следующими заголовками:
| Заголовок | Значение |
|---|---|
X-AACSearch-Event | Имя события, например experiment.promoted |
X-AACSearch-Delivery | Идентификатор доставки (dlv_…), уникальный для события |
X-AACSearch-Signature | t=<unix секунды>,v1=<hex HMAC-SHA256> |
Тело — это конверт, ваши данные лежат в data:
{
"id": "dlv_01J…",
"event": "experiment.promoted",
"created_at": "2026-09-16T10:32:11.482Z",
"organization_id": "org_01J…",
"data": { "experiment_id": "8c1f…", "variant_id": "popular" }
}3. Проверьте подпись
v1 — это hex HMAC-SHA256 от `${t}.${rawBody}` с ключом whsec_….
Считайте его по сырому телу и сравните за постоянное время.
import { createHmac, timingSafeEqual } from "node:crypto";
/** true только для свежей, неизменённой и корректно подписанной доставки. */
export function verifyWebhook(
secret: string,
rawBody: string,
signatureHeader: string,
toleranceSeconds = 300,
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=") as [string, string]),
);
const timestamp = Number(parts.t);
const provided = parts.v1;
if (!Number.isFinite(timestamp) || !provided) return false;
// Отклоняем повтор старых доставок.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(provided, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Две ошибки, ломающие проверку
Разбор до того, как проверено. Читайте сырые байты и проверяйте подпись по
ним. JSON-парсер, который сериализует тело заново, меняет его — и подпись
никогда не совпадёт. Сравнение через ===. Используйте сравнение за
постоянное время, чтобы проверку нельзя было подобрать по таймингу.
Проверка: отправьте тестовую доставку и сравните вычисленное значение с
заголовком — они должны совпасть. Затем измените один символ секрета и
убедитесь, что проверка возвращает false.
4. Подтверждайте быстро, обрабатывайте потом
Верните 2xx сразу после проверки подписи, а работу выполняйте асинхронно.
Медленный получатель обрабатывается так же, как сбойный.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const rawBody = await request.text();
const signature = request.headers.get("X-AACSearch-Signature") ?? "";
if (!verifyWebhook(env.WEBHOOK_SECRET, rawBody, signature)) {
return new Response("invalid signature", { status: 401 });
}
const delivery = JSON.parse(rawBody);
const id = request.headers.get("X-AACSearch-Delivery") ?? delivery.id;
// Дедупликация: доставка at-least-once (см. «Повторные попытки»).
if (await env.SEEN.get(id)) return new Response("ok", { status: 200 });
await env.SEEN.put(id, "1", { expirationTtl: 86_400 * 3 });
await env.QUEUE.send({ event: delivery.event, data: delivery.data });
return new Response("ok", { status: 200 });
},
};Проверка: в строке доставки в консоли HTTP-код 2xx, число попыток — 1.
Повторные попытки и идемпотентность
Доставка гарантируется at-least-once. Неудачные попытки повторяются с экспоненциальной задержкой и полным джиттером:
| Свойство | Значение |
|---|---|
| Всего попыток | 6 (первая плюс 5 повторов) |
| Базовая задержка | 30 секунд |
| Потолок | 15 минут |
| Джиттер | Полный — каждая пауза равномерно выбирается из [0, min(30с × 2^(n-1), 15мин)] |
| Последняя попытка | Не позже примерно 30 минут после первой |
В консоли написано «повторяется дважды» — платформа повторяет пять раз
На экране доставки указано, что неудачные попытки повторяются дважды. На самом деле очередь доставки даёт шесть попыток суммарно. Рассчитывайте окно дедупликации по большему числу.
Повтор происходит только тогда, когда сбой может пройти сам:
| Ваш ответ | Повтор? |
|---|---|
2xx | Нет — доставлено |
408, 429 | Да |
5xx | Да |
Любые другие 4xx и 3xx | Нет — доставка помечается неудачной |
Поскольку повтор может последовать за попыткой, ответ на которую вы не увидели,
обработчик обязан быть идемпотентным. Ключуйтесь по X-AACSearch-Delivery
(как в шаге 4) и делайте побочный эффект безопасным при повторе.
Проверка: направьте эндпоинт на URL, возвращающий 500. В консоли доставка
будет повторяться с растущей задержкой и оставаться в статусе pending, пока
шестая попытка не пометит её как failed.
Доступные события
| Группа | События |
|---|---|
| Поддержка | support.thread.created, support.message.created |
| Ассистент | conversation.completed |
| Эксперименты | experiment.started, experiment.stopped, experiment.promoted, experiment.winner_detected* |
| Хостинг | hosting.enabled, hosting.updated, hosting.disabled |
| Медиа | media.uploaded |
| API-ключи | api_key.created, api_key.revoked |
| Качество | quality.regression* |
| События | event.replayed* |
| Алерты | alert.fired |
* Нет группы в списке выбора консоли — подписывайтесь через Все события.
Список events вебхука может также быть шаблоном *, который покрывает все
события, включая добавленные позже. webhook.test отправляется только кнопкой
Отправить тест и не является подписываемым событием.
Просмотр доставок
Страница эндпоинта показывает 100 последних доставок с событием, статусом, HTTP-кодом, числом попыток и временем. Строка открывает полную запись: статус, HTTP-код, попытки, время создания и завершения, текст ошибки и точное JSON-тело запроса с возможностью копирования.
Это помогает различить случаи:
| Симптом | Вероятная причина | Решение |
|---|---|---|
401 / non-2xx на каждой доставке | Подпись проверяется по повторно сериализованному телу | Проверяйте по сырому телу (шаг 3) |
| Подпись никогда не совпадает | Неверный секрет после ротации | Скопируйте актуальный секрет; ротация действует сразу |
| Обработка выполнена дважды | Нет дедупликации между повторами | Ключуйтесь по X-AACSearch-Delivery |
Доставка остаётся pending | Эндпоинт медленный (>30 с) или отвечает 5xx | Сначала подтвердите, обрабатывайте асинхронно |
| Ничего не приходит, список пуст | Ни одно событие не попало в подписку | Проверьте список событий или включите Все события |
| Эндпоинт отключён | Переключатель выключен | Включите его на странице эндпоинта или в списке |
Далее
- Ошибки и повторные попытки — контракт ошибок API и идемпотентность.
- A/B-эксперименты — события
experiment.*в контексте. - Организация и биллинг — роли, управляющие вебхуками.