Привет, Хабр! Меня зовут Иван, я битрикс-разработчик.
Заходят как-то в бар Битрикс24 и Диадок — и быстро выясняется, что работать вместе им пока сложно. Битрикс24 отвечает за сделки и коммуникации, Диадок — за документы и статусы. А бизнесу нужна единая цепочка: документ появился в Диадоке — менеджер сразу видит его в CRM без переключений и ручных сверок.
Рассказываю, как мы реализовали такую интеграцию с нуля. Настроили автоматическую передачу документов из Диадока в Битрикс24. Документы создают лиды, а статусы, файлы и история изменений отображаются в CRM.
Задача
Компания — производитель спецтехники. В работе используются три системы:
ящик Диадок с полным документооборотом;
облачный Битрикс24;
собственный сайт.
Необходимо настроить автоматическую передачу входящих и исходящих документов из Диадока в Битрикс24, чтобы они сразу превращались в лиды. Менеджеры должны видеть статусы, историю изменений и файлы прямо в CRM, без переключения между сервисами.
Для интеграции рассматривали два варианта: создать отдельное приложение для Битрикс24 или использовать сайт как посредник и настроить обмен через вебхуки.
Мы выбрали второй вариант. Он оказался быстрее в реализации и проще в поддержке, а сайт получил роль связующего звена между Диадоком и CRM.
Как мы реализовали интеграцию
Интеграцию разделили на три этапа:
Описали структуру данных через Protocol Buffers. Определили, какие данные нужны от Диадока: статусы, документы и события. Создали proto-файлы и перевели их с синтаксиса proto2 на актуальный proto3. Это позволило корректно компилировать структуры в PHP-классы.
Настроили API для получения данных из Диадока. На основе официальной документации сформировали REST-запросы для загрузки данных из ящика клиента. Реализовали авторизацию, работу с токенами и фильтрацию по времени и событиям.
-
Связали сайт и Битрикс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. Алгоритм выглядит так:
-
Запуск по расписанию. На сервере каждые пять минут запускается 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); } } } } Получение новых событий из Диадока. Через REST API загружается список событий за последние пять минут: отправка, подписание, согласование, отклонение и другие статусы.
Проверка документа в Битрикс24. Каждый объект проверяется по внешнему коду. Если лид уже существует — переходим к сравнению данных. Если лида нет — создаем новый.
Сравнение состояния документа. При обработке событий сравниваем текущее и предыдущее состояние документа, переданные Диадоком. Обновляем данные только при фактических изменениях статуса, наличия файла, без избыточных запросов.
Принятие решения. Если документ новый — создается лид с заполнением всех полей. Если документ изменился — либо обновляем данные лида, либо добавляем комментарий о событии.
Комментарии в ленте. Все ключевые изменения — подписан, отправлен, на согласовании, отклонен — фиксируются в виде комментариев внутри лида. Менеджер видит историю без перехода в Диадок.
Хэширование данных. Чтобы не перегружать API, используем хэши. Если данные не изменились — обновления не отправляются. Комментарии это не затрагивает.
В итоге CRM получает только актуальные данные, а документы обновляются без задержек. Менеджеры работают с полной и достоверной картиной, не переключаясь между системами и не тратя время на ручные проверки.




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