Руководства

Как принимать вебхуки

Доступ для AI:

Укажите свой HTTPS-адрес, и Rewio будет присылать на него POST при появлении новых отзывов и ответов. Это замена опросу: узнать о новом отзыве в момент, когда он появился, а не когда вы в очередной раз спросили.

На этом строят уведомления менеджеру о негативе, постановку задач на ответ, обновление карточки филиала в CRM и пересчёт витрины на сайте. Если же вам нужна не реакция на событие, а полная копия отзывов у себя, это синхронизация, а не вебхуки.

Проверка подписи описана в §3, все методы есть в справочнике.

1. Создать вебхук

curl -X POST https://api.rewio.ru/v3/webhooks \
  -H "X-API-Key: hrev_ВАШ_FULL_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{
        "target": "https://your-server.com/hooks/reviews",
        "event_types": ["reviews.new"],
        "object_ids": [10, 20],
        "bucket_window": "immediate",
        "filters": {}
      }'
# -> { "id": 456, "target": "...", "is_active": true, ... }
  • target обязан быть HTTPS; приватные / loopback / metadata-адреса отклоняются.
  • object_ids опущен или null – все ваши объекты; иначе только перечисленные. Папки в вебхуках не поддерживаются: перечислите объекты явно.
  • Секрет в ответе не возвращается: он общий на аккаунт и берётся отдельно (см. §3).
  • Проверить доставку в любой момент: POST /v3/webhooks/{id}/test.

Фильтры (filters)

Объект filters сужает, какие отзывы вызывают доставку. Поля объединяются по И – отзыв доставляется, только если проходит каждый заданный фильтр. Опустите поле (или весь объект), чтобы фильтр не действовал.

{
  "allowed_ratings": [1, 2, 3, 4, 5],
  "allowed_sources": ["yandex_maps"],
  "has_text": false,
  "has_images": false
}

allowed_ratings – корзины оценок; отзыв проходит, если его корзина в списке:

корзиначто попадает
5rating == 5.0
44.0 ≤ rating < 5.0
33.0 ≤ rating < 4.0
22.0 ≤ rating < 3.0
10.0 ≤ rating < 2.0
0без числовой оценки (текстовые площадки), только по явному запросу

Поле опущено – это то же самое, что [1,2,3,4,5]: приходят все отзывы с оценкой, без неоценённых. Чтобы получать и неоценённые, добавьте 0.

Примеры: [1,2] – только негатив, [5] – только идеальные, [0,1,2,3,4,5] – вообще всё.

  • allowed_sources – машинные имена площадок (поле slug в GET /v3/sources). Опущено или null – все. Регистр не важен. Имя не сверяется с живым списком: опечатка ("yadnex_maps") молча ни с чем не совпадёт, ошибки не будет.
  • has_texttrue доставляет только отзывы с непустым текстом.
  • has_imagestrue только с прикреплёнными фото.

2. Что вы получаете

POST на ваш target с JSON-телом. Одна доставка несёт пачку отзывов, а не один отзыв.

Разбирайте доставку по полю event: оно же дублируется в заголовке X-Hrev-Event.

eventКогда срабатываетТело
reviews.newпри сборе появились новые отзывыотзывы, сгруппированные по объектам
replies.newу отзыва появился ответ организации или отзыв пришёл уже с нимта же форма; читайте reply_text и reply_date
scrape.run.finishedсбор завершился. Приходит, даже если нового неттолько finished_at

replies.new срабатывает и на ответы, опубликованные через нас: для вас это подтверждение, что ответ дошёл до площадки.

Подписывайтесь на любую комбинацию через 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-Idid первого отзыва в пачке (0 для scrape.run.finished)
X-Hrev-Timestampunix-секунды, участвуют в подписи
X-Hrev-Signaturesha256=<hex> – одна или несколько через запятую (см. §3)

Тело reviews.new

{
  "schema_version": "3",
  "event": "reviews.new",
  "delivery_id": 456,
  "occurred_at": "2026-07-19T12:00:00",
  "objects": [
    {
      "object_id": 20,
      "name": "Grand Hotel",
      "review_count": 2,
      "reviews": [
        {
          "id": 78910,
          "external_review_id": "yandex_maps_abc",
          "source_id": 1,
          "source_slug": "yandex_maps",
          "rating": 4.5,
          "text": "Отличный отель ...",
          "author": "Иван П.",
          "review_date": "2026-07-19T00:00:00",
          "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": {
    "review_count": 2,
    "by_source": {"yandex_maps": 2},
    "average_rating": 4.5,
    "negative_rating_count": 0
  },
  "truncated": false
}
  • rating – по шкале 1–5, может быть дробным (4.5).
  • Площадка приходит парой source_id + source_slug – теми же, что в GET /v3/sources.
  • reply_text / reply_date несут ответ организации, если он есть (иначе null).
  • truncated: true – payload превысил лимит размера, детализация по отзывам опущена (objects[].reviews нет, но summary и object_id/review_count остаются). Дотяните детали через REST при необходимости.

Форму тела выбирает подписка, а не запрос. Вебхук, созданный через POST /v3/webhooks, получает schema_version: "3" – он и описан здесь. Подписки, созданные раньше, через /v2, продолжают получать прежнее тело (schema_version: "2", groups[] вместо objects[], площадка одной строкой source) – ровно то, что получали до этого.

Тело replies.new

Форма та же, что у reviews.new. В objects[].reviews приходят отзывы, которые только что получили ответ или пришли уже с ним, читайте reply_text и reply_date.

Изменённый ответ повторно не доставляется, только впервые появившийся.

Тело scrape.run.finished

Событие без отзывов: сообщает, что сбор завершился. Приходит раз за цикл каждому подписанному вебхуку, даже если нового не найдено.

{
  "schema_version": "3",
  "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/v3/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 False
const 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 /v3/webhooks/rotate-secret перевыпускает секрет аккаунта и возвращает новый.

Следующие 24 часа каждая доставка подписывается и новым, и предыдущим секретом; через запятую в X-Hrev-Signature. Проверка выше принимает любую из перечисленных подписей, поэтому обновить приёмник можно в любой момент этого окна. После 24 часов остаётся только новый.

curl -X POST https://api.rewio.ru/v3/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 /v3/webhooks/{id}.

Чем больше попыток, тем выше шанс задвоенной доставки, поэтому дедупликация по X-Hrev-Delivery-Id (§5) обязательна.

Если доставка так и не дошла, напишите нам – историю попыток по вашему вебхуку (статус, коды и тела ответов, времена) мы поднимем со своей стороны.

On this page