Вебхуки приложения и события: настройка, темы, проверка подписи

Обновлено: 11 ноября 2026 · 8 мин чтения

Вебхук это HTTP-запрос, который отправляется на Ваш сервер, когда в Clomni происходит событие: как только клиент пишет, приходит message.created, и проверять список каждую минуту не нужно. Эта статья о вебхуке приложения на developer.clomni.ai. Вебхук workspace в панели («Settings › Workspace settings › Integrations › Webhook») отдельный, для него см. статью «Настроить вебхук (webhook): отправлять события в свою систему».

Шаги

  1. 1

    Настройте вебхук

    В разделе приложения «Webhooks»:

    1. Укажите URL. Адрес должен начинаться с https:// и быть открыт из интернета: внутренние адреса, такие как localhost, 10.x, 192.168.x, не принимаются, перенаправления не выполняются.
    2. Выберите темы. Во внутреннем приложении можно выбрать только темы, которые соответствуют одобренному scope, в публичном приложении достаточно запрошенного scope.
    3. Сохраните секрет: он начинается с whsec_ и показывается только один раз.

    У одного приложения один адрес вебхука.

  2. 2

    Подтвердите адрес

    Когда Вы сохраняете URL, Clomni отправляет туда { "type": "webhook.verify", "challenge": "c2b1f0a8e7d94c3b" }. Ваш сервер должен в течение 5 секунд ответить 200 с этим же значением в теле: { "challenge": "c2b1f0a8e7d94c3b" }. Если ответа нет, вебхук остаётся «не подтверждён» и события не отправляются: исправьте сервер и сохраните адрес на портале ещё раз. Этот запрос тоже подписывается.

  3. 3

    Темы и нужные scope

    • conversation.created, conversation.updated (метка, приоритет, атрибут), conversation.status_changed, conversation.assigned: conversations:read.
    • message.created (входящее, исходящее, внутренняя заметка), message.updated (статус стал delivered, read, failed): messages:read.
    • contact.created, contact.updated: contacts:read.

    Если scope удалён из приложения, соответствующая тема тоже перестаёт приходить. В публичном приложении, когда администратор отключает приложение, приходит app.uninstalled независимо от выбранных тем.

  4. 4

    Формат запроса

    Заголовки: Content-Type: application/json, User-Agent: Clomni-Webhooks/1.0, webhook-id, webhook-timestamp, webhook-signature. Тело:

    {
      "id": "evt_2Wq8mX4vT9kLpR3n",
      "type": "message.created",
      "api_version": "v1",
      "workspace_id": 42,
      "occurred_at": "2026-11-08T16:00:00.412Z",
      "data": { "id": 51234, "conversation_number": 1042, "direction": "incoming", "content": "Здравствуйте, когда придёт мой заказ?" }
    }

    id совпадает с заголовком webhook-id, используйте его, чтобы распознать дубликат. data в том же виде, что в эндпоинтах синхронизации. В conversation.updated и conversation.status_changed есть также changed_attributes. В публичном приложении в конверте есть ещё installation_id.

  5. 5

    Проверьте подпись

    Вебхук без проверки может подделать кто угодно.

    1. Читайте тело в сыром виде, не разбирайте JSON и не записывайте его заново.
    2. Подписываемый текст: {webhook-id}.{webhook-timestamp}.{сырое тело}.
    3. Ключ: часть секрета после whsec_, декодированная из base64.
    4. Вычислите HMAC-SHA256 и запишите результат в base64.
    5. В webhook-signature может быть несколько записей v1,<подпись> через пробел: если одна из них совпадает при сравнении за постоянное время, подпись верна.
    6. Если webhook-timestamp старше 5 минут, отклоните запрос.
    import crypto from "node:crypto";
    
    function verifyWebhook(rawBody, headers, secret) {
      const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"];
      if (!id || !ts || !sigs || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
      const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
      const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
      return sigs.split(" ").some((part) => {
        const [v, sig] = part.split(",");
        const given = Buffer.from(sig || "", "base64");
        return v === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
      });
    }
    import base64, hashlib, hmac, time
    
    def verify_webhook(raw_body: bytes, headers, secret: str) -> bool:
        msg_id, ts, sigs = headers.get("webhook-id"), headers.get("webhook-timestamp"), headers.get("webhook-signature")
        if not msg_id or not ts or not sigs or abs(time.time() - int(ts)) > 300:
            return False
        key = base64.b64decode(secret.removeprefix("whsec_"))
        expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()).decode()
        return any(p.partition(",")[0] == "v1" and hmac.compare_digest(p.partition(",")[2], expected) for p in sigs.split(" "))

    Тестовые значения: секрет whsec_aSGonRLU/DDLGZkKrR6fe7DCdyC22NiWOb8feJg/O6k=, webhook-id evt_2Wq8mX4vT9kLpR3n, webhook-timestamp 1794153600, тело {"id":"evt_2Wq8mX4vT9kLpR3n","type":"message.created","api_version":"v1","workspace_id":42,"occurred_at":"2026-11-08T16:00:00Z","data":{"id":51234}}. Результат должен быть v1,hJufoXNHJ1dUd1zcZdtiPLkp9Px9eS+ZIUi9odi4Cvk=. Этот секрет только для документации, время старое: при проверке отключите проверку 5 минут.

  6. 6

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

    • Сервер должен вернуть 2xx за 5 секунд: поставьте событие в очередь и сразу ответьте.
    • Если 2xx не пришёл, Clomni отправляет повторно через 1 минуту, 5 минут, 30 минут, 2 часа, 6 часов, 12 часов и 24 часа.
    • Если 3 дня подряд ни одна доставка не удалась, вебхук отключается, и владельцу приложения уходит e-mail. Исправьте сервер и снова включите вебхук на портале.
    • Одно и то же событие может прийти дважды (отбрасывайте дубликаты по webhook-id), порядок не гарантируется (сортируйте по occurred_at).
    • message.updated не сообщает причину failed: прочитайте сообщение и посмотрите content_attributes.external_error.
  7. 7

    Проверка на портале

    В разделе «Webhooks» есть журнал доставок за последние 30 дней (тема, статус, код ответа, длительность, ошибка). «Send test» (отправить тест) сразу отправляет пробное событие типа webhook.test, «Resend» (отправить повторно) повторяет неудачную доставку. После смены секрета в течение 24 часов в заголовке две подписи (старая и новая). Если сервер долго не работал, заберите пропущенные события через GET /v1/events с теми же id. Для локальной проверки получите HTTPS-адрес через туннель, например ngrok или Cloudflare Tunnel.

  8. 8

    Если вебхук не приходит

    1. Вебхук на портале в статусе «подтверждён»? Если нет, сервер не возвращает challenge за 5 секунд.
    2. Одобрен ли scope темы?
    3. Отвечает ли сервер 2xx за 5 секунд? Посмотрите в журнале код ответа и ошибку.
    4. Открыт ли URL из интернета, это https, нет ли перенаправления?
    5. Если вебхук отключён, включите его снова.

Документ