Интеграция «заказ на сайте → сделка в Битрикс24» — одна из самых востребованных доработок и одновременно одна из самых аварийных. Причина в том, что она связывает две независимые системы в момент, критичный для бизнеса: оформление заказа.
Дальше — рабочая реализация, но сначала главный вопрос, который нужно решить до написания кода: что произойдёт, если Битрикс24 в этот момент недоступен.
Задача
При оформлении заказа в интернет-магазине на 1С-Битрикс автоматически создавать сделку в Битрикс24:
- Найти существующего контакта по телефону/email
- Если не найден — создать нового
- Создать сделку и привязать к контакту
- Добавить товары из корзины в сделку
Заказ не должен зависеть от доступности CRM. Если код создания сделки выполняется синхронно в обработчике оформления заказа, то при недоступности портала (обновление, сбой сети, превышенный лимит) покупатель увидит ошибку и уйдёт. Магазин перестал продавать, потому что недоступна CRM, — ситуация, которую бизнес не простит.
Правильная схема: обработчик события сохраняет задание на синхронизацию (запись в своей таблице), а обмен выполняет фоновый процесс — cron-скрипт или агент. Заказ создаётся всегда; сделка появляется через минуту, и никого это не беспокоит.
Побочная выгода та же, что и в других интеграциях: появляется место для повторов. Портал ответил ошибкой — задание осталось в очереди, а не потерялось.
Подготовка
Создание вебхука в Битрикс24
- Перейдите в Разработчикам → Другое → Входящий вебхук
- Выберите права доступа:
crm— работа с CRMuser— информация о пользователях
- Скопируйте URL вебхука
URL будет вида: https://your-portal.bitrix24.ru/rest/1/abc123xyz456/
Этот URL целиком является секретом — токен встроен прямо в адрес. Не коммитьте его, не логируйте полный адрес запроса, не пересылайте в мессенджере. Место ему в конфигурации вне репозитория.
И выдавайте вебхуку минимально необходимые права. Для этой задачи нужны crm и user; отмечать всё подряд «чтобы точно работало» — значит превратить утёкший URL из проблемы в катастрофу.
Базовая реализация
Добавьте в /local/php_interface/init.php:
<?php
use Bitrix\Main\EventManager;
use Bitrix\Main\Web\HttpClient;
// Регистрация обработчика события
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderSaved',
'createDealFromOrder'
);
/**
* Отправка запроса к API Битрикс24
*/
function sendB24Request(string $method, array $params): array
{
$webhookUrl = 'https://your-portal.bitrix24.ru/rest/1/your-webhook-token/';
$httpClient = new HttpClient([
'socketTimeout' => 10,
'streamTimeout' => 30,
]);
$response = $httpClient->post($webhookUrl . $method, $params);
if ($httpClient->getStatus() !== 200) {
AddMessage2Log("B24 API Error: HTTP {$httpClient->getStatus()}", 'b24_integration');
return [];
}
return json_decode($response, true) ?? [];
}
/**
* Создание сделки из заказа
*/
function createDealFromOrder(\Bitrix\Main\Event $event): void
{
$order = $event->getParameter('ENTITY');
// Обрабатываем только новые заказы
if (!$event->getParameter('IS_NEW')) {
return;
}
try {
$basket = $order->getBasket();
$props = $order->getPropertyCollection();
// Получаем данные покупателя
$name = $props->getPayerName()?->getValue() ?? '';
$phone = $props->getPhone()?->getValue() ?? '';
$email = $props->getUserEmail()?->getValue() ?? '';
// Ищем или создаём контакт
$contactId = findOrCreateContact($name, $phone, $email);
if (!$contactId) {
throw new \RuntimeException('Не удалось создать контакт');
}
// Создаём сделку
$dealId = createDeal($order, $contactId);
if (!$dealId) {
throw new \RuntimeException('Не удалось создать сделку');
}
// Добавляем товары
if ($basket && count($basket->getQuantityList()) > 0) {
addProductsToDeal($dealId, $basket);
}
AddMessage2Log("Создана сделка #{$dealId} для заказа #{$order->getId()}", 'b24_integration');
} catch (\Throwable $e) {
AddMessage2Log("Ошибка создания сделки: {$e->getMessage()}", 'b24_integration');
}
}
/**
* Поиск или создание контакта
*/
function findOrCreateContact(string $name, string $phone, string $email): ?int
{
// Поиск по телефону
if ($phone) {
$result = sendB24Request('crm.contact.list.json', [
'filter' => ['PHONE' => $phone],
'select' => ['ID'],
]);
if (!empty($result['result'][0]['ID'])) {
return (int) $result['result'][0]['ID'];
}
}
// Поиск по email
if ($email) {
$result = sendB24Request('crm.contact.list.json', [
'filter' => ['EMAIL' => $email],
'select' => ['ID'],
]);
if (!empty($result['result'][0]['ID'])) {
return (int) $result['result'][0]['ID'];
}
}
// Создание нового контакта
$contactFields = [
'fields' => [
'NAME' => $name ?: 'Покупатель',
'OPENED' => 'Y',
'ASSIGNED_BY_ID' => 1,
'TYPE_ID' => 'CLIENT',
'SOURCE_ID' => 'WEB',
],
'params' => ['REGISTER_SONET_EVENT' => 'Y'],
];
if ($phone) {
$contactFields['fields']['PHONE'] = [
['VALUE' => $phone, 'VALUE_TYPE' => 'WORK'],
];
}
if ($email) {
$contactFields['fields']['EMAIL'] = [
['VALUE' => $email, 'VALUE_TYPE' => 'WORK'],
];
}
$result = sendB24Request('crm.contact.add.json', $contactFields);
return isset($result['result']) ? (int) $result['result'] : null;
}
/**
* Создание сделки
*/
function createDeal(\Bitrix\Sale\Order $order, int $contactId): ?int
{
$dealFields = [
'fields' => [
'TITLE' => "Заказ №{$order->getId()} с сайта",
'CONTACT_ID' => $contactId,
'OPPORTUNITY' => $order->getPrice(),
'CURRENCY_ID' => $order->getCurrency(),
'ASSIGNED_BY_ID' => 1,
'SOURCE_ID' => 'WEB',
'STAGE_ID' => 'NEW',
'COMMENTS' => "Заказ создан автоматически. ID на сайте: {$order->getId()}",
],
'params' => ['REGISTER_SONET_EVENT' => 'Y'],
];
$result = sendB24Request('crm.deal.add.json', $dealFields);
return isset($result['result']) ? (int) $result['result'] : null;
}
/**
* Добавление товаров к сделке
*/
function addProductsToDeal(int $dealId, \Bitrix\Sale\Basket $basket): void
{
$rows = [];
foreach ($basket as $item) {
$rows[] = [
'PRODUCT_NAME' => $item->getField('NAME'),
'PRICE' => $item->getPrice(),
'QUANTITY' => $item->getQuantity(),
];
}
sendB24Request('crm.deal.productrows.set', [
'id' => $dealId,
'rows' => $rows,
]);
}Расширенная реализация
Вынесем логику в отдельный класс с дополнительными возможностями:
<?php
namespace Local\Integration\Bitrix24;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Sale\Order;
use Bitrix\Sale\Basket;
class OrderToDealService
{
private string $webhookUrl;
private int $defaultResponsibleId;
private HttpClient $httpClient;
public function __construct(string $webhookUrl, int $responsibleId = 1)
{
$this->webhookUrl = rtrim($webhookUrl, '/') . '/';
$this->defaultResponsibleId = $responsibleId;
$this->httpClient = new HttpClient([
'socketTimeout' => 10,
'streamTimeout' => 30,
]);
}
/**
* Основной метод — создание сделки из заказа
*/
public function createFromOrder(Order $order): ?int
{
$props = $order->getPropertyCollection();
$customerData = [
'name' => $this->getPropertyValue($props, 'FIO')
?? $props->getPayerName()?->getValue()
?? '',
'phone' => $this->getPropertyValue($props, 'PHONE')
?? $props->getPhone()?->getValue()
?? '',
'email' => $this->getPropertyValue($props, 'EMAIL')
?? $props->getUserEmail()?->getValue()
?? '',
'comment' => $this->getPropertyValue($props, 'COMMENT') ?? '',
];
// Поиск или создание контакта
$contactId = $this->findContact($customerData['phone'], $customerData['email'])
?? $this->createContact($customerData);
if (!$contactId) {
throw new \RuntimeException('Не удалось найти/создать контакт');
}
// Создание сделки
$dealId = $this->createDeal($order, $contactId, $customerData['comment']);
if (!$dealId) {
throw new \RuntimeException('Не удалось создать сделку');
}
// Добавление товаров
$basket = $order->getBasket();
if ($basket) {
$this->addProducts($dealId, $basket);
}
return $dealId;
}
/**
* Поиск контакта по телефону или email
*/
private function findContact(?string $phone, ?string $email): ?int
{
if ($phone) {
$result = $this->apiCall('crm.contact.list', [
'filter' => ['PHONE' => $phone],
'select' => ['ID'],
]);
if ($id = $result['result'][0]['ID'] ?? null) {
return (int) $id;
}
}
if ($email) {
$result = $this->apiCall('crm.contact.list', [
'filter' => ['EMAIL' => $email],
'select' => ['ID'],
]);
if ($id = $result['result'][0]['ID'] ?? null) {
return (int) $id;
}
}
return null;
}
/**
* Создание контакта
*/
private function createContact(array $data): ?int
{
$fields = [
'NAME' => $data['name'] ?: 'Покупатель',
'OPENED' => 'Y',
'ASSIGNED_BY_ID' => $this->defaultResponsibleId,
'TYPE_ID' => 'CLIENT',
'SOURCE_ID' => 'WEB',
];
if (!empty($data['phone'])) {
$fields['PHONE'] = [['VALUE' => $data['phone'], 'VALUE_TYPE' => 'MOBILE']];
}
if (!empty($data['email'])) {
$fields['EMAIL'] = [['VALUE' => $data['email'], 'VALUE_TYPE' => 'WORK']];
}
$result = $this->apiCall('crm.contact.add', [
'fields' => $fields,
'params' => ['REGISTER_SONET_EVENT' => 'Y'],
]);
return isset($result['result']) ? (int) $result['result'] : null;
}
/**
* Создание сделки
*/
private function createDeal(Order $order, int $contactId, string $comment = ''): ?int
{
$fields = [
'TITLE' => "Заказ №{$order->getId()} с сайта",
'CONTACT_ID' => $contactId,
'OPPORTUNITY' => $order->getPrice(),
'CURRENCY_ID' => $order->getCurrency(),
'ASSIGNED_BY_ID' => $this->defaultResponsibleId,
'SOURCE_ID' => 'WEB',
'STAGE_ID' => 'NEW',
'COMMENTS' => $comment,
// Пользовательские поля (если настроены)
// 'UF_CRM_ORDER_ID' => $order->getId(),
];
$result = $this->apiCall('crm.deal.add', [
'fields' => $fields,
'params' => ['REGISTER_SONET_EVENT' => 'Y'],
]);
return isset($result['result']) ? (int) $result['result'] : null;
}
/**
* Добавление товаров к сделке
*/
private function addProducts(int $dealId, Basket $basket): void
{
$rows = [];
foreach ($basket as $item) {
$rows[] = [
'PRODUCT_NAME' => $item->getField('NAME'),
'PRICE' => $item->getPrice(),
'QUANTITY' => $item->getQuantity(),
'DISCOUNT_TYPE_ID' => 2, // Процентная скидка
'DISCOUNT_RATE' => $item->getDiscountPrice() > 0
? round($item->getDiscountPrice() / $item->getBasePrice() * 100, 2)
: 0,
];
}
if (!empty($rows)) {
$this->apiCall('crm.deal.productrows.set', [
'id' => $dealId,
'rows' => $rows,
]);
}
}
/**
* Вызов API Битрикс24
*/
private function apiCall(string $method, array $params = []): array
{
$url = $this->webhookUrl . $method . '.json';
$response = $this->httpClient->post($url, $params);
if ($this->httpClient->getStatus() !== 200) {
throw new \RuntimeException(
"B24 API Error: HTTP {$this->httpClient->getStatus()}"
);
}
$data = json_decode($response, true);
if (isset($data['error'])) {
throw new \RuntimeException(
"B24 API Error: {$data['error']} - {$data['error_description']}"
);
}
return $data;
}
/**
* Получение значения свойства заказа по коду
*/
private function getPropertyValue($props, string $code): ?string
{
foreach ($props as $prop) {
if ($prop->getField('CODE') === $code) {
return $prop->getValue();
}
}
return null;
}
}Регистрация обработчика
<?php
// /local/php_interface/init.php
use Bitrix\Main\EventManager;
use Local\Integration\Bitrix24\OrderToDealService;
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderSaved',
function (\Bitrix\Main\Event $event) {
if (!$event->getParameter('IS_NEW')) {
return;
}
$order = $event->getParameter('ENTITY');
try {
$service = new OrderToDealService(
'https://your-portal.bitrix24.ru/rest/1/your-token/',
8 // ID ответственного менеджера
);
$dealId = $service->createFromOrder($order);
AddMessage2Log(
"Создана сделка #{$dealId} для заказа #{$order->getId()}",
'b24_integration'
);
} catch (\Throwable $e) {
AddMessage2Log(
"Ошибка интеграции с B24: {$e->getMessage()}",
'b24_integration'
);
}
}
);Таблица методов API
| Метод | Описание |
|---|---|
crm.contact.list | Поиск контактов |
crm.contact.add | Создание контакта |
crm.deal.add | Создание сделки |
crm.deal.productrows.set | Добавление товаров к сделке |
crm.deal.update | Обновление сделки |
Дубли: главная проблема этой интеграции
Поиск контакта по телефону перед созданием нового — правильная идея, которая работает хуже, чем ожидается. Разберём почему.
Телефон приходит в разном формате. +7 (999) 123-45-67, 89991234567, 79991234567 — для человека это один номер, для поиска по точному совпадению три разных. Нормализуйте номер перед поиском и перед сохранением: оставьте цифры, приведите к единому виду (7XXXXXXXXXX). Без этого каждый второй заказ создаёт нового контакта, и через год в CRM три карточки на одного клиента.
Заказ может отправиться дважды. Повторный запуск фонового обмена, ручной перезапуск после ошибки, событие, сработавшее второй раз, — и появляются две одинаковые сделки. Лечится отметкой на стороне сайта: в заказе хранится ID созданной сделки, и повторная синхронизация видит, что она уже есть.
Гонка при одновременных заказах. Два заказа от одного клиента в один момент: оба процесса ищут контакт, оба не находят, оба создают. Здесь помогает та же отметка плюс обработка на стороне обмена в один поток.
Лимиты и batch
Ограничение на количество запросов — не совет, а то, во что вы упрётесь при первой же массовой выгрузке. Создание одной сделки — это минимум три вызова: найти контакт, создать контакт, создать сделку, плюс четвёртый на товарные позиции.
Метод batch объединяет до 50 команд в один запрос и умеет подставлять результат предыдущей команды в следующую ($result[cmd_name]) — то есть «создать контакт и сразу привязать к нему сделку» делается одним обращением. Для регулярной выгрузки заказов это разница между «укладываемся» и «упираемся в лимит».
Итоги
Обмен — асинхронный. Оформление заказа не должно зависеть от доступности портала.
Телефон нормализуется перед поиском и перед записью. Это основная причина дублей контактов.
В заказе хранится ID созданной сделки — защита от повторной отправки и точка входа для диагностики.
Логируйте обмен, но без секретов. Лог нужен, чтобы отвечать на вопрос «а заказ точно ушёл?»; полный URL вебхука в него попадать не должен.
Нужен мониторинг. Молча сломавшаяся интеграция — это заказы, о которых отдел продаж не знает. Проверка «за последний час есть успешные синхронизации, необработанных заданий не больше N» с уведомлением обязательна.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.