01 01234567890 01234567890

Обновление сделок через REST API Bitrix24: поля, статусы, массовые операции

Разработка сайта 28 August 2026
~2 минуты чтения 2 316 символа 1 просмотр

Как обновлять поля и статусы сделок через crm.deal.update, работать с пользовательскими полями UF_* и делать массовые обновления через batch-запрос — с кодом на PHP

Менеджер по ошибке поставил не ту сумму сделки, интеграция с сайтом не подтянула телефон клиента, а стадия воронки застряла на «Переговоры» неделю назад, хотя договор уже подписан. Руками через интерфейс это правится за пять секунд. А когда таких сделок пятьсот и данные приходят из внешней системы — 1С, скоринга, парсера заявок — руками уже никак. Здесь и нужен REST API Bitrix24: обновление сделки, её полей и статуса делается одним вызовом метода crm.deal.update, а массово — через batch-запрос. В статье разберём сигнатуру метода, смену STAGE_ID, работу с пользовательскими полями UF_*, пакетное обновление и обработку ошибок. Материал для разработчиков, которые уже настроили доступ к REST API и умеют делать первый запрос — если нет, сначала настройте вебхук или регистрацию приложения.

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

  • Входящий вебхук с правами на CRM (scope crm) или REST-приложение с правом на редактирование сделок — без этого метод вернёт ошибку доступа
  • ID сделок, которые нужно обновить, и точный список полей, которые меняем — не «обновить всё», а конкретный набор
  • Список кодов стадий воронки (STAGE_ID), если задача — смена статуса; их можно получить методом crm.dealcategory.stage.list или crm.status.list
  • PHP 7.4+ и библиотека CRest (официальный REST-хелпер Bitrix24) либо просто curl — примеры ниже написаны на CRest, но логика работает и без него
  • Тестовый портал или тестовые сделки на проде — массовое обновление, отправленное не туда, откатить руками намного дольше, чем настроить

Как обновить сделку через REST API Bitrix24: разбираемся с методом crm.deal.update

Шаг 1. Смотрим на сигнатуру метода

crm.deal.update принимает три параметра: id (обязательный, идентификатор сделки), fields (объект с полями, которые нужно изменить) и params — необязательный объект с двумя ключами: REGISTER_SONET_EVENT (регистрировать ли событие в живой ленте, Y/N) и REGISTER_HISTORY_EVENT (создавать ли запись в истории изменений). Важное отличие от crm.deal.add: в fields передаются только те поля, которые меняются, остальные остаются как есть. Не нужно каждый раз пересылать весь объект сделки — это частая ошибка у тех, кто привык к PUT-семантике других API.

<?php
require_once('crest.php'); // официальный класс-хелпер CRest от Bitrix24

// Обновляем только название и сумму, остальные поля сделки не трогаем
$result = CRest::call('crm.deal.update', [
    'ID' => 4521,
    'FIELDS' => [
        'TITLE' => 'Поставка оборудования — уточнённая сумма',
        'OPPORTUNITY' => 158000,
    ],
]);

if (isset($result['result']) && $result['result'] === true) {
    echo 'Сделка обновлена';
}

Метод возвращает {"result": true} при успехе — без каких-либо данных по изменённой сделке. Если нужно увидеть итоговое состояние, придётся отдельно вызвать crm.deal.get.

Официальная документация помечает crm.deal.update как метод, развитие которого остановлено, — вместо него рекомендуют универсальный crm.item.update (тот же принцип работы с полями, но единый метод для сделок, лидов, смарт-процессов и других сущностей CRM через параметр entityTypeId). На практике crm.deal.update продолжает работать и никуда резко не пропадёт, но для нового кода в 2026 году разумнее сразу закладываться на crm.item.update — в статье используется старый метод, потому что именно он стоит в большинстве существующих интеграций и именно его чаще всего дорабатывают, а не переписывают с нуля.

Шаг 2. Обновляем отдельные поля сделки

Помимо TITLE и OPPORTUNITY, часто меняют COMMENTS, ASSIGNED_BY_ID (смена ответственного), CONTACT_IDS (массив ID контактов, именно массив, а не одно число), COMPANY_ID, CLOSEDATE. Дата передаётся строкой в формате, который понимает портал — обычно ГГГГ-ММ-ДД, но безопаснее сверяться с форматом, который возвращает crm.deal.get по этому же полю на вашем портале: локаль портала влияет на разбор даты.

<?php
// Меняем ответственного и добавляем контакт к сделке
$result = CRest::call('crm.deal.update', [
    'ID' => 4521,
    'FIELDS' => [
        'ASSIGNED_BY_ID' => 27,
        'CONTACT_IDS' => [318, 402], // именно массив ID, даже если контакт один
    ],
]);

Частая ошибка — передать CONTACT_IDS одним числом вместо массива. Запрос не упадёт с понятной ошибкой сразу везде: где-то Bitrix24 примет число и превратит его в массив из одного элемента, а где-то тихо проигнорирует поле. Лучше не проверять это опытным путём и сразу оборачивать в массив.

Ещё момент, который редко упоминают: crm.deal.update ничего не знает о том, что поле уже поменялось между тем, как вы прочитали сделку через crm.deal.get, и тем, как отправили обновление. Блокировки записи на уровне API нет — если два процесса одновременно шлют FIELDS с разными значениями одного поля, победит тот запрос, который пришёл последним. Для интеграций, где риск гонки реален (например, обновление суммы сделки одновременно из 1С и из формы на сайте), стоит либо сериализовать такие обновления на своей стороне через очередь, либо обновлять узкий набор полей, а не весь объект — тогда конфликтующих полей физически меньше.

Шаг 3. Меняем статус сделки: STAGE_ID и стадии воронки

Смена статуса — это обновление поля STAGE_ID. Формат значения зависит от того, в основной воронке сделка или в дополнительной. Для основной воронки (её CATEGORY_ID равен 0) код стадии передаётся без префикса: NEW, PREPARATION, WON, LOSE. Для дополнительной воронки (когда в компании настроено несколько направлений продаж) код стадии автоматически получает префикс с номером направления — например, стадия DECISION в воронке с CATEGORY_ID = 2 хранится как C2:DECISION. Если передать код без префикса туда, где он нужен, — Bitrix24 либо не найдёт такую стадию, либо применит её не в том направлении, если совпадение случайное.

<?php
// Переводим сделку в дополнительной воронке (CATEGORY_ID = 2) на стадию "Принятие решения"
$result = CRest::call('crm.deal.update', [
    'ID' => 4521,
    'FIELDS' => [
        'STAGE_ID' => 'C2:DECISION',
    ],
]);

Практический момент: crm.deal.update меняет стадию в пределах той воронки, где сделка уже находится. Если нужно одновременно перевести сделку в другое направление продаж — обновляйте CATEGORY_ID и соответствующий по этой воронке STAGE_ID одним вызовом, иначе стадия может не примениться корректно. Список актуальных кодов стадий для конкретной воронки удобнее получать методом crm.dealcategory.stage.list, а не хардкодить — коды может переименовать администратор портала, а сам код (в отличие от названия на экране) обычно не меняется, но полагаться на память не стоит. Этот метод, как и crm.deal.update, в документации помечен устаревшим в пользу crm.category.* — но пока рабочий и по-прежнему встречается в большинстве действующих интеграций.

Шаг 4. Работаем с пользовательскими полями UF_*

Пользовательские поля (свойства сделки, которых нет в стандартном наборе — например, «Источник заявки» или «Номер договора») хранятся под техническими именами вида UF_CRM_1234567890. Обновляются они точно так же, как обычные поля — внутри того же FIELDS.

<?php
// Записываем значение в пользовательское поле "Номер договора"
$result = CRest::call('crm.deal.update', [
    'ID' => 4521,
    'FIELDS' => [
        'UF_CRM_1699999999' => 'Д-2026-0451',
    ],
]);

Техническое имя поля не угадывается по названию в интерфейсе — его нужно смотреть в списке полей сделки (crm.deal.fields) или в разделе настройки пользовательских полей. Ещё один нюанс: поля типа «список» (enumeration) принимают не текст варианта, а его внутренний ID — если в интерфейсе выбрано «Сайт», в API нужно передать числовой идентификатор этого варианта списка, который тоже виден в crm.deal.fields внутри описания поля.

Массовое обновление через batch-запрос

Шаг 5. Собираем batch для пакетного обновления

Обновлять сделки по одной синхронными запросами — рабочий, но медленный вариант: на большинстве тарифов Bitrix24 ограничивает интенсивность запросов примерно двумя в секунду (на Enterprise — до пяти), система защиты называется «дырявое ведро» (leaky bucket) — счётчик растёт с каждым запросом и снижается со временем; при превышении портал отвечает QUERY_LIMIT_EXCEEDED с кодом 503. Метод batch позволяет отправить до 50 подзапросов одним HTTP-вызовом — это и быстрее, и меньше давит на лимит.

<?php
// Готовим массив команд: ключ — произвольный ID подзапроса, значение — метод и параметры
$commands = [];
$dealsToUpdate = [
    4521 => ['STAGE_ID' => 'C2:WON'],
    4522 => ['STAGE_ID' => 'C2:WON'],
    4523 => ['STAGE_ID' => 'C2:LOSE', 'COMMENTS' => 'Клиент отказался, бюджет заморожен'],
];

foreach ($dealsToUpdate as $dealId => $fields) {
    $commands['deal_' . $dealId] = 'crm.deal.update?' . http_build_query([
        'ID' => $dealId,
        'FIELDS' => $fields,
    ]);
}

$batchResult = CRest::call('batch', [
    'halt' => 0, // 0 — выполнять все команды, даже если одна из них упала с ошибкой
    'cmd' => $commands,
]);

halt = 0 значит, что batch не останавливается на первой ошибке — падает только тот подзапрос, где она возникла, остальные выполняются, а ошибки собираются в result_error под тем же ключом, что и сама команда. halt = 1 останавливает выполнение при первой же ошибке — подходит, когда порядок команд важен и следующие без предыдущей не имеют смысла. Вложенность запрещена: вызвать batch внутри batch нельзя. Это две разные ошибки: превышение лимита в 50 команд возвращает ERROR_BATCH_LENGTH_EXCEEDED, а попытка вложенного batch — отдельную ошибку метода, не связанную с количеством команд.

Если у вас больше 50 сделок — режьте массив на чанки по 50 и отправляйте несколько batch-запросов подряд, с небольшой паузой между ними, чтобы не упереться в тот же лимит интенсивности, от которого batch и должен был спасти.

Шаг 6. Обрабатываем ошибки обновления

Ответ batch возвращает result (успешные подзапросы) и result_error (проблемные, с ключом по имени команды). Разбирать нужно оба массива — иначе часть сделок молча останется необновлённой, а лог покажет «всё ок», потому что сам HTTP-запрос действительно завершился с кодом 200.

<?php
// Проверяем, какие сделки не обновились, и логируем причину
if (!empty($batchResult['result']['result_error'])) {
    foreach ($batchResult['result']['result_error'] as $key => $error) {
        // $error содержит error и error_description
        error_log("Сделка не обновлена ({$key}): {$error['error_description']}");
    }
}

Для одиночных вызовов crm.deal.update логика та же: успех — это result === true, а не просто отсутствие исключения. Отдельно стоит ловить QUERY_LIMIT_EXCEEDED (503) — при нём стоит повторить запрос с растущей задержкой, а не сразу же — и OPERATION_TIME_LIMIT (429), который означает, что превышен лимит по совокупному времени выполнения конкретного метода; портал подсказывает время сброса в поле operating_reset_at, до которого метод лучше не дёргать вообще.

Помимо лимитов, в result_error встречаются содержательные ошибки самого метода: ERROR_ARGUMENT, если поле или значение не подходят по формату (например, буквенный код стадии не из той воронки), и ACCESS_DENIED, если у вебхука или пользователя, от имени которого выполняется запрос, не хватает прав на редактирование конкретной сделки — это отдельно от прав на сам вебхук и завязано на права пользователя в CRM. Текст ошибки в error_description обычно достаточно конкретный, чтобы понять причину без похода в документацию — но откладывать его в лог всё равно стоит, руками пересматривать сотни сделок после ночной синхронизации никто не будет.

Частые ошибки и как их избежать

Ошибка: пересылают в FIELDS весь объект сделки, включая поля, которые не менялись. Возникает по инерции от других API с PUT-семантикой. Иногда это безобидно, но если среди пересланных полей оказалось устаревшее значение (например, взятое из кеша за час до запроса) — обновление затирает более свежие изменения, сделанные менеджером вручную за это время. Решение: формировать FIELDS только из тех полей, которые реально нужно поменять именно сейчас.

Ошибка: код стадии передают без учёта воронки. Разработчик берёт код NEW из основной воронки и использует его для сделки в дополнительном направлении продаж, где такого кода без префикса не существует. Решение: перед сменой статуса запросить актуальный список стадий воронки методом crm.dealcategory.stage.list и не хранить коды захардкоженными в коде дольше одного релиза.

Ошибка: массовое обновление без обработки result_error. Batch отдаёт 200 даже если половина подзапросов упала — и если проверять только код HTTP-ответа, кажется, что всё прошло. Решение: всегда разбирать result_error и логировать, какие именно ID не обновились и почему.

Ошибка: обновляют сделки в цикле по одной без учёта лимита запросов. При синхронизации из внешней системы на 300+ сделок легко влететь в QUERY_LIMIT_EXCEEDED посреди процесса, если слать запросы без паузы. Решение: собирать команды в batch по 50 штук, а не слать поштучно там, где это не обязательно.

Из практики А2

На одном проекте по интеграции с 1С нужно было синхронизировать статусы оплаты по сделкам несколько раз в день — примерно пара сотен изменений за проход. Сначала сделали синхронный цикл с одиночными вызовами crm.deal.update — работало, но иногда падало с QUERY_LIMIT_EXCEEDED на середине списка, потому что в это же время с тем же порталом работали менеджеры, и их запросы из интерфейса расходовали тот же лимит интенсивности. Переписали на batch по 50 команд за раз с паузой в секунду между пакетами — с тех пор лимит не упирался ни разу за несколько месяцев эксплуатации.

Отдельно наткнулись на то, что часть сделок хранилась в дополнительной воронке для оптовых клиентов, и код стадии WON, взятый из основной воронки, там просто не срабатывал — batch возвращал result_error с кодом ERROR_ARGUMENT, а не молча игнорировал. Разобрались только когда стали действительно читать result_error, а не полагаться на факт, что HTTP-код ответа был 200. После этого добавили в скрипт синхронизации кеш соответствия CATEGORY_ID → допустимые STAGE_ID, который обновляется раз в сутки — обращаться к crm.dealcategory.stage.list на каждую сделку было бы лишним запросом, а хардкодить коды стадий, как выяснилось на этом же проекте, плохая идея, потому что администратор портала иногда пересобирает воронки.

Итог

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

Интеграция бизнес-процессов Bitrix24 с внешними системами через webhook
Следующая статья 25.08.2026