К содержанию
BSTCRMСправка

Вебхуки — CRM сама сообщит о событии

Создана сделка, сменился статус, пришла оплата, добавлен клиент — CRM отправит уведомление на ваш сервер.

Вебхук — это уведомление, которое CRM сама отправляет на адрес вашего сервера, когда что-то произошло. Не нужно каждую минуту спрашивать «есть ли новые сделки?» — CRM пришлёт их сама за несколько секунд.

События

  • deal.created — создана сделка.
  • deal.status_changed — сменился статус сделки (в уведомлении есть и прежний статус).
  • deal.paid — по сделке пришла оплата (в уведомлении — сумма оплаты).
  • client.created — добавлен клиент.

Подключить вебхук

  1. Откройте Настройки → API и вебхуки, блок «Вебхуки».
  2. Вставьте адрес вашего сервера. Подходит только https:// и только адрес, доступный из интернета.
  3. Отметьте события, которые нужны, и нажмите Добавить адрес.
    Адрес, события и кнопка «Добавить адрес»
    Адрес, события и кнопка «Добавить адрес»
  4. Скопируйте секрет из тёмного блока — он показывается один раз. По нему ваш сервер проверяет, что уведомление пришло именно из BSTCRM.
  5. Нажмите Проверить — CRM отправит тестовое событие ping. Ответ сервера и все отправки видны по кнопке Журнал.

Что приходит

Запрос POST с JSON. Заголовки: X-BSTCRM-Event (событие), X-BSTCRM-Delivery (уникальный номер уведомления), X-BSTCRM-Signature (подпись).

json
{
  "id": "5e1c9a2e-0f7b-4f43-9b8b-6a2f1a9d3c71",
  "event": "deal.status_changed",
  "created_at": "2026-10-01T09:12:44Z",
  "data": {
    "deal": {
      "id": 214, "url": "https://ваша-компания.bstcrm.ru/orders/214", "title": "Новая сделка",
      "branch": {"id": 1, "name": "Акорус"},
      "status": {"id": 12, "name": "Успешно реализовано", "is_success": true},
      "total_amount": 4500.0, "paid_amount": 4500.0, "debt": 0.0,
      "client": {"id": 75, "name": "Иван Тестовый", "phone": "+79001234567"},
      "vehicle": {"id": 41, "brand": "Volkswagen", "model": "Polo", "plate": "Х345ТУ 78", "vin": "V6W5YDLMA9TZZV479"},
      "created_at": "2026-10-01T08:40:02Z", "issued_at": "2026-10-01T09:12:44Z"
    },
    "previous_status": {"id": 11, "name": "Готово к выдаче"}
  }
}

Для deal.paid вместо previous_status приходит "payment": {"amount": 4500.0}, для client.created — "client": {...}.

Как ответить

Верните код 200–299 в течение 5 секунд. Если сервер не ответил или ответил ошибкой, CRM повторит отправку через 5 и через 30 секунд. После 50 неудачных отправок подряд адрес выключается — включить его снова можно кнопкой Включить.

Проверка подписи

Заголовок выглядит так: X-BSTCRM-Signature: t=1696150364,v1=5f2b…. Подпись — HMAC-SHA256 от строки <t>.<тело запроса> с вашим секретом. Проверяйте подпись по сырому телу запроса и отклоняйте уведомления старше 5 минут.

Python:

python
import hashlib, hmac, time

def is_from_bstcrm(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    ts, signature = parts.get("t", "0"), parts.get("v1", "")
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature) and abs(time.time() - int(ts)) < 300

PHP:

php
<?php
function isFromBstcrm(string $rawBody, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $parts);
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1'] ?? '') && abs(time() - (int)$parts['t']) < 300;
}
$raw = file_get_contents('php://input');
if (!isFromBstcrm($raw, $_SERVER['HTTP_X_BSTCRM_SIGNATURE'] ?? '', 'whsec_ваш_секрет')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
http_response_code(200);

Node.js:

javascript
const crypto = require("crypto");
function isFromBstcrm(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
Не нашли ответ?Напишите нам — ответим и допишем статью, чтобы следующему было понятнее.
Связаться с нами