Привет, Хабр! Меня зовут Иван, я битрикс-разработчик.
Заходят как-то в бар Битрикс24 и Диадок — и быстро выясняется, что работать вместе им пока сложно. Битрикс24 отвечает за сделки и коммуникации, Диадок — за документы и статусы. А бизнесу нужна единая цепочка: документ появился в Диадоке — менеджер сразу видит его в CRM без переключений и ручных сверок.
Рассказываю, как мы реализовали такую интеграцию с нуля. Настроили автоматическую передачу документов из Диадока в Битрикс24. Документы создают лиды, а статусы, файлы и история изменений отображаются в CRM.
Задача
Компания — производитель спецтехники. В работе используются три системы:
-
ящик Диадок с полным документооборотом;
-
облачный Битрикс24;
-
собственный сайт.
Необходимо настроить автоматическую передачу входящих и исходящих документов из Диадока в Битрикс24, чтобы они сразу превращались в лиды. Менеджеры должны видеть статусы, историю изменений и файлы прямо в CRM, без переключения между сервисами.
Для интеграции рассматривали два варианта: создать отдельное приложение для Битрикс24 или использовать сайт как посредник и настроить обмен через вебхуки.
Мы выбрали второй вариант. Он оказался быстрее в реализации и проще в поддержке, а сайт получил роль связующего звена между Диадоком и CRM.
Как мы реализовали интеграцию
Интеграцию разделили на три этапа:
1. Описали структуру данных через Protocol Buffers. Определили, какие данные нужны от Диадока: статусы, документы и события. Создали proto-файлы и перевели их с синтаксиса proto2 на актуальный proto3. Это позволило корректно компилировать структуры в PHP-классы.
2. Настроили API для получения данных из Диадока. На основе официальной документации сформировали REST-запросы для загрузки данных из ящика клиента. Реализовали авторизацию, работу с токенами и фильтрацию по времени и событиям.
3. Связали сайт и Битрикс24 через вебхуки. На сайте настроили отправку данных в облачный Битрикс24 через REST API. Реализовали:
-
создание новых лидов;
-
обновление существующих;
-
добавление комментариев о изменениях документов.
Каждый объект CRM проходит сравнение с предыдущим состоянием — обновляем только изменившиеся данные, чтобы не перегружать систему.
Точки риска подхода
Прежде чем интеграция заработала стабильно, пришлось разобрать несколько технических моментов. Они могли вызвать ошибки в работе системы и потребовали отдельной проработки:
-
Формат обмена данными.
Для передачи данных использовали Protocol Buffers — компактный формат, который рекомендует сам Диадок. Он поддерживает обратную совместимость, а значит, более стабильный и надежный, чем JSON.
Сложность заключалась в том, что документация Диадока использовала синтаксис proto2, тогда как в официальных версиях компилятора Protocol Buffers поддержка PHP для этой версии уже отсутствовала. Поэтому структуру данных пришлось адаптировать под актуальный формат. После обновления данные начали корректно собираться и передаваться, и интеграция заработала стабильно.
Работа с API.
У API Диадока несколько версий, и не все методы содержат одинаковые данные.
Ключевым требованием было корректное отображение статусов документов. В методе /V3/GetDocflowEvents на момент разработки нужного поля не было. Поддержка Диадока подтвердила, что в новой версии оно пока не предусмотрено.
Мы временно использовали метод /V2/GetDocflowEvents, где это поле есть. Позже разработчики добавили итоговый статус и в версию V3.
Скрытый текст
<?php namespace O2k\Diadoc\Integration;class Api{private const API_KEY = '';private const SERVICE_URL = 'https://diadoc-api.kontur.ru';private const RESOURCE_AUTHENTICATE = '/V3/Authenticate';private const RESOURCE_GET_DOCFLOWS_EVENTS_V2 = '/V2/GetDocflowEvents';private const RESOURCE_GET_DOCFLOWS_EVENTS_V3 = '/V3/GetDocflowEvents';private const RESOURCE_GET_DOCUMENT = '/V3/GetDocument';private const RESOURCE_GET_DOCUMENT_TYPES = '/V2/GetDocumentTypes';private const RESOURCE_GET_BOX = '/GetBox';private const RESOURCE_PARSE_TITLE_XML = '/ParseTitleXml';private const RESOURCE_GET_ENTITY_CONTENT = '/V4/GetEntityContent';private const RESOURCE_GENERATE_PRINT_FORM_FROM_ATTACHMENT = '/GeneratePrintFormFromAttachment';private const RESOURCE_GET_GENERATED_PRINT_FORM = '/GetGeneratedPrintForm';private const RESOURCE_GET_MESSAGE = '/V5/GetMessage';const METHOD_GET = 'GET'; const METHOD_POST = 'POST';private $token;public function getDocflowEventsV3($boxId,$startTimestamp,$endTimestamp,$sortDirection = 1,$afterIndexKey = null,$populateDocuments = false,$injectEntityContent = false,$populatePreviousDocumentStates = false) {if (!$boxId) {return false;}$uriParameters = ['boxId' => $boxId,];$startProtoTimestamp = new \Diadoc\Api\Proto\Timestamp(['Ticks' => \O2k\DateTime\Helper::convertTimestampToTicks($startTimestamp)]);$endProtoTimestamp = new \Diadoc\Api\Proto\Timestamp(['Ticks' => \O2k\DateTime\Helper::convertTimestampToTicks($endTimestamp)]);$protoFilter = new \Diadoc\Api\Proto\TimeBasedFilter();$protoFilter->setFromTimestamp($startProtoTimestamp);$protoFilter->setToTimestamp($endProtoTimestamp);$protoFilter->setSortDirection($sortDirection);$getDocflowEventsRequest = new \Diadoc\Api\Proto\Docflow\GetDocflowEventsRequest(['Filter' => $protoFilter,'AfterIndexKey' => $afterIndexKey,'PopulateDocuments' => $populateDocuments,'InjectEntityContent' => $injectEntityContent,'PopulatePreviousDocumentStates' => $populatePreviousDocumentStates]);$serializedProtoData = $getDocflowEventsRequest->serializeToString();$response = $this->doRequest( self::RESOURCE_GET_DOCFLOWS_EVENTS_V3, [ 'boxId' => $boxId, ],'POST',$serializedProtoData );$docflowEvents = new \Diadoc\Api\Proto\Docflow\GetDocflowEventsResponseV3;$docflowEvents->mergeFromString($response);return $docflowEvents; }public function getDocflowEventsV2($boxId,$startTimestamp,$endTimestamp,$sortDirection = 1,$afterIndexKey = null,$populateDocuments = false,$injectEntityContent = false,$populatePreviousDocumentStates = false) {if (!$boxId) {return false;}$uriParameters = ['boxId' => $boxId,];$startProtoTimestamp = new \Diadoc\Api\Proto\Timestamp(['Ticks' => \O2k\DateTime\Helper::convertTimestampToTicks($startTimestamp)]);$endProtoTimestamp = new \Diadoc\Api\Proto\Timestamp(['Ticks' => \O2k\DateTime\Helper::convertTimestampToTicks($endTimestamp)]);$protoFilter = new \Diadoc\Api\Proto\TimeBasedFilter();$protoFilter->setFromTimestamp($startProtoTimestamp);$protoFilter->setToTimestamp($endProtoTimestamp);$protoFilter->setSortDirection($sortDirection);$getDocflowEventsRequest = new \Diadoc\Api\Proto\Docflow\GetDocflowEventsRequest(['Filter' => $protoFilter,'AfterIndexKey' => $afterIndexKey,'PopulateDocuments' => $populateDocuments,'InjectEntityContent' => $injectEntityContent,'PopulatePreviousDocumentStates' => $populatePreviousDocumentStates]);$serializedProtoData = $getDocflowEventsRequest->serializeToString();$response = $this->doRequest( self::RESOURCE_GET_DOCFLOWS_EVENTS_V2, [ 'boxId' => $boxId, ],'POST',$serializedProtoData );$docflowEvents = new \Diadoc\Api\Proto\Docflow\GetDocflowEventsResponse;$docflowEvents->mergeFromString($response);return $docflowEvents; }public function getMessage($boxId, $messageId, $entityId, $originalSignature = false, $injectEntityContent = false){$requestData = ['boxId' => $boxId,'messageId' => $messageId,'entityId' => $entityId];if ($originalSignature) {$requestData['originalSignature'] = $originalSignature;}if ($injectEntityContent) {$requestData['injectEntityContent'] = $injectEntityContent;}$response = $this->doRequest( self::RESOURCE_GET_MESSAGE, $requestData );$message = new \Diadoc\Api\Proto\Events\Message;$message->mergeFromString($response); return $message;}public function getEntityContent($boxId, $messageId, $entityId){$response = $this->doRequest( self::RESOURCE_GET_ENTITY_CONTENT, [ 'boxId' => $boxId, 'messageId' => $messageId, 'entityId' => $entityId ] ); return $response;}public function generatePrintFormFromAttachment($documentType, $fromBoxId, $documentContent){$response = $this->doRequest( self::RESOURCE_GENERATE_PRINT_FORM_FROM_ATTACHMENT, [ 'documentType' => $documentType, 'fromBoxId' => $fromBoxId ],'POST',$documentContent ); return $response;}public function getGeneratedPrintForm($printFormId){$response = $this->doRequest( self::RESOURCE_GET_GENERATED_PRINT_FORM, [ 'printFormId' => $printFormId, ] ); return $response;}public function parseTitleXml($boxId, $documentTypeNamedId, $documentFunction, $documentVersion, $titleIndex, $xmlFileContent){$response = $this->doRequest( self::RESOURCE_PARSE_TITLE_XML, [ 'boxId' => $boxId, 'documentTypeNamedId' => $documentTypeNamedId, 'documentFunction' => $documentFunction, 'documentVersion' => $documentVersion, 'titleIndex' => $titleIndex ],'POST',$xmlFileContent ); return $response;}public function getDocumentTypes($boxId) {$response = $this->doRequest(self::RESOURCE_GET_DOCUMENT_TYPES,['boxId' => $boxId,]);$documentTypes = new \Diadoc\Api\Proto\Documents\Types\GetDocumentTypesResponseV2();$documentTypes->mergeFromString($response); return $documentTypes;}public function getBox($boxId) { $response = $this->doRequest( self::RESOURCE_GET_BOX, [ 'boxId' => $boxId ] );$box = new \Diadoc\Api\Proto\Box();$box->mergeFromString($response); return $box; }public function authenticateByPassword($login, $password) {$uriParameters = ['type' => 'password',];$protoData = new \Diadoc\Api\Proto\LoginPassword(['Login' => $login, 'Password' => $password]);$serializedProtoData = $protoData->serializeToString();$response = $this->doRequest( self::RESOURCE_AUTHENTICATE, $uriParameters,'POST',$serializedProtoData ); $this->setToken($response); return $response; }protected function getUri($action, $params = []){$uri = self::SERVICE_URL.$action;if ($params) {$uri .= '?'.http_build_query($params);}return $uri;}protected function doRequest($resource, $params = [], $method = self::METHOD_GET, $data = array()) {$uri = sprintf( '%s%s?%s', self::SERVICE_URL, $resource, http_build_query($params) ); $ch = curl_init($uri); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);curl_setopt($ch, CURLOPT_TIMEOUT, 180); curl_setopt($ch, CURLOPT_HTTPHEADER, $this->buildRequestHeaders()); if ($method == self::METHOD_POST) { curl_setopt($ch, CURLOPT_POST, 0); curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data); } elseif ($method == self::METHOD_GET) { curl_setopt($ch, CURLOPT_HTTPGET, 1); curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1); } $response = curl_exec($ch); curl_close($ch); return $response; } protected function getToken() { return $this->token; } public function setToken($token) { $this->token = $token; }protected function buildRequestHeaders() { $header = 'DiadocAuth ddauth_api_client_id='.self::API_KEY; if ($token = $this->getToken()) { $header .= ', ddauth_token='.$token; } return ['Authorization: ' . $header]; }}
Скрипт без скрипа
Интеграция работает как автоматический цикл, который запускается каждые пять минут. За один такой цикл система проверяет новые события в документообороте и актуализирует данные в CRM. Алгоритм выглядит так:
1. Запуск по расписанию. На сервере каждые пять минут запускается PHP-скрипт, который обращается к API Диадока и Битрикс24.
Скрытый текст
$diadocEngine = new O2k\Diadoc\Integration\Api;$controller = new \O2k\Diadoc\Integration\Bitrix24\Controller($diadocEngine);if ($eventsData = $controller->getDocflowEventsV2Data()) {$arExternalData = $controller->getExternalView($eventsData);if ($arExternalData) {$arExistingLeads = $controller->getLeadList(array_keys($arExternalData));$existingExternalIds = array_keys($arExistingLeads);$result = $controller->prepareData($arExternalData, $arExistingLeads);foreach ($result as $externalId => $arLeadData) {if (in_array($externalId, $existingExternalIds)) {$controller->updateLead($arExistingLeads[$externalId], $arLeadData);} else {$controller->addLead($arLeadData);}}}}
2. Получение новых событий из Диадока. Через REST API загружается список событий за последние пять минут: отправка, подписание, согласование, отклонение и другие статусы.
3. Проверка документа в Битрикс24. Каждый объект проверяется по внешнему коду. Если лид уже существует — переходим к сравнению данных. Если лида нет — создаем новый.
4. Сравнение состояния документа. При обработке событий сравниваем текущее и предыдущее состояние документа, переданные Диадоком. Обновляем данные только при фактических изменениях статуса, наличия файла, без избыточных запросов.
5. Принятие решения. Если документ новый — создается лид с заполнением всех полей. Если документ изменился — либо обновляем данные лида, либо добавляем комментарий о событии.
6. Комментарии в ленте. Все ключевые изменения — подписан, отправлен, на согласовании, отклонен — фиксируются в виде комментариев внутри лида. Менеджер видит историю без перехода в Диадок.
7. Хэширование данных. Чтобы не перегружать API, используем хэши. Если данные не изменились — обновления не отправляются. Комментарии это не затрагивает.
В итоге CRM получает только актуальные данные, а документы обновляются без задержек. Менеджеры работают с полной и достоверной картиной, не переключаясь между системами и не тратя время на ручные проверки.




Результаты
Интеграцию сделали без готовых модулей и платформ. Сайт стал рабочей прослойкой между документооборотом и CRM со стабильной и предсказуемой передачей данных.
Интеграция оказалась полезной и для продаж, и для IT-отдела. Выделили основные эффекты:
-
Автоматизация документооборота. Документы из Диадока автоматически превращаются в лиды в CRM с полной историей изменений.
-
Снижение операционных затрат. Больше практически нет ручной работы по копированию, отслеживанию и обновлению документов.
-
Прозрачность процессов. Статусы и комментарии отображаются прямо в карточке. Менеджеры видят всю хронологию.
-
Быстрая обработка. Время между поступлением документа и реакцией команды заметно сократилось.
-
Оптимальная нагрузка на API. Точечное обновление исключило лишние запросы.
-
Устойчивость к сбоям. Если соединение пропадает, система догружает пропущенные события при следующем запуске.
-
Готовность к развитию. Интеграцию можно масштабировать: подключать дополнительные ящики и менять логику обработки без переписывания всего решения.
Если в компании используются Диадок и CRM, но они работают отдельно, их можно объединить в единую систему. Такой подход упрощает процессы, снижает количество ручных операций и уменьшает вероятность ошибок. Если нужна консультация по подобной интеграции, мы готовы обсудить задачу и предложить техническое решение.
ссылка на оригинал статьи https://habr.com/ru/articles/1023702/