Как принимать вебхуки
Укажите свой HTTPS-адрес, и Rewio будет присылать на него POST при появлении новых отзывов и ответов. Проверка подписи описана в §3, все методы есть в справочнике.
1. Создать вебхук
curl -X POST https://api.rewio.ru/v2/webhooks \
-H "X-API-Key: hrev_ВАШ_FULL_КЛЮЧ" -H "Content-Type: application/json" \
-d '{
"target": "https://your-server.com/hooks/reviews",
"event_types": ["reviews.new"],
"group_ids": [10, 20],
"bucket_window": "immediate",
"filters": {}
}'
# -> { "id": 456, "target": "...", "is_active": true, ... }targetобязан быть HTTPS; приватные / loopback / metadata-адреса отклоняются.group_idsопущен илиnull– все ваши объекты; иначе только перечисленные. Папки в вебхуках не поддерживаются: перечислите объекты явно.- Секрет в ответе не возвращается: он общий на аккаунт и берётся отдельно (см. §3).
- Проверить доставку в любой момент:
POST /v2/webhooks/{id}/test.
Фильтры (filters)
Объект filters сужает, какие отзывы вызывают доставку. Поля объединяются по И –
отзыв доставляется, только если проходит каждый заданный фильтр. Опустите поле (или
весь объект), чтобы фильтр не действовал.
{
"allowed_ratings": [1, 2, 3, 4, 5],
"allowed_sources": ["yandex_maps"],
"has_text": false,
"has_images": false
}allowed_ratings – корзины оценок; отзыв проходит, если его корзина в списке:
| корзина | что попадает |
|---|---|
5 | rating == 5.0 |
4 | 4.0 ≤ rating < 5.0 |
3 | 3.0 ≤ rating < 4.0 |
2 | 2.0 ≤ rating < 3.0 |
1 | 0.0 ≤ rating < 2.0 |
0 | без числовой оценки (текстовые площадки), только по явному запросу |
Поле опущено – это то же самое, что [1,2,3,4,5]: приходят все отзывы с оценкой,
без неоценённых. Чтобы получать и неоценённые, добавьте 0.
Примеры: [1,2] – только негатив, [5] – только идеальные, [0,1,2,3,4,5] – вообще всё.
allowed_sources– машинные имена площадок (какnameвGET /v2/sources). Опущено илиnull– все. Регистр не важен. Имя не сверяется с живым списком: опечатка ("yadnex_maps") молча ни с чем не совпадёт, ошибки не будет.has_text–trueдоставляет только отзывы с непустым текстом.has_images–trueтолько с прикреплёнными фото.
2. Что вы получаете
POST на ваш target с JSON-телом. Одна доставка несёт пачку отзывов, а не один отзыв.
Разбирайте доставку по полю event: оно же дублируется в заголовке X-Hrev-Event.
event | Когда срабатывает | Тело |
|---|---|---|
reviews.new | при сборе появились новые отзывы | отзывы, сгруппированные по объектам |
replies.new | у отзыва появился ответ организации или отзыв пришёл уже с ним | та же форма; читайте reply_text и reply_date |
Событие срабатывает и на ответы, опубликованные через нас: для клиента это подтверждение, что ответ дошёл до площадки.
| scrape.run.finished | сбор завершился. Приходит, даже если нового нет | только finished_at |
Подписывайтесь на любую комбинацию через event_types.
filters применяются к reviews.new и replies.new. Событие scrape.run.finished их
игнорирует: в нём нет отзывов. bucket_window действует на все три:
bucket_window | Доставка |
|---|---|
immediate | один POST вскоре после каждого сбора. По умолчанию |
daily_HOUR | один ответ раз в сутки в конкретный час; HOUR = 0–23, время в UTC. Напр. daily_9 = 09:00 UTC, daily_0 = полночь UTC |
Час дайджеста задаётся в UTC (как всё в системе), без ведущего нуля (daily_9, не daily_09).
Заголовки
| Заголовок | Смысл |
|---|---|
X-Hrev-Event | тип события |
X-Hrev-Delivery-Id | стабильный id доставки, для идемпотентности |
X-Hrev-Event-Id | id первого отзыва в пачке (0 для scrape.run.finished) |
X-Hrev-Timestamp | unix-секунды, участвуют в подписи |
X-Hrev-Signature | sha256=<hex> – одна или несколько через запятую (см. §3) |
Тело reviews.new
{
"schema_version": "1",
"event": "reviews.new",
"delivery_id": 456,
"occurred_at": "2026-07-19T12:00:00",
"groups": [
{
"group_id": 20,
"hotel_name": "Grand Hotel",
"review_count": 2,
"reviews": [
{
"id": 78910,
"external_review_id": "yandex_abc",
"source": "yandex",
"rating": 4.5,
"title": null,
"text": "Отличный отель ...",
"author": "Иван П.",
"review_date": "2026-07-19T00:00:00",
"language": "ru",
"url": "https://yandex.ru/maps/org/1/reviews",
"review_url": "https://yandex.ru/maps/org/1/reviews?reviews[publicId]=abc",
"has_images": false,
"reply_text": null,
"reply_date": null
}
]
}
],
"summary": {
"total_reviews": 2,
"by_source": {"yandex": 2},
"average_rating": 4.5,
"negative_count": 0
},
"truncated": false
}rating– по шкале 1–5, может быть дробным (4.5).reply_text/reply_dateнесут ответ организации, если он есть (иначеnull).truncated: true– payload превысил лимит размера, детализация по отзывам опущена (groups[].reviewsнет, ноsummaryиgroup_id/review_countостаются). Дотяните детали через REST при необходимости.
Тело replies.new
Форма та же, что у reviews.new. В groups[].reviews приходят отзывы, которые только что
получили ответ или пришли уже с ним, читайте reply_text и reply_date.
Изменённый ответ повторно не доставляется, только впервые появившийся.
Тело scrape.run.finished
Событие без отзывов: сообщает, что сбор завершился. Приходит раз за цикл каждому подписанному вебхуку, даже если нового не найдено.
{
"schema_version": "1",
"event": "scrape.run.finished",
"delivery_id": 789,
"occurred_at": "2026-07-19T13:00:00",
"finished_at": "2026-07-19T13:00:00"
}3. Проверка подписи (опционально)
Проверка подписи защищает от подделок: адрес target публичный, и без проверки кто угодно,
узнав его, сможет прислать вам фейковые «новые отзывы». Формально она необязательна: без неё
принимайте POST и отвечайте 2xx.
Секрет подписи, один на аккаунт
Все доставки со всех ваших вебхуков подписаны одним секретом аккаунта. Он виден всегда:
curl https://api.rewio.ru/v2/webhooks/secret \
-H "X-API-Key: hrev_ВАШ_FULL_КЛЮЧ"
# -> { "secret": "whsec_...", "rotated_at": null }Нужен ключ со скоупом full: секрет позволяет подделывать подписи, поэтому ключу на чтение
он не отдаётся. Секрет действует, пока вы не перевыпустите его сами (см. §4).
Как проверять
Подписываются точные отправляемые байты, с префиксом-таймстампом:
signed = f"{X-Hrev-Timestamp}." + <сырые байты тела запроса>
sig = HMAC_SHA256(secret, signed) # hexПроверяйте по сырому телу, до парсинга JSON: повторная сериализация меняет байты и ломает подпись.
import hashlib, hmac, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
ts = headers["X-Hrev-Timestamp"]
# отбрасываем устаревшие / переигранные доставки (±5 мин)
if abs(int(time.time()) - int(ts)) > 300:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
# при ротации заголовок может нести несколько подписей через запятую
for part in headers["X-Hrev-Signature"].split(","):
part = part.strip()
if part.startswith("sha256=") and hmac.compare_digest(part[7:], expected):
return True
return Falseconst crypto = require("crypto");
function verify(headers, rawBody, secret) {
const ts = headers["x-hrev-timestamp"];
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(Buffer.concat([Buffer.from(ts + "."), rawBody])).digest("hex");
return headers["x-hrev-signature"].split(",").some((p) => {
p = p.trim();
if (!p.startsWith("sha256=")) return false;
const got = Buffer.from(p.slice(7));
const exp = Buffer.from(expected);
// длины обязательно сверить до timingSafeEqual: на разной длине он бросает
// RangeError, и приёмник упадёт 500-й вместо честного «подпись неверна»
return got.length === exp.length && crypto.timingSafeEqual(got, exp);
});
}4. Ротация секрета (без простоя)
POST /v2/webhooks/rotate-secret перевыпускает секрет аккаунта и возвращает новый.
Следующие 24 часа каждая доставка подписывается и новым, и предыдущим секретом; через
запятую в X-Hrev-Signature. Проверка выше принимает любую из перечисленных подписей,
поэтому обновить приёмник можно в любой момент этого окна. После 24 часов остаётся только новый.
curl -X POST https://api.rewio.ru/v2/webhooks/rotate-secret \
-H "X-API-Key: hrev_ВАШ_FULL_КЛЮЧ"
# -> { "secret": "whsec_...", "rotated_at": "2026-07-22T12:00:00" }5. Идемпотентность
Доставки приходят как минимум один раз: сбой между отправкой и учётом может переслать ту же доставку повторно. От этого защищают две вещи:
-
Та же доставка всегда переиспользует
X-Hrev-Delivery-Id– дедупьте по нему:if seen(delivery_id): # напр. Redis SET NX, TTL 24ч return 200 # уже обработано process(payload) mark_seen(delivery_id) -
Отзывы дедуплицируются на нашей стороне: в одной доставке отзыв не встретится дважды.
Отвечайте 2xx быстро, в пределах таймаута доставки. Тяжёлую работу выносите в фон.
Что вернуть в ответе
Подтверждение получения – это сам HTTP-статус: любой 2xx означает «принято, больше не
пересылать». Тело ответа не читается и не разбирается.
Возвращайте короткий JSON вместо пустого ответа: явный Content-Type не даст фреймворку
или прокси подсунуть HTML-страницу. Минимальный вариант:
HTTP/1.1 200 OK
Content-Type: application/json
{"ok": true}Достаточно и {}: важен только код 2xx. Любой не-2xx или таймаут считается недоставкой,
и включаются повторы из §6.
6. Ретраи и обработка ошибок
| Ваш ответ | Результат |
|---|---|
2xx | успех |
408, 429, 5xx, таймаут, ошибка соединения | ретрай с backoff |
3xx и прочие 4xx | постоянная ошибка, без ретрая |
Лестница backoff: 30с → 1м → 2м → 5м → 10м → 15м, дальше каждые 30м. Всего
до 30 попыток за ~12 часов, после чего доставка уходит в dead_letter.
Подписка не отключается сама. Сколько бы доставок ни сорвалось, канал
продолжает работать: почините приёмник – события пойдут снова. На каждый 50-й
провал подряд мы пишем владельцу аккаунта. Выключить и включить подписку
вручную можно через PATCH /v2/webhooks/{id}.
Чем больше попыток, тем выше шанс задвоенной доставки, поэтому дедупликация по
X-Hrev-Delivery-Id (§5) обязательна.
Если доставка так и не дошла, напишите нам – историю попыток по вашему вебхуку (статус, коды и тела ответов, времена) мы поднимем со своей стороны.