Битрикс

Синхронизация остатков и цен из Коробки Битрикс24 на сайт 1С-Битрикс — пошаговое руководство

Синхронизация остатков и цен из Коробки Битрикс24 на сайт 1С-Битрикс — пошаговое руководство

{ “@context”: “https://schema.org”, “@type”: “TechArticle”, “headline”: “Синхронизация остатков и цен из коробки Битрикс24 на сайт 1С-Битрикс: полное руководство”, “description”: “Подробная инструкция по настройке односторонней синхронизации товаров, остатков и цен из коробочного Битрикс24 в интернет-магазин на 1С-Битрикс. Готовый код на PHP, примеры, чек-лист.”, “image”: “https://automata.sale/images/blog/bitrix24-1c-bitrix-sync.png”, “datePublished”: “2026-03-13”, “dateModified”: “2026-03-13”, “author”: { “@type”: “Organization”, “name”: “AUTOMATA SALE”, “url”: “https://automata.sale” }, “publisher”: { “@type”: “Organization”, “name”: “AUTOMATA SALE”, “url”: “https://automata.sale”, “logo”: { “@type”: “ImageObject”, “url”: “https://automata.sale/images/logo.png” } }, “mainEntityOfPage”: { “@type”: “WebPage”, “@id”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/” }, “articleSection”: “Интеграции”, “keywords”: “синхронизация Битрикс24, остатки на сайт, цены из CRM, 1С-Битрикс интеграция, коробка Битрикс24”, “inLanguage”: “ru”, “proficiencyLevel”: “Intermediate”, “dependencies”: “Коробочный Битрикс24, 1С-Битрикс: Управление сайтом (Бизнес), PHP 7.4+”, “about”: [ { “@type”: “Thing”, “name”: “Битрикс24”, “url”: “https://www.bitrix24.ru/” }, { “@type”: “Thing”, “name”: “1С-Битрикс”, “url”: “https://www.1c-bitrix.ru/” } ] }

{ “@context”: “https://schema.org”, “@type”: “HowTo”, “name”: “Как настроить синхронизацию остатков и цен из Битрикс24 на сайт 1С-Битрикс”, “description”: “Пошаговая инструкция по настройке автоматической передачи цен и остатков товаров из коробочного Битрикс24 в интернет-магазин на 1С-Битрикс.”, “image”: “https://automata.sale/images/blog/bitrix24-1c-bitrix-sync.png”, “totalTime”: “PT2H”, “estimatedCost”: { “@type”: “MonetaryAmount”, “currency”: “RUB”, “value”: “0” }, “supply”: [ { “@type”: “HowToSupply”, “name”: “Коробочный Битрикс24 с модулем Каталог” }, { “@type”: “HowToSupply”, “name”: “Сайт на 1С-Битрикс: Управление сайтом (редакция Бизнес)” } ], “tool”: [ { “@type”: “HowToTool”, “name”: “SSH-доступ к обоим серверам” }, { “@type”: “HowToTool”, “name”: “Текстовый редактор или IDE” }, { “@type”: “HowToTool”, “name”: “Браузер для тестирования” } ], “step”: [ { “@type”: “HowToStep”, “position”: 1, “name”: “Подготовка: определить ID инфоблоков и сгенерировать токен”, “text”: “Узнайте ID инфоблока каталога в Битрикс24 и на сайте. Сгенерируйте секретный токен длиной от 32 символов командой openssl rand -hex 32.”, “url”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/#что-понадобится-перед-началом-работы” }, { “@type”: “HowToStep”, “position”: 2, “name”: “Настроить отправку данных из Битрикс24”, “text”: “Добавьте код обработчика событий в файл /local/php_interface/init.php на сервере Битрикс24. Код подписывается на события обновления товара, цены и остатков, собирает данные и отправляет HTTP POST запрос на сайт.”, “url”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/#шаг-1-код-для-битрикс24—отправка-данных” }, { “@type”: “HowToStep”, “position”: 3, “name”: “Настроить приём данных на сайте”, “text”: “Создайте файл /local/api/sync_handler.php на сервере сайта. Скрипт проверяет токен авторизации, находит товар по XML_ID, обновляет цену через CPrice::SetBasePrice и остаток через CCatalogProduct::Update.”, “url”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/#шаг-2-код-для-сайта—приём-и-обновление” }, { “@type”: “HowToStep”, “position”: 4, “name”: “Настроить безопасность”, “text”: “Убедитесь что сайт работает по HTTPS. Создайте файлы .htaccess для защиты директорий API и логов. Ограничьте доступ к обработчику по IP-адресу сервера Битрикс24.”, “url”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/#шаг-3-безопасность-и-защита” }, { “@type”: “HowToStep”, “position”: 5, “name”: “Протестировать синхронизацию”, “text”: “Запустите тестовый скрипт для проверки связи. Затем измените товар в Битрикс24 вручную и убедитесь, что данные обновились на сайте. Проверьте файлы логов на обоих серверах. Удалите тестовый скрипт.”, “url”: “https://automata.sale/blog/bitrix/sinhronizaciya-ostatkov-cen-bitrix24-sajt/#шаг-4-тестирование” } ] }

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “Можно ли синхронизировать товары из облачного Битрикс24, а не коробочного?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Нет, описанный метод работает только с коробочной версией Битрикс24, потому что требует доступа к файлу init.php и серверным событиям OnAfterIBlockElementUpdate. В облачном Б24 для аналогичной задачи нужно использовать REST API с исходящими вебхуками — это другая архитектура.” } }, { “@type”: “Question”, “name”: “Что произойдёт, если сайт временно недоступен в момент синхронизации?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “HTTP-запрос вернёт ошибку, она запишется в лог sync_outgoing.log на сервере Б24. Данные не будут потеряны навсегда, но автоматически повторно не отправятся. Достаточно ещё раз сохранить товар в Б24 после восстановления сайта. Для критичных проектов рекомендуется добавить механизм очереди с повторными попытками.” } }, { “@type”: “Question”, “name”: “Подходит ли это решение для каталога с 10 000+ товаров?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Для ежедневной работы — да. Синхронизация происходит поштучно и мгновенно при сохранении товара менеджером. Для первоначальной массовой загрузки каталога используйте стандартный обмен с 1С или пакетный скрипт.” } }, { “@type”: “Question”, “name”: “Нужно ли устанавливать одинаковые XML_ID вручную?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Если товары попали в обе системы через импорт из 1С, XML_ID уже совпадают автоматически. Если товары создавались вручную, XML_ID нужно заполнить одинаковыми значениями в обеих системах — в поле Внешний код.” } }, { “@type”: “Question”, “name”: “Синхронизируются ли торговые предложения (размеры, цвета)?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “В базовой версии — нет. Синхронизируются только простые товары. Торговые предложения (SKU) имеют отдельный инфоблок и собственные цены и остатки. Для их поддержки нужно доработать код: обрабатывать дочерний инфоблок SKU и передавать данные по каждому предложению отдельно.” } }, { “@type”: “Question”, “name”: “Как часто происходит синхронизация?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Мгновенно, в момент сохранения товара менеджером в Битрикс24. Это событийная модель, а не синхронизация по расписанию. Задержка составляет 1–3 секунды.” } }, { “@type”: “Question”, “name”: “Безопасно ли передавать данные между серверами?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Да, при соблюдении трёх условий: HTTPS (шифрование канала), секретный токен (аутентификация запроса) и ограничение по IP-адресу (только сервер Б24 может обращаться к обработчику). Все три меры реализованы в приведённом коде.” } }, { “@type”: “Question”, “name”: “Будет ли работать с сайтом не на 1С-Битрикс?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Код отправки данных из Б24 (init.php) универсален — это обычный HTTP POST с JSON. Принимающую сторону нужно переписать под вашу CMS: WordPress/WooCommerce, OpenCart, Laravel и т.д. Логика та же: принять запрос, проверить токен, найти товар, обновить данные.” } } ] }

Синхронизация Битрикс24 и 1С-Битрикс

Синхронизация остатков и цен из коробки Битрикс24 на сайт 1С-Битрикс: полное руководство

Задача: менеджеры ведут товары и остатки в CRM-системе (коробочный Битрикс24), а на сайте интернет-магазина (1С-Битрикс) данные должны обновляться автоматически — без ручного дублирования и без сторонних сервисов.

Если вы управляете интернет-магазином на 1С-Битрикси ведёте учёт товаров вкоробочном Битрикс24, рано или поздно встаёт вопрос: как автоматически передавать актуальные цены и остатки из CRM на сайт? Ручной перенос данных — это ошибки, задержки и потерянные заказы. В этой статье мы разберём, как настроитьавтоматическую одностороннюю синхронизацию товаров с готовым кодом и пояснениями к каждому шагу.


Содержание

  1. Для кого эта статья
  2. Архитектура решения
  3. Что понадобится перед началом работы
  4. Шаг 1. Код для Битрикс24 — отправка данных
  5. Шаг 2. Код для сайта — приём и обновление
  6. Шаг 3. Безопасность и защита
  7. Шаг 4. Тестирование
  8. Частые проблемы и их решение
  9. Когда стоит обратиться к специалисту
  10. Итоги

Для кого эта статья

Эта инструкция будет полезна:

  • Владельцам интернет-магазинов на 1С-Битрикс, которые ведут каталог и остатки в коробочном Битрикс24.
  • Веб-разработчикам и системным интеграторам, которые настраивают обмен данными между CRM и сайтом.
  • Руководителям отделов продаж в компаниях Москвы, Санкт-Петербурга, Екатеринбурга и других городов России, где менеджеры работают в Б24, а клиенты покупают через сайт.

Уровень сложности: средний. Нужен доступ к файловой системе обоих серверов и базовое понимание PHP.


Архитектура решения

Прежде чем писать код, важно понимать общую схему. Мы строим одностороннюю синхронизацию — данные движутся только в одном направлении:

┌──────────────────────┐                    ┌──────────────────────┐
│                      │   HTTP POST        │                      │
│   КОРОБКА БИТРИКС24  │ ─────────────────► │   САЙТ 1С-БИТРИКС   │
│   (Источник данных)  │   JSON + Токен     │   (Интернет-магазин) │
│                      │                    │                      │
│  • Менеджер меняет   │                    │  • Принимает запрос   │
│    цену или остаток  │                    │  • Находит товар      │
│  • Событие Битрикс   │                    │    по XML_ID          │
│    срабатывает       │                    │  • Обновляет цену     │
│  • Данные            │                    │    и остаток          │
│    отправляются      │                    │  • Пишет лог          │
│                      │                    │                      │
└──────────────────────┘                    └──────────────────────┘

Ключевые принципы

ПринципРеализация
Связка товаровПо полю XML_ID (Внешний код) — одинаковый в Б24 и на сайте
ТриггерСобытие OnAfterIBlockElementUpdate в Б24
ТранспортHTTP POST запрос с JSON-телом
БезопасностьСекретный токен + проверка IP + HTTPS
Защита от зацикливанияГлобальный флаг + проверка, что действие выполнил живой пользователь
СкладОдин основной, передаётся общий остаток QUANTITY

Почему именно XML_ID?

XML_ID (Внешний код)— стандартное поле в Битриксе, предназначенное для связи элементов между разными системами. Если ваши товары выгружаются из 1С, XML_ID уже заполнен. Если нет — его нужно заполнить вручную или скриптом, и значениедолжно совпадать в Б24 и на сайте.


Что понадобится перед началом работы

Доступы

  • SSH или FTP-доступ к серверу Битрикс24 (коробка).
  • SSH или FTP-доступ к серверу сайта (1С-Битрикс).
  • Права администратора в обоих системах.

Данные для настройки

Перед написанием кода определите:

ПараметрГде узнатьПример
ID инфоблока каталога в Б24Админка Б24 → Контент → Инфоблоки14
ID инфоблока каталога на сайтеАдминка сайта → Контент → Инфоблоки2
ID типа базовой ценыМагазин → Типы цен1 (BASE)
URL сайтаhttps://example.ru

Как узнать ID инфоблока

Выполните в командной строке PHP на соответствующем сервере:

<?php
// Подключаем ядро Битрикс
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
CModule::IncludeModule('iblock');

$rs = CIBlock::GetList(['SORT' => 'ASC'], []);
while ($ar = $rs->Fetch()) {
echo "ID: {$ar['ID']} | {$ar['NAME']} | Тип: {$ar['IBLOCK_TYPE_ID']}\n";
}

Генерация секретного токена

Используйте надёжный токен длиной от 32 символов:

# В терминале Linux/Mac:
openssl rand -hex 32

# Результат (пример):
# a3f8b2c1d4e5f6071829304a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8

Шаг 1. Код для Битрикс24 — отправка данных

Что делает этот код

  1. Подписывается на событие обновления товара в инфоблоке.
  2. Проверяет, что изменение сделал живой пользователь (не робот, не агент, не фоновый процесс).
  3. Собирает XML_ID, цену и остаток.
  4. Отправляет JSON-запрос на сайт.

Файл: /local/php_interface/init.php

⚠️ Если файл init.php уже существует — не заменяйтеего, адобавьте код в конец.

<?php
/**
* =============================================================
* СИНХРОНИЗАЦИЯ ТОВАРОВ: Коробка Б24 → Сайт
* Добавить в /local/php_interface/init.php на сервере Битрикс24
* =============================================================
*/

use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;

// ----- КОНФИГУРАЦИЯ (измените под себя) -----

if (!defined('SYNC_SITE_URL')) {
define('SYNC_SITE_URL', 'https://ваш-сайт.ru/local/api/sync_handler.php');
}

if (!defined('SYNC_AUTH_TOKEN')) {
define('SYNC_AUTH_TOKEN', 'ВСТАВЬТЕ_СГЕНЕРИРОВАННЫЙ_ТОКЕН');
}

if (!defined('SYNC_CATALOG_IBLOCK_ID')) {
define('SYNC_CATALOG_IBLOCK_ID', 14); // ID инфоблока каталога в Б24
}

if (!defined('SYNC_PRICE_TYPE_ID')) {
define('SYNC_PRICE_TYPE_ID', 1); // ID типа базовой цены
}

if (!defined('SYNC_HTTP_TIMEOUT')) {
define('SYNC_HTTP_TIMEOUT', 10);
}

if (!defined('SYNC_ENABLE_LOG')) {
define('SYNC_ENABLE_LOG', true);
}

if (!defined('SYNC_LOG_FILE')) {
define('SYNC_LOG_FILE', $_SERVER['DOCUMENT_ROOT']
. '/local/logs/sync_outgoing.log');
}

// ----- РЕГИСТРАЦИЯ ОБРАБОТЧИКОВ СОБЫТИЙ -----

$eventManager = EventManager::getInstance();

// Событие обновления элемента инфоблока
$eventManager->addEventHandlerCompatible(
'iblock',
'OnAfterIBlockElementUpdate',
'SyncProductToSite'
);

// Событие изменения цены (цена может меняться отдельно)
$eventManager->addEventHandlerCompatible(
'catalog',
'OnPriceUpdate',
'SyncProductToSiteOnPriceChange'
);

// Событие изменения складских данных
$eventManager->addEventHandlerCompatible(
'catalog',
'OnProductUpdate',
'SyncProductToSiteOnProductUpdate'
);

// ----- ФЛАГ ЗАЩИТЫ ОТ ЗАЦИКЛИВАНИЯ -----

$GLOBALS['SYNC_TO_SITE_RUNNING'] = false;

// =============================================================
// ОБРАБОТЧИК: обновление элемента инфоблока
// =============================================================
function SyncProductToSite(&$arFields)
{
if ($GLOBALS['SYNC_TO_SITE_RUNNING'] === true) {
return;
}

if (
!isset($arFields['IBLOCK_ID'])
|| (int)$arFields['IBLOCK_ID'] !== SYNC_CATALOG_IBLOCK_ID
) {
return;
}

if (!_syncIsManualAction()) {
return;
}

$elementId = (int)$arFields['ID'];
if ($elementId <= 0) {
return;
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = true;

try {
$productData = _syncGetProductData($elementId);
if ($productData !== null) {
_syncSendToSite($productData);
}
} catch (\Exception $e) {
_syncLog('ERROR', 'Exception: ' . $e->getMessage());
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = false;
}

// =============================================================
// ОБРАБОТЧИК: изменение цены
// =============================================================
function SyncProductToSiteOnPriceChange($ID, $arFields)
{
if ($GLOBALS['SYNC_TO_SITE_RUNNING'] === true) {
return;
}

if (!_syncIsManualAction()) {
return;
}

$productId = (int)($arFields['PRODUCT_ID'] ?? 0);
if ($productId <= 0) {
return;
}

$catalogGroupId = (int)($arFields['CATALOG_GROUP_ID'] ?? 0);
if ($catalogGroupId !== SYNC_PRICE_TYPE_ID) {
return;
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = true;

try {
$productData = _syncGetProductData($productId);
if ($productData !== null) {
_syncSendToSite($productData);
}
} catch (\Exception $e) {
_syncLog('ERROR', 'OnPriceUpdate Exception: ' . $e->getMessage());
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = false;
}

// =============================================================
// ОБРАБОТЧИК: изменение остатков
// =============================================================
function SyncProductToSiteOnProductUpdate($ID, $arFields)
{
if ($GLOBALS['SYNC_TO_SITE_RUNNING'] === true) {
return;
}

if (!_syncIsManualAction()) {
return;
}

$productId = (int)$ID;
if ($productId <= 0) {
return;
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = true;

try {
$productData = _syncGetProductData($productId);
if ($productData !== null) {
_syncSendToSite($productData);
}
} catch (\Exception $e) {
_syncLog('ERROR', 'OnProductUpdate Exception: ' . $e->getMessage());
}

$GLOBALS['SYNC_TO_SITE_RUNNING'] = false;
}

// =============================================================
// ПРОВЕРКА: действие выполнено вручную?
// =============================================================
function _syncIsManualAction(): bool
{
global $USER;

// CLI (агенты, cron) — не ручное действие
if (php_sapi_name() === 'cli') {
return false;
}

// Нет HTTP-хоста — фоновый процесс
if (empty($_SERVER['HTTP_HOST'])) {
return false;
}

// Нет авторизованного пользователя
if (
!is_object($USER)
|| !method_exists($USER, 'GetID')
|| (int)$USER->GetID() <= 0
) {
return false;
}

return true;
}

// =============================================================
// СБОР ДАННЫХ О ТОВАРЕ
// =============================================================
function _syncGetProductData(int $elementId): ?array
{
Loader::includeModule('iblock');
Loader::includeModule('catalog');

// Получаем элемент инфоблока
$rsElement = \CIBlockElement::GetList(
[],
[
'ID'        => $elementId,
'IBLOCK_ID' => SYNC_CATALOG_IBLOCK_ID,
],
false,
false,
['ID', 'XML_ID', 'NAME', 'IBLOCK_ID']
);

$arElement = $rsElement->Fetch();
if (!$arElement) {
_syncLog('SKIP', "Элемент ID={$elementId} не найден");
return null;
}

$xmlId = trim($arElement['XML_ID'] ?? '');
if ($xmlId === '') {
_syncLog('SKIP', "Элемент ID={$elementId}: пустой XML_ID");
return null;
}

// Получаем базовую цену
$price    = 0;
$currency = 'RUB';

$rsPrice = \CPrice::GetList(
[],
[
'PRODUCT_ID'       => $elementId,
'CATALOG_GROUP_ID' => SYNC_PRICE_TYPE_ID,
]
);

if ($arPrice = $rsPrice->Fetch()) {
$price    = (float)$arPrice['PRICE'];
$currency = $arPrice['CURRENCY'] ?: 'RUB';
}

// Получаем остаток
$quantity  = 0;
$rsProduct = \CCatalogProduct::GetByID($elementId);
if ($rsProduct) {
$quantity = (float)$rsProduct['QUANTITY'];
}

$data = [
'xml_id'    => $xmlId,
'price'     => $price,
'currency'  => $currency,
'quantity'  => $quantity,
'name'      => $arElement['NAME'],
'source_id' => $elementId,
];

_syncLog('DATA', 'Подготовлено: '
. json_encode($data, JSON_UNESCAPED_UNICODE));

return $data;
}

// =============================================================
// ОТПРАВКА ДАННЫХ НА САЙТ
// =============================================================
function _syncSendToSite(array $productData): bool
{
$httpClient = new \Bitrix\Main\Web\HttpClient([
'socketTimeout' => SYNC_HTTP_TIMEOUT,
'streamTimeout' => SYNC_HTTP_TIMEOUT,
'redirect'      => false,
]);

$httpClient->setHeader('Content-Type', 'application/json');
$httpClient->setHeader('Accept', 'application/json');

$requestBody = json_encode([
'auth_token' => SYNC_AUTH_TOKEN,
'action'     => 'update_product',
'timestamp'  => date('c'),
'data'       => $productData,
], JSON_UNESCAPED_UNICODE);

_syncLog('SEND', "→ " . SYNC_SITE_URL
. " | XML_ID: " . $productData['xml_id']);

$response   = $httpClient->post(SYNC_SITE_URL, $requestBody);
$httpStatus = $httpClient->getStatus();
$errors     = $httpClient->getError();

if (!empty($errors)) {
$errorStr = implode('; ', $errors);
_syncLog('ERROR', "HTTP ошибка: {$errorStr}");
return false;
}

if ($httpStatus !== 200) {
_syncLog('ERROR', "HTTP {$httpStatus}: {$response}");
return false;
}

$responseData = json_decode($response, true);
$success = ($responseData['status'] ?? '') === 'success';
$message = $responseData['message'] ?? '';

_syncLog(
$success ? 'OK' : 'FAIL',
"XML_ID: {$productData['xml_id']} | {$message}"
);

return $success;
}

// =============================================================
// ЛОГИРОВАНИЕ
// =============================================================
function _syncLog(string $level, string $message): void
{
if (!SYNC_ENABLE_LOG) {
return;
}

$logDir = dirname(SYNC_LOG_FILE);
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}

$timestamp = date('Y-m-d H:i:s');
$line = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;

file_put_contents(SYNC_LOG_FILE, $line, FILE_APPEND | LOCK_EX);

// Ротация при > 5 МБ
if (
file_exists(SYNC_LOG_FILE)
&& filesize(SYNC_LOG_FILE) > 5 * 1024 * 1024
) {
rename(
SYNC_LOG_FILE,
SYNC_LOG_FILE . '.' . date('Ymd_His') . '.bak'
);
}
}

Пояснения к коду

Зачем три обработчика событий?

В Битрикс24 цена, остаток и основные поля товара могут обновляться независимо друг от друга. Менеджер может изменить только цену, не трогая остальные поля — в этом случае OnAfterIBlockElementUpdate не сработает. Поэтому мы подписываемся на три события:

СобытиеКогда срабатывает
OnAfterIBlockElementUpdateИзменение полей элемента (название, XML_ID, активность и т.д.)
OnPriceUpdateИзменение цены
OnProductUpdateИзменение складских данных (количество)

Как работает защита от зацикливания?

Глобальная переменная $GLOBALS['SYNC_TO_SITE_RUNNING'] — это простой, но надёжный механизм. Когда один из обработчиков начинает работу, он ставит флаг true. Если в процессе работы Битрикс вызовет другой обработчик (например, обновление цены вызовет пересчёт элемента) — второй обработчик увидит флаг и сразу выйдет.

Дополнительно функция _syncIsManualAction() отсекает:

  • Агенты — фоновые задачи, выполняемые по расписанию.
  • CLI-скриптыcron-задачи, импорт из 1С.
  • Запросы без авторизации — REST API, вебхуки.

Шаг 2. Код для сайта — приём и обновление

Что делает этот код

  1. Принимает POST-запрос от Б24.
  2. Проверяет секретный токен.
  3. Находит товар на сайте по XML_ID.
  4. Обновляет цену и остаток.
  5. Отвечает JSON-ом с результатом.
  6. Всё логирует.

Подготовка

Создайте директории на сервере сайта:

mkdir -p /home/bitrix/www/local/api
mkdir -p /home/bitrix/www/local/logs
chmod 755 /home/bitrix/www/local/api
chmod 775 /home/bitrix/www/local/logs

Файл: /local/api/sync_handler.php

<?php
/**
* =============================================================
* ПРИЁМНИК СИНХРОНИЗАЦИИ ТОВАРОВ
* Файл: /local/api/sync_handler.php на сервере сайта (1С-Битрикс)
* =============================================================
*/

// ----- КОНФИГУРАЦИЯ (измените под себя) -----

const SYNC_AUTH_TOKEN            = 'ВСТАВЬТЕ_ТОТ_ЖЕ_ТОКЕН_ЧТО_И_В_Б24';
const SYNC_SITE_CATALOG_IBLOCK_ID = 2;   // ID инфоблока каталога на сайте
const SYNC_LOG_FILE              = __DIR__ . '/../../logs/sync.log';
const SYNC_LOG_MAX_SIZE          = 5 * 1024 * 1024; // 5 МБ

// Разрешённые IP (пустой массив = любые)
const SYNC_ALLOWED_IPS = [
// '111.222.333.444',  // IP сервера Б24
];

// ----- ИНИЦИАЛИЗАЦИЯ ЯДРА БИТРИКС -----

$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../../..');
$DOCUMENT_ROOT = $_SERVER['DOCUMENT_ROOT'];

define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_NO_ACCELERATOR_RESET', true);
define('STOP_STATISTICS', true);
define('NO_AGENT_STATISTIC', 'Y');
define('NO_AGENT_CHECK', true);
define('DisableEventsCheck', true);

require($DOCUMENT_ROOT . '/bitrix/modules/main/include/prolog_before.php');

use Bitrix\Main\Loader;

header('Content-Type: application/json; charset=utf-8');

// ----- ОСНОВНАЯ ЛОГИКА -----

try {
// 1. Проверка метода
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
syncResponse('error', 'Допустим только POST', 405);
}

// 2. Проверка IP
if (!empty(SYNC_ALLOWED_IPS)) {
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '';
if (!in_array($clientIp, SYNC_ALLOWED_IPS, true)) {
syncLog('DENIED', "Запрет IP: {$clientIp}");
syncResponse('error', 'Доступ запрещён', 403);
}
}

// 3. Чтение тела запроса
$rawInput = file_get_contents('php://input');

if (empty($rawInput)) {
syncLog('ERROR', 'Пустое тело запроса');
syncResponse('error', 'Пустой запрос', 400);
}

$requestData = json_decode($rawInput, true);

if (json_last_error() !== JSON_ERROR_NONE) {
syncLog('ERROR', 'Невалидный JSON: '
. json_last_error_msg());
syncResponse('error', 'Невалидный JSON', 400);
}

// 4. Проверка токена
$receivedToken = $requestData['auth_token'] ?? '';

if (!hash_equals(SYNC_AUTH_TOKEN, $receivedToken)) {
syncLog('DENIED', 'Неверный токен');
syncResponse('error', 'Ошибка авторизации', 401);
}

// 5. Проверка действия
if (($requestData['action'] ?? '') !== 'update_product') {
syncResponse('error', 'Неизвестное действие', 400);
}

// 6. Извлечение данных
$data = $requestData['data'] ?? [];

$xmlId    = trim($data['xml_id'] ?? '');
$price    = (float)($data['price'] ?? 0);
$currency = trim($data['currency'] ?? 'RUB');
$quantity = (float)($data['quantity'] ?? 0);
$name     = $data['name'] ?? '';

if ($xmlId === '') {
syncLog('ERROR', 'Пустой XML_ID');
syncResponse('error', 'XML_ID обязателен', 400);
}

syncLog('RECV', "XML_ID={$xmlId} | Цена={$price} {$currency}"
. " | Остаток={$quantity} | {$name}");

// 7. Подключаем модули
Loader::includeModule('iblock');
Loader::includeModule('catalog');

// 8. Ищем товар по XML_ID
$productId = syncFindProductByXmlId($xmlId);

if ($productId === null) {
syncLog('NOT_FOUND', "Товар XML_ID={$xmlId} не найден");
syncResponse('error',
"Товар XML_ID={$xmlId} не найден на сайте", 404);
}

syncLog('FOUND', "XML_ID={$xmlId} → ID={$productId}");

// 9. Обновляем цену
$priceOk = syncUpdatePrice($productId, $price, $currency);

// 10. Обновляем остаток
$qtyOk = syncUpdateQuantity($productId, $quantity);

// 11. Ответ
$msg = sprintf(
'ID=%d: цена %s, остаток %s',
$productId,
$priceOk ? '✓' : '✗',
$qtyOk   ? '✓' : '✗'
);

syncLog('DONE', $msg);

$status = ($priceOk && $qtyOk) ? 'success' : 'partial';
syncResponse($status, $msg, 200);

} catch (\Exception $e) {
syncLog('EXCEPTION', $e->getMessage());
syncResponse('error', 'Внутренняя ошибка', 500);
}

// =============================================================
// ФУНКЦИИ
// =============================================================

/**
* Поиск товара по XML_ID
*/
function syncFindProductByXmlId(string $xmlId): ?int
{
// Сначала ищем среди активных товаров
$rsElement = \CIBlockElement::GetList(
['ID' => 'ASC'],
[
'IBLOCK_ID' => SYNC_SITE_CATALOG_IBLOCK_ID,
'XML_ID'    => $xmlId,
'ACTIVE'    => 'Y',
],
false,
['nTopCount' => 1],
['ID', 'XML_ID', 'NAME']
);

if ($arElement = $rsElement->Fetch()) {
return (int)$arElement['ID'];
}

// Если среди активных не нашли — ищем среди всех
$rsElement2 = \CIBlockElement::GetList(
['ID' => 'ASC'],
[
'IBLOCK_ID' => SYNC_SITE_CATALOG_IBLOCK_ID,
'XML_ID'    => $xmlId,
],
false,
['nTopCount' => 1],
['ID', 'XML_ID', 'NAME']
);

if ($arElement2 = $rsElement2->Fetch()) {
syncLog('WARN', "XML_ID={$xmlId} найден, но неактивен"
. " (ID={$arElement2['ID']})");
return (int)$arElement2['ID'];
}

return null;
}

/**
* Обновление базовой цены товара
*/
function syncUpdatePrice(
int $productId,
float $price,
string $currency = 'RUB'
): bool {
// Читаем текущую цену для сравнения
$currentPrice = null;
$rsPrice = \CPrice::GetList(
[],
[
'PRODUCT_ID'       => $productId,
'CATALOG_GROUP_ID' => 1,
]
);

if ($arCurrent = $rsPrice->Fetch()) {
$currentPrice = (float)$arCurrent['PRICE'];
}

// Цена не изменилась — пропускаем
if (
$currentPrice !== null
&& abs($currentPrice - $price) < 0.01
) {
syncLog('SKIP_PRICE',
"ID={$productId}: цена не изменилась ({$price})");
return true;
}

$result = \CPrice::SetBasePrice(
$productId,
$price,
$currency
);

if ($result) {
syncLog('PRICE_OK', sprintf(
'ID=%d | %s → %s %s',
$productId,
$currentPrice !== null ? $currentPrice : 'н/д',
$price,
$currency
));
return true;
}

syncLog('PRICE_ERR',
"Не удалось обновить цену ID={$productId}");
return false;
}

/**
* Обновление остатка товара
*/
function syncUpdateQuantity(int $productId, float $quantity): bool
{
// Читаем текущий остаток для сравнения
$currentQty = null;
$rsProduct  = \CCatalogProduct::GetByID($productId);

if ($rsProduct) {
$currentQty = (float)$rsProduct['QUANTITY'];
}

// Остаток не изменился — пропускаем
if (
$currentQty !== null
&& abs($currentQty - $quantity) < 0.001
) {
syncLog('SKIP_QTY',
"ID={$productId}: остаток не изменился ({$quantity})");
return true;
}

$result = \CCatalogProduct::Update($productId, [
'QUANTITY'               => $quantity,
'QUANTITY_TRACE'         => 'Y',
'CAN_BUY_ZERO'          => 'N',
'NEGATIVE_AMOUNT_TRACE'  => 'N',
]);

if ($result) {
syncLog('QTY_OK', sprintf(
'ID=%d | %s → %s',
$productId,
$currentQty !== null ? $currentQty : 'н/д',
$quantity
));
return true;
}

syncLog('QTY_ERR',
"Не удалось обновить остаток ID={$productId}");
return false;
}

/**
* JSON-ответ и завершение скрипта
*/
function syncResponse(
string $status,
string $message,
int $httpCode = 200
): void {
http_response_code($httpCode);

echo json_encode([
'status'    => $status,
'message'   => $message,
'timestamp' => date('c'),
], JSON_UNESCAPED_UNICODE);

if (
defined('B_PROLOG_INCLUDED')
&& B_PROLOG_INCLUDED === true
) {
require_once(
$_SERVER['DOCUMENT_ROOT']
. '/bitrix/modules/main/include/epilog_after.php'
);
}

die();
}

/**
* Логирование
*/
function syncLog(string $level, string $message): void
{
$logDir = dirname(SYNC_LOG_FILE);
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}

$ts       = date('Y-m-d H:i:s');
$ip       = $_SERVER['REMOTE_ADDR'] ?? '-';
$logLine  = "[{$ts}] [{$level}] [IP:{$ip}] {$message}" . PHP_EOL;

file_put_contents(
SYNC_LOG_FILE,
$logLine,
FILE_APPEND | LOCK_EX
);

// Ротация при превышении лимита
if (
file_exists(SYNC_LOG_FILE)
&& filesize(SYNC_LOG_FILE) > SYNC_LOG_MAX_SIZE
) {
rename(
SYNC_LOG_FILE,
SYNC_LOG_FILE . '.' . date('Ymd_His') . '.bak'
);
}
}

Шаг 3. Безопасность и защита

Синхронизация передаёт коммерческие данные между серверами. Вот обязательные меры защиты.

3.1. HTTPS

Оба сервера обязаны работать по HTTPS. Без шифрования токен и данные передаются в открытом виде.

# Проверка SSL-сертификата сайта:
curl -I https://ваш-сайт.ru/local/api/sync_handler.php

3.2. Файл .htaccess для директории API

Файл: /local/api/.htaccess

# Запрещаем листинг директории
Options -Indexes

# Разрешаем доступ только к обработчику

Require all granted

# Всё остальное — запрещено

Require all denied

# Логи — запрещены

Require all denied

3.3. Файл .htaccess для директории логов

Файл: /local/logs/.htaccess

# Полный запрет доступа к логам через веб
Require all denied

3.4. Ограничение по IP

Узнайте IP-адрес сервера Б24 и добавьте его в конфигурацию приёмника:

// В файле sync_handler.php:
const SYNC_ALLOWED_IPS = [
'123.45.67.89',  // IP вашего сервера Битрикс24
];

Как узнать IP сервера Б24:

# На сервере Б24 выполните:
curl ifconfig.me

3.5. Требования к токену

ПараметрМинимумРекомендация
Длина32 символа64 символа
СоставБуквы + цифры+ спецсимволы
ХранениеВ кодеВ .env или dbconn.php

Шаг 4. Тестирование

4.1. Тестовый скрипт

Создайте временный файл для проверки. Удалите его сразу после тестирования!

Файл: /local/api/test_sync.php

<?php
/**
* ТЕСТОВЫЙ СКРИПТ — УДАЛИТЬ ПОСЛЕ ПРОВЕРКИ!
* Имитирует запрос от Б24 к сайту.
*/

$siteUrl   = 'https://ваш-сайт.ru/local/api/sync_handler.php';
$authToken = 'ВСТАВЬТЕ_ВАШ_ТОКЕН';

$testData = [
'auth_token' => $authToken,
'action'     => 'update_product',
'timestamp'  => date('c'),
'data'       => [
'xml_id'    => 'TEST_001',   // ← реальный XML_ID с сайта!
'price'     => 2499.99,
'currency'  => 'RUB',
'quantity'  => 10,
'name'      => 'Тестовый товар',
'source_id' => 0,
],
];

$json = json_encode($testData, JSON_UNESCAPED_UNICODE);

echo "=== ТЕСТ СИНХРОНИЗАЦИИ ===\n\n";
echo "URL: {$siteUrl}\n";
echo "Запрос:\n{$json}\n\n";

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL            => $siteUrl,
CURLOPT_POST           => true,
CURLOPT_POSTFIELDS     => $json,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT        => 15,
CURLOPT_HTTPHEADER     => [
'Content-Type: application/json',
'Accept: application/json',
],
]);

$response  = curl_exec($ch);
$httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

echo "HTTP Code: {$httpCode}\n";

if ($curlError) {
echo "Ошибка cURL: {$curlError}\n";
} else {
$decoded = json_decode($response, true);
echo "Ответ:\n";
echo json_encode(
$decoded,
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
) . "\n";
}

echo "\n=== КОНЕЦ ТЕСТА ===\n";
echo "\n⚠️  УДАЛИТЕ ЭТОТ ФАЙЛ!\n";

4.2. Порядок тестирования

Шаг 1.  Положите все файлы на оба сервера
Шаг 2.  Запустите test_sync.php — проверьте ответ
Шаг 3.  Откройте /local/logs/sync.log — проверьте записи
Шаг 4.  Откройте карточку товара на сайте — цена и остаток обновились?
Шаг 5.  Измените цену товара в Б24 вручную
Шаг 6.  Проверьте sync_outgoing.log на сервере Б24
Шаг 7.  Проверьте sync.log на сайте
Шаг 8.  Проверьте товар на сайте — данные обновились?
Шаг 9.  УДАЛИТЕ test_sync.php

4.3. Пример записей в логе

Лог сайта (/local/logs/sync.log):

[2024-12-20 14:32:15] [RECV] [IP:123.45.67.89] XML_ID=CRM_001 | Цена=1500 RUB | Остаток=25 | Кабель USB-C
[2024-12-20 14:32:15] [FOUND] [IP:123.45.67.89] XML_ID=CRM_001 → ID=847
[2024-12-20 14:32:15] [PRICE_OK] [IP:123.45.67.89] ID=847 | 1200 → 1500 RUB
[2024-12-20 14:32:15] [QTY_OK] [IP:123.45.67.89] ID=847 | 30 → 25
[2024-12-20 14:32:15] [DONE] [IP:123.45.67.89] ID=847: цена ✓, остаток ✓

Лог Б24 (/local/logs/sync_outgoing.log):

[2024-12-20 14:32:14] [DATA] Подготовлено: {"xml_id":"CRM_001","price":1500,"currency":"RUB","quantity":25,"name":"Кабель USB-C","source_id":1234}
[2024-12-20 14:32:14] [SEND] → https://сайт.ru/local/api/sync_handler.php | XML_ID: CRM_001
[2024-12-20 14:32:15] [OK] XML_ID: CRM_001 | ID=847: цена ✓, остаток ✓

Частые проблемы и их решение

Проблема 1: «Товар не найден» (404)

Причина: XML_ID в Б24 и на сайте не совпадают.

Диагностика:

// Выполните на сайте:
CModule::IncludeModule('iblock');
$rs = CIBlockElement::GetList(
[],
['IBLOCK_ID' => 2],  // ваш ID инфоблока
false,
['nTopCount' => 10],
['ID', 'NAME', 'XML_ID']
);
while ($ar = $rs->Fetch()) {
echo "ID={$ar['ID']} | XML_ID={$ar['XML_ID']} | {$ar['NAME']}\n";
}

Решение: Заполните XML_ID на сайте так, чтобы значения совпадали с Б24.

Проблема 2: «Ошибка авторизации» (401)

Причина: Токены не совпадают.

Решение: Скопируйте токен из настроек Б24 и вставьте его в sync_handler.php. Проверьте, что нет лишних пробелов и переносов строк.

Проблема 3: Синхронизация не срабатывает

Причины и решения:

СимптомПричинаРешение
Лог Б24 пустОбработчик не зарегистрированПроверьте, что init.php подключается. Очистите кеш Б24
В логе SKIPПустой XML_IDЗаполните XML_ID у товара в Б24
В логе HTTP ошибкаСайт недоступенПроверьте URL, SSL-сертификат, firewall
Лог Б24 есть, лог сайта пустЗапрос не доходитПроверьте .htaccess, firewall на server сайта

Проблема 4: Срабатывает при импорте из 1С

Причина: Импорт 1С обновляет элементы инфоблока и триггерит события.

Решение: Функция _syncIsManualAction() уже отсекает такие случаи, потому что импорт 1С обычно выполняется без авторизованного пользователя. Если ваш импорт работает иначе — добавьте дополнительную проверку:

// Добавьте в функцию _syncIsManualAction():
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($requestUri, '/bitrix/admin/1c_exchange.php') !== false) {
return false; // Это импорт 1С — не ручное действие
}

Проблема 5: Таймаут при отправке

Причина: Сайт отвечает медленно.

Решение: Увеличьте таймаут:

define('SYNC_HTTP_TIMEOUT', 30); // вместо 10

Структура файлов проекта

СЕРВЕР БИТРИКС24
==========================================
/local/
├── php_interface/
│   └── init.php              ← код отправки
└── logs/
├── .htaccess             ← запрет веб-доступа
└── sync_outgoing.log     ← лог отправленных данных

СЕРВЕР САЙТА
==========================================
/local/
├── api/
│   ├── .htaccess             ← защита директории
│   └── sync_handler.php      ← код приёма и обновления
└── logs/
├── .htaccess             ← запрет веб-доступа
└── sync.log              ← лог принятых данных

Когда стоит обратиться к специалисту

Описанное решение покрывает базовый сценарий: один склад, базовая цена, связка по XML_ID. Если вам нужно больше — рекомендуем обратиться к опытному интегратору:

  • Несколько складов — нужна синхронизация остатков с разбивкой по складам через CCatalogStoreProduct.
  • Торговые предложения (SKU) — у товара несколько вариантов (размер, цвет), каждый со своей ценой и остатком.
  • Несколько типов цен — оптовая, розничная, для дилеров.
  • Двусторонняя синхронизация — заказы с сайта должны попадать обратно в Б24.
  • Очередь и повторные попытки — при недоступности сайта данные должны отправляться позже.
  • Массовый импорт — тысячи товаров обновляются одновременно, и нужна пакетная отправка вместо поштучной.

Часто задаваемые вопросы (FAQ)

Можно ли синхронизировать товары из облачного Битрикс24, а не коробочного?

Нет, описанный метод работает только с коробочной версией Битрикс24, потому что требует доступа к файлу init.php и серверным событиям OnAfterIBlockElementUpdate. В облачном Б24 для аналогичной задачи нужно использовать REST API с исходящими вебхуками — это другая архитектура, которую мы рассмотрим в отдельной статье.

Что произойдёт, если сайт временно недоступен в момент синхронизации?

Данные не будут потеряны навсегда, но и не отправятся автоматически повторно — текущая реализация не включает очередь. HTTP-запрос вернёт ошибку, она запишется в лог sync_outgoing.log на сервере Б24. Чтобы исправить ситуацию, достаточно ещё раз сохранить товар в Б24 после восстановления сайта. Для критичных проектов рекомендуем добавить механизм повторных попыток с очередью.

Подходит ли это решение для каталога с 10 000+ товаров?

Для ежедневной работы— да. Синхронизация происходит поштучно: менеджер меняет один товар — обновляется один товар на сайте. Это мгновенно и не создаёт нагрузки. Однако дляпервоначальной массовой загрузки каталога этот метод не подходит — для этого используйте стандартный обмен с 1С или напишите пакетный скрипт.

Нужно ли устанавливать одинаковые XML_ID вручную?

Если товары попали в обе системы через импорт из 1С, XML_ID уже совпадают — это стандартное поведение. Если товары создавались вручную, XML_ID нужно заполнить. Можно сделать это разово скриптом или указывать при создании товара в поле «Внешний код».

Синхронизируются ли торговые предложения (размеры, цвета)?

В базовой версии — нет. Синхронизируются только простые товары. Торговые предложения (SKU) имеют отдельный инфоблок и собственные цены/остатки. Для их поддержки нужно доработать код: обрабатывать дочерний инфоблок SKU и передавать данные по каждому предложению отдельно.

Как часто происходит синхронизация?

Мгновенно, в момент сохранения товара менеджером. Это не синхронизация по расписанию (раз в час, раз в день), а событийная модель: произошло изменение → сработал обработчик → данные отправлены. Задержка составляет 1–3 секунды.

Безопасно ли передавать данные между серверами?

Да, при соблюдении трёх условий: HTTPS(шифрование канала),секретный токен(аутентификация запроса) иограничение по IP (только сервер Б24 может обращаться к обработчику). Все три меры реализованы в коде.

Будет ли работать с сайтом не на 1С-Битрикс?

Принцип отправки данных (Часть 1, код для Б24) универсален — это обычный HTTP POST с JSON. Принимающую сторону (Часть 2) нужно переписать под CMS вашего сайта: WordPress/WooCommerce, OpenCart, Laravel и т.д. Логика та же: принять запрос, проверить токен, найти товар, обновить данные.


Итоги

Мы реализовали надёжную одностороннюю синхронизацию товаров из коробочного Битрикс24 в интернет-магазин на 1С-Битрикс.

Что сделано

КомпонентРеализация
ТриггерТри события Битрикс: элемент, цена, остаток
ТранспортHTTP POST с JSON
Связка товаровПоле XML_ID (Внешний код)
БезопасностьТокен + проверка IP + HTTPS + .htaccess
Защита от цикловГлобальный флаг + проверка ручного действия
ЛогированиеОба сервера, ротация файлов
ОптимизацияПропуск обновления при неизменных данных

Что передаётся

ПолеОписание
xml_idВнешний код для связки
priceБазовая цена
currencyВалюта
quantityОбщий остаток (1 склад)

Преимущества подхода

  • Мгновенная синхронизация — данные обновляются сразу при сохранении товара менеджером, а не по расписанию.
  • Без сторонних сервисов — не нужны платные коннекторы и внешние API.
  • Прозрачность — подробные логи позволяют отследить каждую операцию.
  • Безопасность — многоуровневая защита от несанкционированного доступа.

Код готов к использованию — подставьте свои значения в блоки конфигурации, пройдите по чек-листу из Шага 4 и запускайте.


Если у вас возникли вопросы по настройке синхронизации Битрикс24 и 1С-Битрикс — оставьте заявку, и мы поможем с внедрением.


Нужна помощь с настройкой или доработкой решений Аспро / 1С-Битрикс? Мы специализируемся на веб-разработке, сложных интеграциях с 1С/Битрикс24 и SEO-оптимизации. Свяжитесь с нами, чтобы повысить продажи и автоматизировать бизнес-процессы.

🚀 Нужна помощь с сайтом на 1С-Битрикс или Аспро?

Я работаю удалённо по всей России и СНГ. Узнайте цены и условия для вашего города:

Все регионы →