# Уведомления через вебхук

> Рецепт на Node.js — небольшой сервер принимает вебхуки Slicer, проверяет подпись и присылает в Telegram сообщение, когда ролик готов или опубликован.

Источник: https://docs.slicer.media/api/recipes/webhook-notify/

Монтаж идёт несколько минут, и ждать его в терминале не нужно. Этот рецепт поднимает маленький сервер, который получает [вебхуки](https://docs.slicer.media/api/webhooks/) Slicer и пишет в Telegram: «Ролик готов» со ссылкой на проект, «Опубликовано» со ссылкой на ролик в соцсети. Так же легко отправлять сообщения в Slack, почту или свою систему задач.

Что понадобится: Node.js 20+, сервер с публичным `https`\-адресом (или туннель для проверки), токен с областью `webhooks`, бот Telegram (его токен выдаёт [@BotFather](https://t.me/BotFather)) и id чата, куда писать.

## Сервер

notify.mjs:

```js
// npm install express
// SLICER_WEBHOOK_SECRET=whsec_… TELEGRAM_BOT_TOKEN=… TELEGRAM_CHAT_ID=… node notify.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const TOLERANCE_SEC = 5 * 60;

function verifySlicerSignature(rawBody, header, secret) {
  if (typeof header !== 'string') return false;
  const pairs = header.split(',').map((part) => part.trim().split('='));
  const timestamp = pairs.find(([key]) => key === 't')?.[1];
  const signatures = pairs.filter(([key]) => key === 'v1').map(([, value]) => value);
  if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SEC) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((value) => {
    const received = Buffer.from(value, 'hex');
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}

async function telegram(text) {
  const response = await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chat_id: process.env.TELEGRAM_CHAT_ID, text, disable_web_page_preview: true }),
  });
  if (!response.ok) console.error('Telegram ответил', response.status);
}

function message(event) {
  const item = event.data ?? {};
  switch (event.type) {
    case 'project.succeeded':
      return `🎬 Ролик «${item.title}» готов — ${Math.round(item.durationSec ?? 0)} с.\n${item.appUrl}`;
    case 'export.succeeded':
      return `📦 Экспорт «${item.title}» готов.\n${item.appUrl}`;
    case 'publication.published':
      return `🚀 Опубликовано в ${item.platform}: ${item.url}`;
    case 'ping':
      return '✅ Вебхук Slicer подключён';
    default:
      return null; // остальные события этому боту не нужны
  }
}

const app = express();
const seen = new Set(); // id событий: повторная доставка не пришлёт второе сообщение

app.post('/slicer/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySlicerSignature(req.body, req.get('Slicer-Signature'), process.env.SLICER_WEBHOOK_SECRET)) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(204); // Slicer ждёт ответа не дольше 10 секунд — отвечаем сразу

  if (seen.has(event.id)) return;
  seen.add(event.id);
  const text = message(event);
  if (text) telegram(text).catch((error) => console.error(error));
});

app.listen(Number(process.env.PORT ?? 3000), () => console.log('Жду вебхуки Slicer'));
```

## Подключение

### 1. Запустите сервер по https

Разместите `notify.mjs` на сервере с публичным `https`\-адресом. Для проверки на своём компьютере подойдёт туннель:

Терминал:

```bash
node notify.mjs &
cloudflared tunnel --url http://localhost:3000
```

Туннель напечатает адрес вида `https://….trycloudflare.com`.

### 2. Зарегистрируйте вебхук

Терминал:

```bash
curl https://api.slicer.media/v1/webhooks \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://notify.example.com/slicer/webhook","events":["project.succeeded","export.succeeded","publication.published"],"description":"Telegram"}' \
  | jq -r '.data.secret, .data.webhook.id'
```

Первая строка ответа — секрет `whsec_…`: передайте его серверу в `SLICER_WEBHOOK_SECRET` и перезапустите его. Вторая — id вебхука.

### 3. Проверьте

Терминал:

```bash
curl -X POST https://api.slicer.media/v1/webhooks/$WEBHOOK_ID/test -H "Authorization: Bearer $SLICER_TOKEN"
```

В чат придёт «✅ Вебхук Slicer подключён». Если нет — посмотрите журнал доставок: `GET /v1/webhooks/{id}/deliveries` покажет код ответа вашего сервера.

## Тонкости

-   **Сырое тело.** `express.raw` отдаёт тело как есть — подпись считается именно по нему. Если в приложении уже стоит `express.json()` глобально, подключите маршрут вебхука до него.
-   **Повторы.** Если сервер не ответил за 10 секунд или ответил ошибкой, Slicer повторит доставку позже. Множество `seen` не даёт отправить сообщение дважды; в продакшене храните id событий в Redis или базе.
-   **Безопасность.** Не логируйте секрет и не отвечайте подробностями на неверную подпись. Ссылка `appUrl` открывает проект только после входа в аккаунт Slicer, поэтому её можно отправлять в рабочий чат.

## Что дальше

[ВебхукиВсе события, подпись и расписание повторов.](https://docs.slicer.media/api/webhooks/)[Ролик из папкиЗапуск монтажа из скрипта.](https://docs.slicer.media/api/recipes/folder-to-reels/)
