К содержимому
Type — API сайта
Разделы

Вебхуки и проверка подписи

Вебхук — вызов от CRM к сайту, когда изменились объекты, цены или занятость. По нему сайт сбрасывает кэш, и ему не нужно часто опрашивать API.

Настройка

Настройки → Аренда → Сайт:

  1. Укажите адрес вебхука на сайте, например https://site.example.com/api/type-webhook. Принимается только https:// с публичным адресом.
  2. Нажмите «Новый секрет» — CRM покажет секрет подписи один раз. Сохраните его в переменной окружения TYPE_WEBHOOK_SECRET на сервере сайта.
  3. Нажмите «Проверить» — CRM отправит тестовый вызов rental.site_ping.

Запрос

CRM отправляет POST на адрес вебхука с телом JSON:

json
{"event":"rental.site_changed","changes":["occupancy","prices"],"unit_ids":["11111111-2222-4333-8444-555555555555"],"at":"2026-12-23T10:00:00Z"}
ПолеЧто значит
eventrental.site_changed — данные изменились; rental.site_ping — проверка из CRM
changesчто сбросить: units (тексты, фото, удобства, архив), prices (цены, особые даты, скидки), occupancy (брони и блокировки)
unit_idsзатронутые объекты; null — все
atкогда произошло изменение
  • Изменения за 10 секунд CRM собирает в один вызов.
  • Ответьте любым 2xx. Если ответ другой или сайт не ответил, CRM повторит вызов через 1, 5 и 30 минут.
  • На rental.site_ping просто ответьте 2xx, кэш не трогайте.

Подпись

Заголовки запроса:

ЗаголовокЗначение
X-Type-Timestampвремя отправки, секунды Unix
X-Type-Signaturehex HMAC-SHA256 по секрету вебхука от строки "{timestamp}.{body}"

body — сырые байты тела запроса, а не разобранный и снова собранный JSON: любой лишний пробел меняет подпись.

Как проверять:

  1. Прочитайте сырое тело.
  2. Отклоните вызов, если X-Type-Timestamp отличается от текущего времени больше чем на 300 секунд, — так повтор перехваченного запроса не сработает.
  3. Посчитайте подпись и сравните с X-Type-Signature за постоянное время (timingSafeEqual, hash_equals). Не совпала — ответьте 401 и ничего не сбрасывайте.

Подпись вебхука — защита сайта от поддельных вызовов. Это не ключ доступа к API: ключ API в вебхуке не передаётся.

С пакетом

ts
// app/api/type-webhook/route.ts
import { revalidateTag } from 'next/cache';
import { verifyWebhook } from '@type/site-client';

export async function POST(req: Request) {
  const body = await req.text(); // сырое тело
  const res = await verifyWebhook(body, req.headers, process.env.TYPE_WEBHOOK_SECRET!);
  if (!res.ok) return Response.json({ error: res.reason }, { status: 401 });
  if (res.payload.event === 'rental.site_changed') {
    for (const change of res.payload.changes) revalidateTag(`rental:${change}`); // units | prices | occupancy
  }
  return Response.json({ ok: true });
}

verifyWebhook возвращает { ok: true, payload } или { ok: false, reason }, где reason — missing (нет заголовков), timestamp (вне окна 300 с), signature или payload (тело не JSON вебхука).

Если кэшируете по объектам, ставьте теги вида rental:occupancy:<unit_id> и сбрасывайте только затронутые; при unit_ids: null — все.

Без пакета (Node.js)

ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(body: string, ts: string | null, sig: string | null, secret: string): boolean {
  if (!ts || !sig || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${ts}.${body}`).digest();
  const got = Buffer.from(sig.toLowerCase(), 'hex');
  return got.length === expected.length && timingSafeEqual(got, expected);
}

Без пакета (PHP)

php
function type_webhook_ok(string $body, string $ts, string $sig, string $secret): bool {
    if ($ts === '' || abs(time() - (int) $ts) > 300) {
        return false;
    }
    $expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
    return hash_equals($expected, strtolower($sig));
}

Пример для проверки

Все значения выдуманы — используйте их в тесте своей проверки (без окна 300 с):

  • секрет: whsec_example_0123456789abcdef0123456789abcdef;
  • X-Type-Timestamp: 1798000000;
  • тело — JSON из раздела «Запрос» выше, байт в байт.

Ожидаемая подпись X-Type-Signature:

text
99f5257e8baf058067be3cbc81276a70060c67ada36dec102cfcbe116bb71ece