Перейти до вмісту

← Документація

Вебхуки

Обмін подіями з іншими системами в обидва боки: підпис і повтори вихідних, ключ і формат тіла для вхідних.

Що це

Вебхук — це спосіб для іншої системи дізнаватись про те, що сталось у Systemer, не питаючи нас про це щохвилини. Ви вказуєте адресу й перелік подій; щойно одна з них настає, ми надсилаємо на цю адресу підписаний POST із JSON.

Звідси інтегрується будь-що, що вміє приймати HTTP: сценарій у n8n чи Make, чужа CRM, бот у чаті команди, власний сервіс. Підписка налаштовується в Налаштування → Вебхуки й потребує права керувати інтеграціями.

Підписка на «усі події» не підтримується навмисно. Перелік завжди явний: поява нової події в продукті не має сама почати відправляти дані в систему, власник якої про цю подію не знає.

Як підключити

  • Налаштування → Вебхуки → «Нова адреса». Адреса приймача має бути на https.
  • Оберіть події. Надсилатимуться лише вони.
  • Збережіть секрет підпису. Він показується один раз: у нас він зберігається в сховищі секретів і не читається назад.
  • Натисніть «Надіслати тест» — у журналі доставок зʼявиться подія webhook.ping із відповіддю вашого сервера.

Якщо секрет загубився, створіть новий кнопкою «Новий секрет»: попередній лишається чинним ще добу, тому оновлення приймача не призводить до розриву.

Що приходить

Заголовки запиту несуть ідентифікатор події, її тип і підпис. Ідентифікатор належить події, а не спробі доставки: повтор тієї самої події приходить із тим самим значенням, і саме за ним приймач має відкидати дублі.

Заголовки

POST /hooks/systemer HTTP/1.1
Content-Type: application/json
Systemer-Event-Id: 5f0c6f1e-...
Systemer-Event-Type: deal.won
Systemer-Signature: t=1790000000,v1=5257a869e7bcd...

Тіло

{
  "id": "5f0c6f1e-...",
  "type": "deal.won",
  "created_at": "2026-10-01T10:00:00Z",
  "organization_id": "0a9d...",
  "data": {
    "entity_type": "deal",
    "entity_id": "7c31...",
    "title": "Угоду виграно",
    "summary": null,
    "url": "https://www.systemer.app/ваша-організація/crm/deals/7c31...",
    "actor_person_id": "3b12..."
  }
}

Поля перелічені поіменно, а не віддаються «як є» з нашої бази. Внутрішні службові дані — кому саме пішло сповіщення всередині вашої команди, які права для цього потрібні — назовні не виходять ніколи.

Як перевірити підпис

Підписується рядок «мітка часу, крапка, сире тіло запиту» алгоритмом HMAC-SHA256 із вашим секретом. Мітка часу входить у підпис, а не стоїть поруч: інакше перехоплений запит можна було б переграти пізніше, підмінивши лише її.

Node.js

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

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);

  // Запит, старший за пʼять хвилин, відхиляємо — захист від переграння.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return header
    .split(',')
    .filter((p) => p.startsWith('v1='))
    .some((p) => timingSafeEqual(Buffer.from(p.slice(3)), Buffer.from(expected)));
}
  • Перевіряйте підпис ДО того, як розбирати тіло: воно ще не ваше, поки підпис не зійшовся.
  • Підписів у заголовку може бути два — це доба після зміни секрета. Приймач має вважати запит дійсним, якщо збігся будь-який.
  • Відкидайте запити з міткою часу, старшою за пʼять хвилин.
  • Будьте ідемпотентними за Systemer-Event-Id: повтор доставки — штатна ситуація, а не помилка.

Доставка й повтори

Ми чекаємо на відповідь 2xx протягом пʼяти секунд. Довга робота на боці приймача має відбуватись після відповіді, а не до неї — інакше доставка вважається невдалою, хоча подію вже прийнято.

  • Мережева помилка, таймаут, 408, 429 і 5xx — повторюємо: через 10 секунд, 30 секунд, 2 хвилини, 10 хвилин, годину й шість годин.
  • Інші 4xx — не повторюємо. Це свідома відмова приймача, і шість однакових запитів нічого не змінять, лише сховають причину.
  • Після сьомої невдачі подія вважається недоставленою. Її видно в журналі, і її можна відправити знову кнопкою «Повторити».
  • Якщо поспіль не доходить сто подій, підписка вимикається, а власник отримує сповіщення. Це не мовчазне зникнення інтеграції — вимкнення видно на екрані з поясненням.

Події не надсилаються миттєво: черга розбирається кожні пʼять хвилин. Це стеля затримки, а не типове значення.

Журнал доставок показує не лише код відповіді, а й її початок. Питання «чому не доходить» майже завжди має відповідь у тілі відповіді вашого сервера, а не в коді статусу.

Вхідні: коли надсилають нам

Зворотний напрямок: зовнішня система надсилає POST нам, і в Systemer зʼявляється лід, задача або операція. Джерело створюється в Налаштування → Вебхуки → Вхідні; воно має свою адресу, свій ключ і рівно один намір.

Операція ззовні лягає ЧЕРНЕТКОЮ і не впливає на жоден звіт, доки людина її не підтвердить. Це не обережність заради обережності: зовнішня система надсилає намір, а не факт. Баг чи дубль на її боці інакше миттєво стає рядком у звіті, а звіт — підставою для рішення про гроші.

  • Один намір на джерело. Ключ, що вміє створювати і лід, і операцію, потрапивши не в ті руки, коштує більше за два ключі.
  • Контекст, якого запит не несе — рахунок для операції, проєкт для задачі, відповідальний за лід, — задається в налаштуваннях джерела один раз.
  • Ключ показується один раз при створенні. Новий ключ можна створити будь-коли; старий перестає діяти тієї ж секунди.

Вхідні: як надіслати запит

Ключ передається заголовком Systemer-Key або як Bearer-токен в Authorization — обидва приймаються. Тіло — JSON.

Новий лід

POST /api/webhooks/inbound/<ідентифікатор джерела>
Systemer-Key: whin_...
Systemer-Idempotency-Key: form-2026-10-01-17f3
Content-Type: application/json

{
  "name": "Олена Ковальчук",
  "company": "ТОВ «Приклад»",
  "email": "olena@example.com",
  "phone": "+380671234567",
  "message": "Потрібен редизайн сайту"
}

Чернетка операції

{
  "kind": "expense",
  "amount": "2400.50",
  "currency": "UAH",
  "date": "2026-10-01",
  "description": "Хостинг за жовтень",
  "counterparty": "Hetzner"
}
  • Валюта операції має збігатися з валютою рахунку, обраного в джерелі, — інакше запит відхиляється з поясненням.
  • Переказ ззовні не приймається: це рух між вашими власними рахунками, і зовнішня система про них не знає.
  • Сума приймається і рядком, і числом, але рядок точніший: число в JSON — це double, і 2400.50 в ньому вже не 2400.50.

Вхідні: що ми відповідаємо

  • 201 — запис створено. У тілі відповіді його тип та ідентифікатор.
  • 401 — джерела немає або ключ не підходить. Відповідь однакова навмисно: інакше адреса стає способом дізнатись, які джерела існують.
  • 409 — джерело на паузі.
  • 422 — тіло або налаштування джерела не дають створити запис. Причина — у полі error, і вона написана для людини.
  • 500 — наш збій. Саме цей запит має сенс повторити.

Надішліть заголовок Systemer-Idempotency-Key — і повтор із тим самим значенням поверне той самий результат замість другого запису. Без нього ключем стає хеш тіла: повтором вважається рівно однакове тіло, що слабше, але краще, ніж нічого.

Кожен запит видно в журналі джерела: що створилось або чому відхилено. Тіло запиту зберігається 30 днів — далі лишається сам запис без вмісту.

Які події доступні

  • CRM: ліду й угоді призначено відповідального, лід конвертовано, угода перейшла на новий етап, угоду виграно, угоду втрачено.
  • Проєкти: проєкту призначено відповідального, проєкт наближається до бюджету, проєкт перевищив бюджет, задачі призначено виконавця.
  • Фінанси: рахунок прострочено, прогнозується касовий розрив, маржа помітно впала.

Внутрішні події — листування в чатах, робота над самою Systemer, підказки AI — назовні не виходять. Перелік розширюється, і кожна нова подія зʼявляється в переліку вимкненою: щоб вона почала надходити, її треба обрати.

Дізнайтесь свою справжню
маржу вже сьогодні.

Один вечір на налаштування — і замість зведення таблиць ви бачите маржу кожного проєкту з накладними й прогноз касового розриву. Без картки, тариф Free назавжди.