Webhooks
Пуш новых отзывов и ответов на ваш сервер с проверкой подписи.
Вместо опроса API вы можете получать пуш при появлении новых отзывов. Вебхук — это ресурс: один вызов создаёт его, и секрет возвращается ровно один раз в этом ответе. CRUD-эндпоинты вебхуков — в справочнике.
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, "secret": "s3cr3t...", ... } сохраните секрет надёжноtargetобязан быть HTTPS; приватные/loopback/metadata-адреса отклоняются.group_idsопущен /null— все ваши группы; иначе только указанные.- Секрет показывается только в этом ответе и при ротации. Сохраните сразу.
- Тестовая доставка в любой момент:
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— имена площадок (как в каждом отзыве и в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>:00 UTC, час 0–23 — напр. 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) |
Тело (event: "reviews.new")
{
"schema_version": "1",
"event": "reviews.new",
"delivery_id": 456,
"occurred_at": "2026-07-19T12:00:00",
"groups": [
{
"group_id": 10,
"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 при необходимости.
Тело (event: "replies.new")
Форма идентична reviews.new. groups[].reviews перечисляет отзывы, которые
только что получили ответ (или пришли уже с ним) — читайте reply_text/reply_date.
Просто изменившийся ответ (отредактированный) не репортится — только впервые
появившийся.
Тело (event: "scrape.run.finished")
Пульс без данных отзывов — сообщает, что цикл завершился, чтобы вы пометили данные
«свежими на finished_at» и подтянули их через REST. Приходит раз за цикл каждому
подписанному вебхуку, даже если нового не найдено.
{
"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. Проверка подписи (обязательно)
Мы подписываем точные отправляемые байты, с префиксом-таймстампом:
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();
return p.startsWith("sha256=") &&
crypto.timingSafeEqual(Buffer.from(p.slice(7)), Buffer.from(expected));
});
}4. Ротация секрета (без простоя)
POST /v2/webhooks/{id}/rotate-secret возвращает новый секрет. Следующие 24 часа
мы подписываем каждую доставку и новым, и предыдущим секретом (через запятую в
X-Hrev-Signature). Т. к. проверка выше принимает любую из перечисленных подписей,
вы можете выкатить новый секрет в любой момент этого окна без потерь. После 24 часов
используется только новый.
5. Идемпотентность
Доставки — at-least-once. Падение воркера между отправкой и нашим учётом может переслать ту же доставку. Защищают две вещи:
-
Та же доставка всегда переиспользует
X-Hrev-Delivery-Id. Дедуп по нему:if seen(delivery_id): # напр. redis SET NX, TTL 24ч return 200 # уже обработано process(payload) mark_seen(delivery_id) -
Контент отзывов дедуплицируется на нашей стороне — в одной доставке отзыв не встретится дважды.
Отвечайте 2xx быстро (быстрее таймаута доставки). Тяжёлую работу — асинхронно.
6. Ретраи и обработка ошибок
| Ваш ответ | Результат |
|---|---|
2xx | успех |
408, 429, 5xx, таймаут, ошибка соединения | ретрай с backoff |
3xx и прочие 4xx | постоянная ошибка, без ретрая |
Лестница backoff: 30с → 1м → 5м → 15м → 30м → 1ч → 2ч → 4ч, до 9 попыток, затем
доставка уходит в dead_letter. После 50 накопленных сбоев вебхук
автоотключается и владельцу приходит алерт — почините эндпоинт и включите заново.
История доставок: GET /v2/webhooks/{id}/deliveries (статус, попытки, код/тело
ответа, времена).