# Битрикс24: создание сделки из заказа интернет-магазина

URL: https://dmeshcheryakov.ru/blog/bitrix24-deal-from-order/
Раздел: 1С-Битрикс
Теги: Битрикс24, 1С-Битрикс, CRM, Интеграция, PHP
Опубликовано: 2022-11-12
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

> Автоматическое создание сделки в CRM при оформлении заказа на сайте 1С-Битрикс. Поиск/создание контакта, привязка товаров.

Интеграция «заказ на сайте → сделка в Битрикс24» — одна из самых востребованных доработок и одновременно одна из самых аварийных. Причина в том, что она связывает две независимые системы в момент, критичный для бизнеса: оформление заказа.

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

## Что должна делать интеграция заказа со сделкой

При оформлении заказа в интернет-магазине на 1С-Битрикс автоматически создавать сделку в Битрикс24:

1. Найти существующего контакта по телефону/email
2. Если не найден — создать нового
3. Создать сделку и привязать к контакту
4. Добавить товары из корзины в сделку

**Заказ не должен зависеть от доступности CRM.** Если код создания сделки выполняется синхронно в обработчике оформления заказа, то при недоступности портала (обновление, сбой сети, превышенный лимит) покупатель увидит ошибку и уйдёт. Магазин перестал продавать, потому что недоступна CRM, — ситуация, которую бизнес не простит.

Правильная схема: обработчик события сохраняет задание на синхронизацию (запись в своей таблице), а обмен выполняет фоновый процесс — cron-скрипт или агент. Заказ создаётся всегда; сделка появляется через минуту, и никого это не беспокоит.

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

## Подготовка

### Создание вебхука в Битрикс24

1. Перейдите в **Разработчикам → Другое → Входящий вебхук**
2. Выберите права доступа:
   - `crm` — работа с CRM
   - `user` — информация о пользователях
3. Скопируйте URL вебхука

URL будет вида: `https://your-portal.bitrix24.ru/rest/1/abc123xyz456/`

**Этот URL целиком является секретом** — токен встроен прямо в адрес. Не коммитьте его, не логируйте полный адрес запроса, не пересылайте в мессенджере. Место ему в конфигурации вне репозитория.

И выдавайте вебхуку минимально необходимые права. Для этой задачи нужны `crm` и `user`; отмечать всё подряд «чтобы точно работало» — значит превратить утёкший URL из проблемы в катастрофу.

## Базовая реализация

Добавьте в `/local/php_interface/init.php`:

```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
<?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
<?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` | Обновление сделки |

## Почему в CRM появляются дубли контактов

Поиск контакта по телефону перед созданием нового — правильная идея, которая работает хуже, чем ожидается. Разберём почему.

**Телефон приходит в разном формате.** `+7 (999) 123-45-67`, `89991234567`, `79991234567` — для человека это один номер, для поиска по точному совпадению три разных. Нормализуйте номер перед поиском и перед сохранением: оставьте цифры, приведите к единому виду (`7XXXXXXXXXX`). Без этого каждый второй заказ создаёт нового контакта, и через год в CRM три карточки на одного клиента.

**Заказ может отправиться дважды.** Повторный запуск фонового обмена, ручной перезапуск после ошибки, событие, сработавшее второй раз, — и появляются две одинаковые сделки. Лечится отметкой на стороне сайта: в заказе хранится ID созданной сделки, и повторная синхронизация видит, что она уже есть.

**Гонка при одновременных заказах.** Два заказа от одного клиента в один момент: оба процесса ищут контакт, оба не находят, оба создают. Здесь помогает та же отметка плюс обработка на стороне обмена в один поток.

## Лимиты и batch

Ограничение на количество запросов — не совет, а то, во что вы упрётесь при первой же массовой выгрузке. Создание одной сделки — это минимум три вызова: найти контакт, создать контакт, создать сделку, плюс четвёртый на товарные позиции.

Метод `batch` объединяет до 50 команд в один запрос и умеет подставлять результат предыдущей команды в следующую (`$result[cmd_name]`) — то есть «создать контакт и сразу привязать к нему сделку» делается одним обращением. Для регулярной выгрузки заказов это разница между «укладываемся» и «упираемся в лимит».

## Итоги

**Обмен — асинхронный.** Оформление заказа не должно зависеть от доступности портала.

**Телефон нормализуется** перед поиском и перед записью. Это основная причина дублей контактов.

**В заказе хранится ID созданной сделки** — защита от повторной отправки и точка входа для диагностики.

**Логируйте обмен, но без секретов.** Лог нужен, чтобы отвечать на вопрос «а заказ точно ушёл?»; полный URL вебхука в него попадать не должен.

**Нужен мониторинг.** Молча сломавшаяся интеграция — это заказы, о которых отдел продаж не знает. Проверка «за последний час есть успешные синхронизации, необработанных заданий не больше N» с уведомлением обязательна.