Yandex.Cloud как хостинг локальных интеграций для Битрикс24
Привет! Меня зовут Сергей Востриков, я руковожу направлением Маркетплейс и интеграций в Битрикс – помогаю развивать REST API и всё «вокруг» него — документацию, витрину Битрикс24 Маркет, кабинет разработчика решений и т.д.
Много общаясь с разработчиками самого разного уровня в чатах по REST API Битрикс24, я часто наблюдаю похожие ситуации: начинающий разработчик, пытаясь решить практическую задачу на REST API, вдруг обнаруживает, что ему нужен сервер или shared-хостинг для работы его бэкенда на PHP и Python.
«Я написал приложение для Битрикс24, а как мне теперь это выгрузить в Битрикс24, чтобы оно работало?» – типичный вопрос. И типичный ответ – никак, если только твоё приложение не является набором статичных html/js файлов, и вся его логика фактически реализуется на уровне фронтенда. Вот такие приложения действительно можно «хостить в облаке Битрикс24», но это сильно ограничивает разработчика в перечне возможных сценариев.
Если хочется сделать встройку виджета в интерфейс Битрикс24, если нужно обрабатывать события REST, если нужно добавить своего робота или триггер в CRM, и многое другое – без бэкенда не обойтись.
И, конечно, не очень радостно обнаружить, что для решения даже такой простой задачи, как, например, автоматическое форматирование телефонных номеров в нужной нам логике, оказывается надо заморачиваться с хостингом, доменами и прочей связанной с этим механикой. За этим надо следить, чистить логи, придумывать бэкапирование и вообще решить целую кучу связанных вопросов, на решение которых у конкретного разработчика Васи (да простят меня все глубокоуважаемые Василии планеты за выбранный пример имени) может и не быть опыта и компетенций.
А что, если за нас с Васей всё уже придумали? Давайте возьмём типичную задачу форматирования телефонных номеров в контактах Битрикс24 (будем добавлять код страны “+7” ко всем номерам, у которых его нет) и решим её так, чтобы нам не пришлось сильно заморачиваться с хостингом?
Функция в облаке
Нам на помощь придёт концепция serverless-функций. Если не вдаваться в глубокие подробности, а они нам с Васей и не нужны, то концепция состоит в том, что мы создадим некий код, который с помощью обычного REST API Битрикс24 будет служить обработчиком событий на создание и изменений контакта, вытаскивать из Битрикс24 номера телефонов, указанных в контакте, форматировать их и отправлять обратно. И этот код будет “лежать” в облаке Yandex. Когда событие на стороне Битрикс24 произойдет (например, кто-то добавит контакт в CRM), Yandex.Cloud пошуршит внутренностями, “поднимет” нужное серверное окружение для выполнения нашего кода, а когда он будет выполнен, то все серверные ресурсы будут освобождены сами собой волшебным образом.
Это означает, что в отличие от shared-хостинга или аренды сервера мы будем платить только за фактическое использование ресурсов. Если сегодня в нашей CRM никто новые контакты не внес или не изменил существующие, то и денег мы за наш обработчик события никому должны не будем. Скажу даже больше, если я правильно интерпретирую официальный прайс-лист, существующий на момент написания статьи, то для небольших компаний, которые не ворочают десятками тысяч контактов в Битрикс24, может оказаться, что использование функция вообще окажется бесплатным. Учтите, данное предположение не является инвестиционной рекомендацией, лучше уточняйте подробности в Яндекс.
С этой оговоркой про деньги вернёмся к организационным и техническим вопросам.
Регистрация и первые шаги
Для начала нам потребуется зарегистрироваться в https://console.yandex.cloud/. Если у нас есть ящик в Яндексе, то процедура пройдёт максимально прозрачно. Внутри нужно будет создать “каталог”.
Далее потребуется создать платежный эккаунт https://center.yandex.cloud/billing/accounts/.
Таки да, надо привязать свою карточку для потенциальной оплаты потраченных ресурсов. На момент написания статьи Яндекс при создании платежного аккаунта дал грант на 4000 рублей для тестирования платных сервисов. По логике вещей, они не пригодятся нам непосредственно для функций, но вдруг пригодятся для другого? Берём!
Зайдя в каталог, мы можем сразу добавить Функцию. Вообще, все эти шаги, в целом, неплохо описаны в документации, так что я здесь не будут останавливаться на подробностях опций функции – умолчания прекрасно сработали сами по себе.
Волшебная функция
Когда мы создадим “заготовку” функции, то увидим нечто вроде
<?php
function handler($event, $context)
{
return [
"statusCode" => 200,
"body" => "Hello, World!",
];
}
Давайте слегка поменяем этот код, воткнув туда логирование данных, которые нам пришли в параметре $event. Ведь именно там мы получим сигнал от Битрикс24, какой именно контакт был добавлен или обновлен, но мы пока не знаем, в каком именно виде эта информация будет получена.
<?php
function handler($event, $context)
{
fwrite(STDERR, json_encode($event, true));
return [
"statusCode" => 200,
"body" => "Event triggered",
];
}
Выше я написал, что не буду описывать опции функции. Я наврал! Нам важно будет включить логирование:
После сохранения новой функции нас перенаправят на страницу с общими настройками, где нам обязательно нужно будет включить опцию “Публичная функция”:
Теперь самое время пойти в Битрикс24 в раздел Разработчикам:
Выбрать группу сценариев “Интегрировать с внешними системами”, а затем выбрать сценарий “Синхронизировать контрагентов”:
Это создаст нам стандартную заготовку, которая прекрасно подойдет для нашей задачи, а именно – входящий вебхук для выполнения методов REST API и настройки для указания обработчика исходящего вебхука:
Как видно на скриншоте, я вставил адрес функции в Yandex.Cloud в качестве обработчика событий ONCRMCONTACTUPDATE и ONCRMCONTACTADD. Обратите внимание на скоуп CRM в полей “Настройка прав” – без этого у нас не получится обращаться к методам crm.* REST API.
Сохраняем настроенный сценарий интеграции, переходим в список контактов CRM, открываем любой контакт и меняем в нём, ну, например, фамилию:
Через некоторое время (логи обновляются не сразу, но довольно быстро) вернёмся в интерфейс нашей функции и перейдем на закладку Логи:
Нас интересует первая (хотя технически она скорее последняя, но мы же не настоящие сварщики) строка:
Внутри лежит довольно богатый набор данных:
{
"httpMethod": "POST",
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "380",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "functions.yandexcloud.net",
"Uber-Trace-Id": "da4fb201e4f1270b:46209c42ce34c869:da4fb201e4f1270b:1",
"User-Agent": "Bitrix24 Webhook Engine",
"X-Forwarded-For": "52.29.163.104",
"X-Real-Remote-Address": "[52.29.163.104]:48370",
"X-Request-Id": "c8f3b25f-0f42-4a09-b1f9-15899bddc952",
"X-Trace-Id": "a0a6cab4-166f-4ee6-91e6-d422bbde7692"
},
"url": "",
"params": [],
"multiValueParams": [],
"pathParams": [],
"multiValueHeaders": {
"Accept-Encoding": [
"gzip"
],
"Content-Length": [
"380"
],
"Content-Type": [
"application/x-www-form-urlencoded"
],
"Host": [
"functions.yandexcloud.net"
],
"Uber-Trace-Id": [
"da4fb201e4f1270b:46209c42ce34c869:da4fb201e4f1270b:1"
],
"User-Agent": [
"Bitrix24 Webhook Engine"
],
"X-Forwarded-For": [
"52.29.163.104"
],
"X-Real-Remote-Address": [
"[52.29.163.104]:48370"
],
"X-Request-Id": [
"c8f3b25f-0f42-4a09-b1f9-15899bddc952"
],
"X-Trace-Id": [
"a0a6cab4-166f-4ee6-91e6-d422bbde7692"
]
},
"queryStringParameters": [],
"multiValueQueryStringParameters": [],
"requestContext": {
"identity": {
"sourceIp": "52.29.163.104",
"userAgent": "Bitrix24 Webhook Engine"
},
"httpMethod": "POST",
"requestId": "c8f3b25f-0f42-4a09-b1f9-15899bddc952",
"requestTime": "23/Apr/2025:14:16:21 +0000",
"requestTimeEpoch": 1745417781
},
"body": "ZXZlbnQ9T05DUk1DT05UQUNUVVBEQVRFJmV2ZW50X2hhbmRsZXJfaWQ9NTgzOSZkYXRhJTVCRklFTERTJTVEJTVCSUQlNUQ9MTM2MDcmdHM9MTc0NTQxNzc4MCZhdXRoJTVCZG9tYWluJTVEPXJlc3RhcGkuYml0cml4MjQucnUmYXV0aCU1QmNsaWVudF9lbmRwb2ludCU1RD1odHRwcyUzQSUyRiUyRnJlc3RhcGkuYml0cml4MjQucnUlMkZyZXN0JTJGJmF1dGglNUJzZXJ2ZXJfZW5kcG9pbnQlNUQ9aHR0cHMlM0ElMkYlMkZvYXV0aC5iaXRyaXguaW5mbyUyRnJlc3QlMkYmYXV0aCU1Qm1lbWJlcl9pZCU1RD1kYTQ1YTAzYjI2NWVkZDg3ODdmOGEyNThkNzkzY2M1ZCZhdXRoJTVCYXBwbGljYXRpb25fdG9rZW4lNUQ9cHc1ZXR4ZjJkYjgzdzBzeGZ5N3RxZXo1bWw0dmhlbWQ=",
"isBase64Encoded": true
}
Он очень страшный и там много полезной информации, которая нам не нужна. Что нас действительно интересует, так это ключ body. Именно тут лежат данные, полученные от Битрикс24.
Исключительно для нашего удобства Яндекс кодирует эти данные в base64. А мог бы заморочиться шифрованием на базе алгоритмов с открытым ключом! Спасибо вам, добрые люди!
Если раскодировать эти данные, то мы получим строку вида:
event=ONCRMCONTACTUPDATE&event_handler_id=5839&data%5BFIELDS%5D%5BID%5D=13607&ts=1745417780&auth%5Bdomain%5D=restapi.bitrix24.ru&auth%5Bclient_endpoint%5D=https%3A%2F%2Frestapi.bitrix24.ru%2Frest%2F&auth%5Bserver_endpoint%5D=https%3A%2F%2Foauth.bitrix.info%2Frest%2F&auth%5Bmember_id%5D=da45a03b265edd8787f8a258d793cc5d&auth%5Bapplication_token%5D=pw5etxf2db83w0sxfy7tqez5ml4vhemd
Или, если совсем по красоте, то
event=ONCRMCONTACTUPDATE&event_handler_id=5839&data[FIELDS][ID]=13607&ts=1745417780&auth[domain]=restapi.bitrix24.ru&auth[client_endpoint]=https://restapi.bitrix24.ru/rest/&auth[server_endpoint]=https://oauth.bitrix.info/rest/&auth[member_id]=da45a03b265edd8787f8a258d793cc5d&auth[application_token]=pw5etxf2db83w0sxfy7tqez5ml4vhemd
Вы видите в этих данных самое ключевое – идентификатор контакта, который был обновлен. Это всё, что нам нужно для решения нашей практической задачи – получения и обновления телефонных номеров.
Перепишем нашу функцию следующим образом:
<?php
define('WEBHOOK_URL', 'https://xxx.bitrix24.ru/rest/1/super-hidden-code/');
function handler($event, $context): array
{
// 1. Проверка метода
if (($event['httpMethod'] ?? '') !== 'POST') {
return [
'statusCode' => 405,
'body' => 'Method Not Allowed'
];
}
// 2. Декодируем тело из base64
$decodedBody = base64_decode($event['body'] ?? '');
parse_str($decodedBody, $parsed); // Получим ассоциативный массив
// 3. Проверка события
$eventType = $parsed['event'] ?? '';
if (!in_array($eventType, ['ONCRMCONTACTADD', 'ONCRMCONTACTUPDATE'])) {
return [
'statusCode' => 400,
'body' => 'Unsupported or missing event type'
];
}
$contactId = $parsed['data']['FIELDS']['ID'] ?? null;
if (!$contactId) {
return [
'statusCode' => 400,
'body' => 'Missing contact ID'
];
}
// 4. Получение контакта
$contact = callBitrix('crm.contact.get', ['id' => $contactId]);
if (!$contact || empty($contact['result'])) {
return [
'statusCode' => 404,
'body' => 'Contact not found'
];
}
$phones = $contact['result']['PHONE'] ?? [];
$updatedPhones = [];
$changed = false;
foreach ($phones as $entry) {
$original = $entry['VALUE'];
$digits = preg_replace('/\D/', '', $original);
// Обновим, если не начинается с + и не международный формат
if (strpos($original, '+7') !== 0 && strpos($original, '+') !== 0) {
$changed = true;
$entry['VALUE'] = '+7' . ltrim($digits, '78');
}
$updatedPhones[] = $entry;
}
// 5. Обновление при необходимости
if ($changed) {
$update = callBitrix('crm.contact.update', [
'id' => $contactId,
'fields' => ['PHONE' => $updatedPhones]
]);
if (!empty($update['error'])) {
return [
'statusCode' => 500,
'body' => 'Failed to update contact: ' . ($update['error_description'] ?? 'Unknown error')
];
}
return [
'statusCode' => 200,
'body' => 'Contact updated successfully'
];
}
return [
'statusCode' => 200,
'body' => 'No update needed'
];
}
function callBitrix(string $method, array $params = []): array
{
$url = WEBHOOK_URL . $method . '.json';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_POST, true);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
return $error ? ['error' => $error] : json_decode($response, true);
}
Не забудьте вставить путь к своему входящему вебхуку в первом DEFINE. Это код можно получить, зайдя на страницу Интеграции в разделе Разработчикам и открыв ту самую интеграцию “Синхронизировать контрагентов”, которую мы создали ранее.
Код, который приведен выше, на самом деле, довольно простой – он аккуратно разбирает то, что пришло в параметре $event, и, если видит, что это наши события на обновление контакта или на появление нового, то сначала методом crm.contact.get получает номера телефонов, потом нехитрыми манипуляциями добавляет “+7” в начало номеров и, наконец, обновляет контакт методом crm.contact.update.
Надо отметить, что приведенный код не использует никакие продвинутые вещи вроде B24PHPSDK и остаётся открытым вопрос, насколько функции Yandex.Cloud получится использовать в более сложных сценариях типа встройки виджетов. Кажется, что очевидных препятствий нет, но вероятнее всего реализация потребует более глубокого погружения в особенности настройки и использования Yandex.Cloud.