Гайды

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 — корзины оценок; отзыв проходит, если его корзина в списке:

корзиначто попадает
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 — имена площадок (как в каждом отзыве и в GET /v2/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

Подписывайтесь на любую комбинацию через event_types. filters применяются к reviews.new и replies.new; scrape.run.finished их игнорирует (в нём нет отзывов). bucket_window применяется ко всем трём:

bucket_windowДоставка
immediateодин POST вскоре после каждого цикла обхода (по умолчанию)
daily_<hour>одна пачка в день в <hour>:00 UTC, час 023 — напр. 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)

Тело (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 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();
    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 (статус, попытки, код/тело ответа, времена).

On this page