CRM-интеграции

CRM-подключение связывает одного бота с одним аккаунтом CRM, чтобы профили клиентов, сделки и сводки диалогов передавались в обе стороны. Поддерживаются четыре провайдера: amoCRM и Kommo, Bitrix24, RetailCRM и OzmaCRM (ozma.io).

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

Что доступно сейчас

Экран CRM находится в кабинете: Интеграции → CRM. Нужно право бота «Реакции». С этого экрана можно:

  • подключить CRM через мастер, переименовать подключение, проверить его, переподключить и удалить;
  • редактировать маппинг полей и сохранять его новой ревизией;
  • прогнать предпросмотр маппинга на реальных профилях подписчиков и запустить первичную синхронизацию;
  • следить за журналом синхронизации – операциями и их задачами по каждой записи, с повтором упавшей задачи;
  • разбирать конфликты, которые политика маппинга оставила человеку.

Планировщик надёжной синхронизации работает: воркер задач просыпается каждые 5 секунд (пачки по 20 задач, аренда задачи 2 минуты), а сверка по опросу – каждые 30 секунд (пачки по 20 просроченных курсоров). Включать что-либо отдельно не нужно.

Доступность провайдеров различается, и это стоит прочитать внимательно:

ПровайдерКак подключаетсяВходящие изменения на практике
amoCRM / KommoOAuth, самостоятельно в мастереопрос – см. раздел «Приём вебхуков и модели доверия»
RetailCRMAPI-ключ, самостоятельно в мастереопрос
OzmaCRMOIDC-клиент плюс логин пользователя, самостоятельно в мастеретолько опрос
Bitrix24подключить нельзя – эндпоинт подключения отвечает 503

Адаптер Bitrix24 реализован полностью и проходит тот же контрактный набор тестов, что и остальные, но OAuth-приложение для него на платформе не настроено, поэтому подключаться не к чему. В мастере его карточка помечена как недоступная, а не даёт заполнить форму, которая всё равно упадёт.

Как достигается провайдер

Любой запрос к CRM, независимо от провайдера, проходит через один защищённый транспорт:

  • Только HTTPS. Без user info в адресе, без своего порта и без прокси, унаследованного из окружения процесса.
  • Хост заново разрешается перед каждым запросом, и соединение отклоняется, если любой из полученных адресов – loopback, приватный, link-local, multicast или адрес облачных метаданных. Редиректы обязаны остаться на том же хосте и проверяются точно так же.
  • Тело ответа ограничено 1 МБ. Таймаут запроса – 30 секунд, TLS-рукопожатия – 10 секунд, заголовков ответа – 15 секунд.
  • Retry-After и X-RateLimit-Reset учитываются как подсказки для повтора, но не более 5 минут. Лимиты частоты – это лимиты самой CRM: дополнительной квоты вызовов на стороне платформы нет.
  • Ошибка провайдера сводится к короткому коду в нижнем регистре. HTTP 429, 5xx и сбои транспорта классифицируются как временные и повторяются, остальное – терминально. Тела ответов, адреса запросов и текст ошибок провайдера никогда не попадают в логи, ошибки задач и аудит.

Три провайдера из четырёх можно направить только на закрытый набор хостов, выводимый из указанного идентификатора аккаунта:

  • amoCRM и Kommo – <аккаунт>.amocrm.ru или <аккаунт>.kommo.com.
  • Bitrix24 – <портал>.bitrix24.ru или <портал>.bitrix24.com. Конфиденциальный обмен токенами дополнительно идёт на единственный фиксированный сервер авторизации oauth.bitrix.info.
  • RetailCRM – <аккаунт>.retailcrm.ru.

OzmaCRM – исключение. Поскольку поддерживаются self-hosted инстансы, её базовый адрес задаёт оператор, а не выводится из разрешённого суффикса. Проверяется только форма (HTTPS, без user info, без порта, без пути, запроса и фрагмента); от обращения на внутренний адрес защищает именно описанный выше запрет приватного egress.

amoCRM и Kommo

Предварительные условия. Оператор платформы регистрирует одну OAuth-интеграцию и задаёт AMOCRM_CLIENT_ID и AMOCRM_CLIENT_SECRET. Обе переменные задаются вместе, а APP_BASE_URL должен быть абсолютным HTTPS-origin, потому что redirect URI выводится из него как <APP_BASE_URL>/api/integrations/crm/amocrm/callback. Пока это не настроено, эндпоинты подключения отвечают 503.

Подключение. В кабинете это карточка amoCRM в мастере. По REST владелец бота отправляет хост аккаунта и переходит по возвращённому адресу:

curl -X POST "$BASE_URL/api/bots/$BOT_ID/crm/connections/amocrm/start" \
  -H "Authorization: Bearer $PAT" \
  -H "Content-Type: application/json" \
  -d '{"account_key":"acme.amocrm.ru"}'

В ответе приходит redirect_url. Браузерное состояние одноразовое, живёт 5 минут и хранится в виде хеша, никогда в открытом виде. Стартов не больше трёх в минуту на аккаунт. Отказ в согласии тоже расходует состояние, поэтому тот же callback нельзя повторить с подделанным кодом.

Проверка при подключении. После обмена токенами аккаунт перечитывается через GET /api/v4/account. Поддомен, который вернула amoCRM, обязан совпасть с указанным ключом аккаунта, иначе подключение падает с несовпадением аккаунта, а валюта аккаунта должна быть корректным кодом ISO 4217. Токены запечатываются конвертным шифрованием с привязкой к подключению, боту, провайдеру, нормализованному хосту аккаунта, версии ключа и ревизии кредов, поэтому шифртекст, перенесённый в другое подключение или на другой разрешённый поддомен, не откроется.

Возможности. Клиенты, лиды, сделки, диалоги, вебхуки, опрос и OAuth, плюс валюта аккаунта.

Поля и воронки. Кастомные поля обнаруживаются для контактов и лидов и доступны как custom:<id поля>. Вычисляемое поле никогда не доступно на запись, поле multiselect помечается как множественное, а неизвестный MyBot тип поля предлагается только на чтение, а не угадывается. Воронки и их статусы приходят из GET /api/v4/leads/pipelines.

Запись. Контакты и лиды пишутся с request_id, равным ключу идемпотентности записи (не длиннее 255 символов), и ответ обязан вернуть ровно одну запись с тем же ключом – иначе эффект считается неоднозначным, а не успешным.

Диалоги. Выгружаются обычным примечанием к контакту или лиду. Сводка и каждое сообщение на входе ограничены 24 000 символами, сохраняются только последние 50 сообщений, а склеенный текст обрезается до 6 000 символов.

Вебхуки. amoCRM и Kommo не подписывают тела вебхуков, поэтому аутентификацией служит сам непрозрачный маршрут приёма. Сверх этого адаптер принимает JSON или form-encoded не больше 256 КБ, отклоняет дубли ключей JSON, хвостовые данные и вложенность глубже 64 уровней и требует ровно одно событие сущности на доставку. Если в теле объявлен account[subdomain], он обязан совпасть с подключённым аккаунтом. Принимаются события добавления и изменения контакта и добавления, изменения и смены статуса лида. Затем запись перечитывается через API, и её updated_at не должен быть старше события, так что авторитетное состояние никогда не берётся из тела вебхука.

Опрос. Контакты листаются по возрастанию updated_at, по 50 на страницу, с зашифрованным курсором.

Bitrix24

Сегодня Bitrix24 подключить нельзя. POST /api/bots/{botID}/crm/connections/bitrix24/start отвечает 503: OAuth-приложения Bitrix24 на платформе нет, а маршрута с API-ключом, как у RetailCRM, для него не существует. Дальше описан готовый и покрытый тестами адаптер, до которого пока нельзя добраться, – читайте это как справку, а не как инструкцию по настройке.

Предварительные условия. Либо локальное OAuth-приложение на портале, либо входящий вебхук. Одно подключение использует что-то одно, никогда оба сразу.

Режим OAuth. Согласие даётся на https://<портал>/oauth/authorize/, обмен токенами идёт на фиксированный сервер oauth.bitrix.info. В ответе с токеном должен быть домен oauth.bitrix.info и клиентский эндпоинт ровно https://<портал>/rest/, иначе подключение отклоняется как несовпадение портала. Возвращённый member_id обязателен и сохраняется: позже им аутентифицируются вебхуки.

Режим входящего вебхука. Портал, member id и токен вебхука запечатываются вместе, а REST-вызовы адресуются как /rest/<member id>/<токен>/<метод>.json. В этом режиме refresh-токена нет, поэтому подключение сообщает, что OAuth недоступен. Так как кред живёт прямо в пути запроса, ошибки транспорта всегда сводятся к стабильному коду, а не цитируют адрес.

Минимум прав. Выдавайте только те CRM-права, которые нужны вашему маппингу. Адаптер вызывает profile, crm.contact.fields, crm.lead.fields, crm.deal.fields, crm.dealcategory.list, crm.status.list, crm.contact.list, методы crm.{contact,lead,deal}.{add,update,get} и crm.timeline.comment.add. Больше ничего не используется.

Возможности. Клиенты, лиды, сделки, диалоги, вебхуки, опрос и идемпотентные внешние ключи. Валюту аккаунта Bitrix24 не сообщает, поэтому денежный маппинг обязан явно указать валюту ISO.

Поля и воронки. Поля контакта, лида и сделки читаются с портала с сохранением флагов «только чтение» и множественности. Воронки – это категории сделок, а статусы каждой категории читаются из crm.status.list с фильтром DEAL_STAGE_<id категории>.

Контакты или лиды. По умолчанию клиент пишется контактом. Настройка {"customer_entity":"lead"} в конфигурации подключения переключает запись на лиды.

Запись. crm.<сущность>.add и crm.<сущность>.update вызываются с REGISTER_SONET_EVENT=N, чтобы синхронизация не засоряла ленту активности. Создание новой записи требует замапленного поля внешнего ключа, и оно должно быть доступно на запись – иначе запись отклоняется, а не создаётся молча без возможности связать её.

Диалоги. Добавляются комментарием в таймлайн контакта, лида или сделки с префиксом [mybot:<ключ идемпотентности>], чтобы повторную доставку было видно в таймлайне.

Вебхуки. application_token и member_id сравниваются за постоянное время, а auth.domain обязан совпасть с подключённым порталом. Тела – только JSON, до 256 КБ, без хвостовых данных. Обрабатываются события ONCRMCONTACTADD/UPDATE/DELETE, ONCRMLEADADD/UPDATE/DELETE и ONCRMDEALADD/UPDATE/DELETE. Любое событие, кроме удаления, перечитывает запись через crm.<сущность>.get, и возвращённый ID обязан совпасть с событием.

Опрос. crm.contact.list с сортировкой по DATE_MODIFY и ID, с продолжением от зашифрованной контрольной точки.

RetailCRM

Предварительные условия. API-ключ. OAuth у RetailCRM здесь нет: и авторизация, и обновление отвечают явным кодом «не поддерживается», а ключ – единственный кред.

Подключение. Карточка RetailCRM в мастере или напрямую:

curl -X POST "$BASE_URL/api/bots/$BOT_ID/crm/connections/retailcrm" \
  -H "Authorization: Bearer $PAT" \
  -H "Content-Type: application/json" \
  -d '{"name":"Магазин","account_key":"acme.retailcrm.ru","api_key":"…"}'

Поле name необязательно и по умолчанию равно хосту аккаунта. В ответе – безопасная сводка подключения, но не ключ.

Минимум прав. При подключении адаптер читает /api/credentials и справочники sites, stores и order-methods; каждый из трёх должен вернуть хотя бы одну пригодную запись. Выгрузка диалогов дополнительно требует права customer_write (на старых аккаунтах – кред /api/v5/customers/notes/create). Без него подключение просто сообщает, что диалоги недоступны, а не падает в момент выгрузки.

Возможности. Клиенты, сделки, заказы, опрос и идемпотентные внешние ключи. Диалоги зависят от права выше, вебхуки – от сохранённого токена, см. ниже.

Поля и воронки. Кастомные поля читаются из /api/v5/custom-fields отдельно для сущностей клиента и заказа и доступны как custom:<код>. Поле доступно на запись, только если RetailCRM пометила его editable. Обнаружение читает по 250 полей за раз, не больше 100 страниц и 10 000 полей. Статусы заказов группируются по сайтам, и каждый сайт становится отдельной воронкой.

Запись. Клиенты и заказы создаются и правятся через /api/v5/customers/... и /api/v5/orders/... формами; API-ключ добавляется на сервере и никогда не попадает в маппинг. Локальный идентификатор MyBot пишется в externalId. Если идентификатор, вернувшийся из RetailCRM, не совпал со связанной записью, эффект помечается неоднозначным, а связь молча не переставляется.

Диалоги. Выгружаются примечанием клиента, текст ограничен 2 000 символами.

Вебхуки. RetailCRM не подписывает колбэки. Подключение можно запечатать токеном, который выдаёт MyBot (от 16 до 256 печатных символов) и который вы вставляете в шаблон вебхука самой RetailCRM; тогда доставки обязаны нести его в заголовке X-Mybot-Webhook-Token, и он сравнивается за постоянное время. Подключение без такого токена отвечает webhook_unsupported, и единственным входящим транспортом остаётся опрос – ровно это и сообщают возможности подключения. В теле самостоятельного подключения выше поля для этого токена пока нет, поэтому созданное вами подключение работает только на опросе. Тела ограничены 64 КБ, тип обязан быть customer или order, событие – create, update или delete, и любое событие, кроме удаления, перечитывает запись по ID, потому что шаблонное тело несёт только идентичность.

Опрос. /api/v5/customers/history и /api/v5/orders/history, по 50 записей на страницу, с продолжением по sinceId или startDate.

OzmaCRM

OzmaCRM – это ozma.io, low-code CRM/ERP на базе OzmaDB. У неё нет фиксированной схемы CRM, поэтому, в отличие от трёх других провайдеров, это вы сообщаете MyBot, какие сущности использовать, а не наоборот.

Предварительные условия.

  • Базовый адрес инстанса. Облачные инстансы выглядят как https://<аккаунт>.api.ozma.org, self-hosted – любой HTTPS-origin, которым вы управляете.
  • Client id и client secret OIDC, а не только логин. Аутентификация идёт по OIDC resource-owner password grant, и запрос отправляется с клиентскими кредами вашего арендатора.
  • Логин и пароль пользователя OzmaCRM, от имени которого будет работать MyBot. Дайте ему минимальную роль, которой хватает на чтение и запись ваших сущностей.
  • Базовый адрес OIDC и realm – для self-hosted инстанса. Для облака realm по умолчанию default на https://account.ozma.io, эндпоинт токена – /auth/realms/<realm>/protocol/openid-connect/token.
  • Журнальная сущность usr.mybot_sync_marks, описанная ниже. Создайте её до подключения: без неё проверка подключения падает.

Refresh-токен обязателен. Срок действия access-токена проверяется локально: как только он истёк, адаптер падает с token_expired, а не обновляет токен посреди записи, – подключение перезапечатывается до следующей отправки, а не в середине эффекта.

Выбор сущностей. Подключение несёт небольшую конфигурацию:

  • customer_entity – схема и сущность с клиентами (обязательно).
  • deal_entity – схема и сущность со сделками (обязательно).
  • conversation_entity – необязательно, сущность для журнала диалогов.
  • deal_status_field – колонка сделки, домен которой становится воронкой.
  • updated_at_field – колонка времени изменения, по умолчанию updated_at.

Каждое имя схемы, сущности и поля должно состоять из строчных ASCII-букв, цифр или подчёркиваний, не начинаться с цифры и быть не длиннее 64 символов. Это не косметика: имена подставляются в FunQL, поэтому белый список здесь – граница защиты от инъекций.

Журнал синхронизации. OzmaCRM – единственный провайдер, где причинные метки MyBot не могут жить на ваших собственных записях. Вместо этого они идут в отдельную журнальную сущность, которую вы создаёте сами: MyBot никогда не разворачивает и не заменяет схему, потому что загрузка макета перезаписала бы ваши данные. Создайте её до включения синхронизации:

create entity usr.mybot_sync_marks with fields:
  entity_kind string not null   -- customer | lead | deal | order | conversation
  remote_id   string            -- nullable: пусто для первой метки новой записи
  marker      string not null   -- ключ идемпотентности эффекта
  direction   string not null   -- outbound | inbound
  created_at  datetime not null

Проверка подключения смотрит /api/check_access, затем этот журнал, затем все настроенные сущности. Отсутствующий или несовместимый журнал приводит к отказу с missing_sync_marks_entity и возвращает определение выше дословно; синхронизация не включается, пока журнала нет.

Поля и воронки. Доступные на запись колонки настроенных сущностей обнаруживаются из их entity info (id пропускается). int и double превращаются в целое, bool – в булево, date и datetime – во время, а string, reference, enum и uuid – в строку; колонка-массив берёт свой подтип и помечается множественной. Неизвестный MyBot тип колонки пропускается, а не угадывается, и остаётся виден через overflow в предпросмотре маппинга. Так как в OzmaDB нет понятия воронки, домен deal_status_field становится одной синтетической воронкой.

Запись. Каждая запись – это один POST /api/entities/transaction с двумя операциями: сначала сама запись, затем вставка в журнал. Обе фиксируются вместе, поэтому метка не может существовать без изменения, которое она описывает. Если транзакция упала, в ошибке сказано, какая из двух операций не прошла.

Колонки, которые MyBot пишет сам, фиксированы, поэтому ваши сущности обязаны их принимать:

  • Клиент – name, плюс phone, email и tags, когда они есть, плюс все замапленные колонки, которые обнаружение пометило доступными на запись.
  • Сделка – name, status, amount_minor, currency, customer (id связанного клиента) и tags. Записи сделки нужны статус и трёхбуквенная валюта.
  • Диалог – customer или deal (id владельца), summary и created_at.

Замапленное значение, чья колонка не помечена доступной на запись, отбрасывается до запроса, поэтому ревизия маппинга не может протащить запись в колонку, которую никто не предлагал.

Диалоги. Сводка и каждое сообщение ограничены 16 384 байтами, сохраняются последние 50 сообщений, а склеенный текст обрезается до 32 000 байт по границе корректного UTF-8.

Опрос – и только опрос. Изменения читаются keyset-запросом FunQL с сортировкой по вашей колонке updated_at и id, по 200 строк на страницу, с продолжением строго после последней возвращённой строки, – поэтому перекрывающиеся окна опроса не могут пропустить или продублировать запись. Каждая строка левым соединением связывается с журналом, чтобы отличить входящее изменение от эха собственной последней записи MyBot.

Своих вебхуков у OzmaDB нет, и маршрута приёма для OzmaCRM в MyBot сегодня тоже нет. Поэтому подключение OzmaCRM получает входящие изменения только по циклу опроса; триггера, который можно было бы установить, пока не существует.

Обнаружение полей и маппинг

Маппинг – это неизменяемая ревизия. Правка маппинга создаёт новую ревизию и не переосмысляет уже запущенную задачу, поэтому сегодняшнее изменение не может переписать смысл записи, поставленной в очередь раньше.

Каждое сопоставление поля называет:

  • локальный путьprofile.id, profile.name, profile.first_name, profile.last_name, profile.phone, profile.email, profile.tags или любое другое свойство профиля как profile.<свойство>; а для сделок – deal.id, deal.customer_id, deal.name, deal.pipeline, deal.status, deal.amount_minor (или deal.value_minor), deal.currency либо любое другое свойство сделки как deal.<свойство>;
  • удалённый ключ из списка обнаруженных полей;
  • сущность – клиент, лид, сделка или заказ;
  • направление – исходящее (MyBot → CRM) или входящее (CRM → MyBot);
  • преобразование;
  • и обязательно ли значение.

Преобразования

identity, string, integer, boolean, timestamp, datetime_rfc3339, phone_e164, email_normalized, enum_map, tags и money_minor_currency. (minor_units остаётся для маппингов, созданных до того, как у денег появилась явная валюта.)

Они декларативные и никогда не являются кодом клиента:

  • email_normalized обрезает пробелы и приводит к нижнему регистру, требует формы адреса и отклоняет всё длиннее 320 символов.
  • phone_e164 принимает пробелы, дефисы и скобки как разделители и выдаёт номер с + длиной от 9 до 16 символов, не начинающийся с +0.
  • string ограничивает значение 10 000 символами.
  • tags требует строк, обрезает пробелы, отклоняет пустые теги и теги длиннее 128 символов, а также убирает дубли.
  • enum_map отображает только по закрытому списку, который вы задаёте. На входе список инвертируется, и если два локальных значения ведут в одно удалённое, сопоставление неоднозначно и удалённое значение уходит в overflow, а не угадывается.
  • money_minor_currency выдаёт знаковые минорные единицы плюс валюту ISO 4217.

Чему должен удовлетворять маппинг

Сохранение ревизии падает, если локальный путь неизвестен, удалённого ключа нет у этой сущности, исходящее сопоставление целится в поле, помеченное провайдером только на чтение, у enum_map нет значений, у денежного маппинга нет валюты ISO в верхнем регистре, два сопоставления пишут в одно и то же удалённое назначение в одном направлении для одной сущности, два входящих сопоставления пишут в одно локальное назначение или преобразование не связывает локальный и удалённый типы. Тегам нужны обе стороны, а триггер обязан назвать известное событие, свою воронку и один из её статусов.

Теги: только входящие

Метки MyBot никогда не уезжают в CRM тегами. В профиле подписчика хранятся идентификаторы меток, а не человекочитаемые названия, с которыми сравнивает сопоставление тегов, поэтому отправка их наружу записала бы в карточку клиента непрозрачные строки вида 7f3c1a2e-…. Исходящий список тегов намеренно остаётся пустым.

Входящие теги работают штатно: сопоставление тегов преобразует и нормализует удалённые теги в локальные, а незамапленный удалённый тег уходит в overflow, а не пропадает. Если нужно показать метку MyBot в CRM, замапьте её как обычное свойство профиля через enum_map.

Overflow

Каждый маппинг объявляет одно назначение для overflow – поле или примечание плюс ключ, – и оно обязательно. Ничто, что не удалось сопоставить, не выбрасывается. Значение, не прошедшее преобразование, отсутствующее обязательное значение, локальное свойство без сопоставления, незамапленный тег, незамапленное удалённое свойство и незамапленный удалённый тег записываются в overflow вместе с исходным значением и причиной и уходят в это назначение.

Политика конфликтов

  • mybot_wins – удалённое изменение никогда не перезаписывает локально изменённый профиль.
  • crm_wins – удалённое изменение применяется.
  • newest_wins – побеждает более поздняя отметка времени; при точном совпадении изменение уходит на ручной разбор.
  • manual – каждое конфликтующее изменение уходит на ручной разбор.

Профиль, в котором ещё не зафиксировано локальное изменение, принимает удалённое при любой политике. Независимо от политики удалённое изменение, которое не новее последнего уже записанного удалённого изменения по связи, игнорируется.

Предпросмотр маппинга

До любой записи маппинг можно спроецировать на реальные записи. Предпросмотр детерминирован, не зависит от провайдера и потому не может вызвать удалённую запись – он лишь показывает, что было бы отправлено.

Он берёт не больше 20 профилей подписчиков, показывает каждое замапленное поле с исходным значением, преобразованным значением, предупреждением и ошибкой, и перечисляет overflow, который дал бы этот маппинг. Рядом с выборкой он сообщает число затронутых записей, помечая его точным, когда запрос по своим записям полон, или оценочным, когда доступна только ограниченная оценка.

Список сделок в выборке всегда пуст – и в предпросмотре, и в первичной синхронизации. Своего объекта «сделка» у MyBot до подключения CRM нет, поэтому наружу выгружать нечего, кроме профилей подписчиков. Сделки движутся в обратную сторону: они приходят из CRM и становятся событиями клиента и целями (см. раздел «Сделки, деньги и атрибуция»). Поля сделки в редакторе маппинга по-прежнему настраиваются и используются для входящих изменений и для сделок, которые вы отправляете из диалога.

Исходящие записи, связи и идемпотентность

Каждая запись несёт стабильный ключ идемпотентности по записи и приводит к сохранённой связи – паре вашего локального идентификатора и id записи в CRM вместе с удалённой версией и последними локальной, удалённой и синхронизированной отметками времени. Именно связь, а не догадка, превращает повторную запись в обновление, а не в дубль.

Каждый провайдер закрепляет ключ в своём родном механизме: request_id у amoCRM и Kommo, поле внешнего ключа и метка в комментарии у Bitrix24, externalId у RetailCRM и транзакционная строка журнала у OzmaCRM.

Приём вебхуков и модели доверия

Входящие изменения приходят в MyBot либо из курсора опроса, либо вебхуком от провайдера. На стороне вебхуков есть три отдельных маршрута приёма, по одному на провайдера, и у каждого своя модель доверия:

МаршрутЧем аутентифицируется доставка
POST /hooks/crm/{routeKey}/amocrmв теле ничего не подписано: секрет – сам непрозрачный маршрут, плюс account[subdomain] обязан совпасть с подключённым аккаунтом
POST /hooks/crm/{routeKey}/bitrix24application_token и member_id сравниваются за постоянное время, а auth.domain обязан совпасть с подключённым порталом
POST /hooks/crm/{routeKey}/retailcrmзаголовок X-Mybot-Webhook-Token, выданный MyBot, сравнивается за постоянное время; подключение без сохранённого токена отвечает webhook_unsupported

Ключ маршрута случаен для каждого подключения и хранится только в виде хеша, а провайдер закрепляется в момент регистрации: отправка тела amoCRM на маршрут RetailCRM или на неподключённое подключение – это не отдельная ошибка, а тот же самый 404, что и неизвестный маршрут. Доставка, попавшая в нужный маршрут, но не прошедшая проверку адаптера, получает один общий 400. Ни один из ответов не сообщает, существует ли маршрут или подключение. Любое тело ограничено 256 КиБ, а на чтение даётся 10 секунд.

Сегодня эти маршруты ещё не самостоятельные. Адрес маршрута для подключения генерируется при его создании, но не показывается в кабинете и не возвращается ни одним API, а для токена вебхука RetailCRM нет поля в теле подключения. На практике это значит, что входящие изменения у всех провайдеров приходят по циклу опроса. Модели доверия выше – это то, чему доставка должна будет удовлетворять, когда адрес маршрута начнут выдавать; читайте раздел как справку, а не как чек-лист настройки.

Входящие изменения и подавление эха

Входящее изменение приходит либо из проверенного вебхука, либо из курсора опроса, и оба пути сходятся в одной нормализации.

Общая проблема обоих путей – эхо: MyBot пишет в CRM, CRM сообщает об изменении обратно, и наивная интеграция зацикленно применяет собственную запись. Подавление здесь по эффекту и одноразовое:

  1. Исходящая запись ставит метку на удалённой записи и фиксирует её как ожидаемую для этой связи.
  2. Входящее изменение подавляется, только если несёт метку, которая всё ещё ожидается для ровно этого подключения и удалённой записи, не старше 24 часов и наблюдалась не позже последнего известного удалённого изменения связи.
  3. После этого метка расходуется.

Практическое следствие важно: более позднее изменение с тем же значением считается настоящей правкой человека, а не очередным эхом. Если кто-то откроет CRM и поправит поле, которое MyBot только что записал, – пусть даже вернёт то же значение, – эта правка не будет проглочена. Метка, которая так и не вернулась эхом, просто истекает по своему сроку, поэтому потерянный вебхук не может навсегда заблокировать входящую синхронизацию связи.

Стабильный идентификатор связи для этого намеренно не используется. Он постоянен всю жизнь связи, так что подавление по нему проглатывало бы и все последующие правки человека.

Как только изменение пережило подавление и политику конфликтов, замапленные значения становятся аудируемыми изменениями свойств профиля, каждое со своим происхождением: подключение, связь, провайдер, удалённый id, удалённая версия и время изменения. Пустое значение в необязательном входящем поле очищает локальное свойство; в обязательном – уходит в overflow.

Повторная сверка

Сверка по опросу сама продвигает просроченные курсоры каждые 30 секунд, но подключение можно заставить догнать состояние немедленно:

curl -X POST "$BASE_URL/api/bots/$BOT_ID/crm/connections/$CONNECTION_ID/reconcile" \
  -H "Authorization: Bearer $PAT"

Эндпоинт отвечает {"status":"reconciling"} и перечитывает ленту изменений провайдера от сохранённого курсора. Используйте его после сбоя у провайдера, после доставки, которая, по вашему мнению, потерялась, или когда правки на стороне CRM явно не видно в MyBot. Повторять безопасно: уже применённое изменение опознаётся по связи и ключу идемпотентности, а не применяется дважды.

Кнопки для этого эндпоинта в кабинете пока нет. Он доступен только по API, со скоупом crm:write и правом бота «Реакции».

Действия из диалога

Оператор, ведущий переписку, может посмотреть на CRM-сторону конкретного человека и отправить его туда вручную. Эти три действия существуют только по REST – вкладки CRM в операторском инбоксе пока нет. Всем трём нужно право бота «Диалоги».

  • GET /api/bots/{botID}/subscribers/{subscriberID}/crm/status (subscribers:read) – связан ли подписчик, с каким подключением и удалённой записью, когда синхронизировался последний раз и чем закончилась последняя задача.
  • POST /api/bots/{botID}/subscribers/{subscriberID}/crm/sync (subscribers:write) – отправить подписчика сейчас. В теле указывается connection_id, а также может быть необязательная deal (имя, воронка, статус, value_minor, currency, дополнительные свойства), которую создадут или обновят вместе с клиентом. Подписчик проецируется ровно так же, как его проецирует фоновая синхронизация, поэтому ручная отправка никогда не разойдётся с плановой.
  • POST /api/bots/{botID}/subscribers/{subscriberID}/crm/export-conversation (subscribers:write) – записать переписку в CRM примечанием. В теле только connection_id; что именно уезжает, описано ниже.

Каждое действие возвращает тот же объект надёжной операции, что показывает журнал синхронизации, поэтому ручное действие видно в списке операций подключения наравне с остальными.

Что содержит выгрузка переписки

Выгрузка – это урезанная и отредактированная выдержка, а не стенограмма. До того как что-либо покинет MyBot:

  • Сохраняются не больше 50 последних сообщений. Более старая переписка обрезается, а не выгружается постранично.
  • Каждое сообщение обрезается до 4 000 символов, и сводка тоже.
  • Вложение превращается в буквальный текст [attachment] – без имени файла, без ссылки, без MIME-типа и размера. CRM узнаёт, что что-то было приложено, и ничего больше.
  • Авторы сводятся к двум ролям, customer и operator. Кто именно из операторов написал строку, наружу не уезжает.
  • Пустые сообщения отбрасываются, а не выгружаются пустыми строками.

Сверху накладываются лимиты провайдера: amoCRM держит склеенный текст в 6 000 символов, RetailCRM – в 2 000, OzmaCRM – в 32 000 байт.

Приватные заметки операторов не уезжают никогда. Это не фильтр, который можно неправильно настроить: путь выгрузки читает обычную ленту сообщений диалога и не имеет ни ветки кода, ни поля запроса, которые вели бы к таблице приватных заметок. Там просто нечего случайно приложить. Что такое приватная заметка – см. Поддержка.

Передача квалифицированного лида ИИ-агентом

ИИ-агент может сам передать квалифицированного лида в CRM через один закрытый инструмент – crm.sync_qualified_lead. Это инструмент с эффектом записи, поэтому он подчиняется обычным правилам подтверждения и идемпотентности агента.

Безопасным для модели его делают две границы:

  • Подключение должно быть явно разрешено этому агенту. Инструмент видит только те id подключений, что перечислены в политике инструментов ИИ-агента этого бота. Модель, назвавшая любое другое подключение – в том числе реальное и принадлежащее тому же боту, – получает отказ до того, как что-то выполнится.
  • Факты ограничены полями, которые уже настроены в маппинге. Модель может приложить не больше 20 фактов вида {key, value} (ключ до 128 символов, значение до 1 000). Каждый ключ обязан быть локальным путём, который текущая ревизия маппинга уже пишет наружу в поле клиента, сделки или заказа. Неизвестный ключ отклоняет весь вызов – его не отбрасывают молча и не принимают молча, потому что и то и другое отдало бы модели решение о том, что попадёт в вашу CRM.

Переписку модель не подставляет. Флаг export_conversation просит сервер приложить её, и сервер берёт её той же ограниченной и отредактированной выгрузкой, что описана выше: подписчик, бот и креды определяются на сервере.

Инструмент возвращает id операции и её статус, больше ничего.

Сделки, деньги и атрибуция

Деньги всегда представлены знаковой 64-битной минорной суммой плюс валютой ISO 4217, никогда числом с плавающей точкой. В полезной нагрузке провайдера едет только минорная сумма, валюта остаётся в канонической записи сделки. На входе денежный маппинг сохраняет минорную сумму и предупреждает, что валюта зафиксирована маппингом: если валюта может меняться, замапьте её отдельным полем.

Изменение сделки порождает событие клиента с именем crm.deal_created, crm.deal_updated, crm.deal_won или crm.deal_lost с локальным id, id клиента, воронкой, статусом и, когда они есть, минорной суммой и валютой. Выигранная сделка дополнительно даёт цель crm.deal_won для атрибуции с той же суммой и валютой.

Приватность и маскирование

  • Креды вообще нельзя сериализовать: тип кредов отказывается превращаться в JSON, поэтому токен не может случайно утечь через ответ, строку лога или полезную нагрузку задачи.
  • Креды и курсоры зашифрованы на диске и привязаны к одному подключению, боту, провайдеру, аккаунту, версии ключа и ревизии кредов. Шифртекст, перенесённый в другое место, не откроется, а не авторизует чужой аккаунт.
  • Чтение подключения отдаёт только безопасную сводку: id, бот, провайдер, имя, ключ аккаунта, статус, возможности, ревизия маппинга, код последней ошибки и отметки времени. Шифртекста кредов и материала маршрута вебхука в ней нет.
  • Метаданные запросов и ответов, которые хранятся для разбора проблем, маскируются: метод, схема, хост и путь, код статуса и подсказка для повтора. Любой заголовок, в имени которого есть authorization, token, secret, api-key, cookie или password, хранится как [redacted].
  • Выгрузки переписки – ограниченные выдержки с обезличенными ролями, а приватные заметки операторов исключены из них структурно.
  • Входящие изменения профиля аудируются вместе с происхождением, поэтому любое значение, записанное CRM в профиль, прослеживается до подключения и удалённой записи, откуда оно пришло.
  • Связи с CRM – отдельная категория хранения со сроком 730 дней; они попадают в выгрузку и удаление данных наравне с остальными категориями.

Состояние подключения, переподключение и отключение

Один бот может держать одно подключение на пару «провайдер + ключ аккаунта». Подключение создаётся в состоянии connected; набор состояний также допускает pending, expired, disabled и failed вместе со стабильным кодом последней ошибки.

Переподключение атомарно проворачивает запечатанные креды относительно ожидаемой ревизии, поэтому два одновременных переподключения не могут переплестись и оставить наполовину провёрнутое подключение. Так как печать привязана к ревизии, прежний шифртекст перестаёт открываться в момент фиксации нового.

Чтобы отключиться безопасно:

  1. Сначала отзовите кред на стороне CRM – доступ OAuth-приложения для amoCRM и Kommo, API-ключ для RetailCRM, OIDC-клиент для OzmaCRM.
  2. Уберите входящий путь, если настраивали его: подписку на вебхук в CRM.
  3. Удалите подключение – кнопкой «Удалить» на карточке подключения или запросом DELETE /api/bots/{botID}/crm/connections/{connectionID} (crm:write). Удаление подключения уносит с собой его ревизии маппинга, связи и курсоры и ничего не меняет внутри вашей CRM.

Журнальная сущность OzmaCRM принадлежит вам: её безопасно оставить и безопасно удалить, когда ни одно подключение не использует этот инстанс.

API

Все маршруты ниже находятся под /api/bots/{botID}/crm/…, если не указано иное, и всем дополнительно нужно право бота «Реакции». Схемы запросов и ответов – в Справочнике API.

Подключения

ЭндпоинтСкоупЧто делает
GET /connectionscrm:readсписок подключений (безопасные сводки)
GET /connections/{connectionID}crm:readсводка одного подключения
POST /connections/{provider}/startcrm:writeначать браузерный OAuth-сценарий, возвращает redirect_url. Сегодня только amoCRM, остальные провайдеры отвечают 503
GET /api/integrations/crm/{provider}/callbackцель браузерного редиректа; аутентификацией служит одноразовое состояние, токен не нужен
POST /connections/retailcrmcrm:writeподключить RetailCRM по API-ключу
POST /connections/ozmacrmcrm:writeподключить OzmaCRM по OIDC-кредам, логину и конфигурации сущностей
PATCH /connections/{connectionID}crm:writeпереименовать
DELETE /connections/{connectionID}crm:writeотключить и удалить маппинги, связи и курсоры
POST /connections/{connectionID}/testcrm:writeзаново прогнать проверки связи и предварительных условий

Маппинг и предпросмотр

ЭндпоинтСкоупЧто делает
GET /connections/{connectionID}/fieldscrm:readобнаруженные удалённые поля
GET /connections/{connectionID}/pipelinescrm:readворонки и их статусы
GET /connections/{connectionID}/mappingcrm:readтекущая ревизия маппинга
PUT /connections/{connectionID}/mappingcrm:writeсохранить новую неизменяемую ревизию
POST /connections/{connectionID}/previewcrm:readспроецировать маппинг на реальные профили – намеренно чтение: удалённой записи не происходит

Синхронизация

ЭндпоинтСкоупЧто делает
POST /connections/{connectionID}/initial-synccrm:writeвыгрузить профили подписчиков наружу
POST /connections/{connectionID}/reconcilecrm:writeпринудительная догоняющая сверка
GET /operationscrm:readоперации, новейшие первыми, keyset-пагинация
GET /operations/{operationID}crm:readодна операция и её задачи
POST /operations/{operationID}/cancelcrm:writeотменить выполняющуюся операцию
POST /jobs/{jobID}/retrycrm:writeповторить одну упавшую задачу
GET /conflictscrm:readзадачи, ждущие ручного разбора
POST /conflicts/{jobID}/resolvecrm:writeразрешить один конфликт

Действия из диалога (subscribers:read / subscribers:write, право бота «Диалоги», см. раздел «Действия из диалога» выше) – это GET|POST /api/bots/{botID}/subscribers/{subscriberID}/crm/status|sync|export-conversation. В Справочнике API их пока нет.

Инструменты MCP

По MCP опубликованы шесть инструментов только для чтения, всем нужен crm:read:

  • list_crm_connections – подключения бота с провайдером, статусом и возможностями.
  • get_crm_connection – безопасная сводка одного подключения.
  • get_crm_mapping – текущая ревизия маппинга подключения.
  • list_crm_operations – последние операции синхронизации, новейшие первыми, при желании в рамках одного подключения.
  • get_crm_operation – одна операция с её задачами (без сырой полезной нагрузки запроса задачи).
  • list_crm_conflicts – задачи, ждущие ручного разбора конфликта.

Ни один из них никогда не возвращает креды. Инструментов записи для CRM нет намеренно: подключение, синхронизация, повтор и разрешение конфликта остаются только в REST, поэтому агент может наблюдать за CRM-интеграцией, но не может менять её поведение.

Известные ограничения

Записаны здесь, чтобы никто не открывал их заново как баги:

  • Bitrix24 подключить нельзя – эндпоинт подключения отвечает 503.
  • Метки MyBot не уезжают в CRM тегами; входящее сопоставление тегов работает.
  • В предпросмотре и первичной синхронизации список сделок всегда пуст – своего объекта «сделка» у MyBot нет.
  • Приём вебхуков пока не самостоятельный: адрес маршрута подключения нигде не выдаётся, поэтому по факту все провайдеры работают на опросе, а токен вебхука RetailCRM нельзя задать в теле подключения.
  • У OzmaCRM нет триггера реального времени – только опрос.
  • У повторной сверки нет кнопки в кабинете, только API.
  • У действий CRM по подписчику нет интерфейса; они доступны только по API и отсутствуют в Справочнике API.

Смежные страницы

  • Интеграции – другие внешние сервисы, которые можно подключить к боту.
  • Источники и ключи – REST- и OzmaDB-коннекторы для прямого чтения и записи данных.
  • Веб-запросы и вебхуки – входящий и исходящий HTTP.
  • Люди и профили – профили, которые читает и пишет маппинг CRM.
  • Поддержка – переписка, из которой собирается выгрузка, и приватные заметки.
  • ИИ-агент – агент, который может передать квалифицированного лида.
  • MCP – шесть инструментов CRM только для чтения.
  • Аналитика – куда попадает выигранная сделка как цель.