Метод crm.deal.productrows.set - рабочая лошадка при интеграции интернет-магазина или бота с Битрикс24: через него я пробрасываю в сделку список товаров с ценами, количеством и скидками почти в каждом проекте, где CRM синхронизируется с сайтом или ботом. За несколько лет работы с REST API Битрикс24 набил на этом методе достаточно шишек, чтобы рассказать не только сигнатуру из документации, но и то, что в ней явно не написано.
Что делает crm.deal.productrows.set и чем он отличается от add
Метод принимает ID сделки и массив товарных строк, а на выходе полностью пересобирает список товаров в этой сделке. Это не add в смысле «добавить ещё одну позицию» - это именно set, то есть замена всего содержимого разом. Если в сделке уже было пять товаров, а вы отправили массив из трёх других строк, останется ровно три - старые пять исчезнут независимо от того, упоминали вы их или нет.
Логика такая же, как у PUT в REST: вы не патчите отдельное поле, а присылаете полное новое состояние. У Битрикс24 нет отдельного метода вроде productrows.add для точечного добавления одной позиции в существующий список - если нужно дописать товар, придётся сначала прочитать текущий список через crm.deal.productrows.get, дополнить массив в коде и уже потом вызвать set с полным набором.
На практике это первое, что ломает интеграции у новичков: бот или скрипт синхронизации вызывает set при каждом добавлении товара в корзину, передавая только новый товар - и стирает всё, что было добавлено раньше.
Формат запроса и обязательные поля товарной строки
Каждый элемент массива rows - это объект с набором полей. На практике достаточно такого минимума:
PRODUCT_ID- ID товара из каталога Битрикс24; можно передать 0, если товара нет в каталогеPRODUCT_NAME- обязателен, если PRODUCT_ID равен 0, иначе название подтянется из карточки товараPRICE- цена за единицу с учётом валюты сделкиQUANTITY- количество, число, а не строкаTAX_RATEиTAX_INCLUDED- ставка налога и признак, включён ли он в ценуDISCOUNT_TYPE_IDиDISCOUNT_SUMилиDISCOUNT_RATE- тип и размер скидкиMEASURE_CODE- код единицы измерения (796 для «шт.» по классификатору ОКЕИ)
Вызов через вебхук выглядит так:
curl -X POST
"https://your-domain.bitrix24.ru/rest/1/webhook_code/crm.deal.productrows.set"
-H "Content-Type: application/json"
-d '{
"id": 4521,
"rows": [
{
"PRODUCT_ID": 118,
"PRICE": 3200,
"QUANTITY": 2,
"TAX_RATE": 20,
"TAX_INCLUDED": "Y"
},
{
"PRODUCT_ID": 0,
"PRODUCT_NAME": "Доставка СДЭК",
"PRICE": 450,
"QUANTITY": 1,
"TAX_RATE": 0
}
]
}'
Вторая строка в примере - не товар из каталога, а строка доставки СДЭК, посчитанная заранее через калькулятор служб доставки. Такой приём я использую постоянно: считаю стоимость доставки на бэкенде и добавляю её отдельной позицией в сделку, чтобы менеджер видел итоговую сумму заказа целиком, без переключения между вкладками CRM.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Почему set затирает старые строки и как обновить только одну позицию
Поскольку метод переписывает список целиком, любое частичное обновление сводится к трём шагам: получить текущие строки через crm.deal.productrows.get, найти или изменить нужный элемент массива в коде, отправить обратно весь массив через set. Пропускать первый шаг нельзя - даже если вы хотите поменять количество только у одной позиции.
В n8n это обычно оформляется как HTTP Request на get, дальше Function-нода с правкой JSON, и снова HTTP Request на set. Экономить на шаге чтения не стоит: я видел проект, где интеграция с сайтом на Tilda писала товары в сделку напрямую при каждом изменении корзины, без предварительного get - в результате при повторном оформлении заказа тем же клиентом старые позиции из первой сделки просто исчезали, потому что второй вызов их не учитывал.
Скидки, налоги и валюта в товарных строках
Поля скидки путают чаще всего. DISCOUNT_TYPE_ID принимает два значения: 1 - скидка в процентах (тогда используется DISCOUNT_RATE), 2 - скидка в валюте сделки (тогда используется DISCOUNT_SUM). Если передать оба поля одновременно без указания типа, Битрикс24 применит то, что соответствует DISCOUNT_TYPE_ID, а второе поле молча проигнорирует - итоговая сумма разойдётся с ожидаемой, и разбираться придётся через logs интеграции.
| Поле | Что задаёт | Частая ошибка |
|---|---|---|
| DISCOUNT_TYPE_ID = 1 | Скидка в процентах через DISCOUNT_RATE | Указывают DISCOUNT_SUM вместо DISCOUNT_RATE |
| DISCOUNT_TYPE_ID = 2 | Скидка суммой через DISCOUNT_SUM | Забывают пересчитать PRICE, если скидка уже включена в цену от поставщика |
| TAX_INCLUDED | Включён ли налог в PRICE | Ставят Y, но передают цену без налога - итог по сделке занижается |
| Валюта строки | Берётся из валюты сделки, отдельного поля нет | Пытаются передать цену в другой валюте без конвертации |
Отдельно стоит помнить про PRICE_EXCLUSIVE - цену без учёта скидки, которая используется для отображения зачёркнутой цены в некоторых представлениях каталога. Если её не передать, Битрикс24 подставит значение PRICE, и зачёркнутая цена в интерфейсе сделки просто не появится.
Типичные ошибки при вызове productrows.set
За практику встречались одни и те же грабли:
- PRODUCT_ID = 0 без PRODUCT_NAME - строка сохраняется с именем «Без названия», и менеджер не понимает, что за товар в сделке
- QUANTITY передан строкой вместо числа - часть SDK и обёрток над REST API это глотают, но при вызове напрямую через batch запрос может вернуть ошибку валидации по конкретной строке массива
- Смешение старого каталога (crm_product) и Универсальных элементов CRM - если магазин работает через новый каталог второй версии, для позиций нужен ownerType/ownerId и соответствующие методы, а productrows.set остаётся привязан именно к сделкам
- Превышение лимита запросов - облачный Битрикс24 ограничивает REST API примерно двумя запросами в секунду на приложение, и синхронизация заказов из WooCommerce или Tilda пачками легко в это упирается; спасает batch-запрос, где productrows.set и update сделки уходят одним HTTP-вызовом
- Права пользователя, от имени которого выполнен вебхук - если у него нет доступа на редактирование сделки конкретной воронки, метод вернёт ошибку ACCESS_DENIED, а не тихо проигнорирует товары
Как я собираю строки товаров в реальных интеграциях
В проектах с ботом на aiogram цепочка обычно такая: пользователь набирает корзину в Telegram, бот на вебхуке создаёт сделку через crm.deal.add, затем формирует массив rows из содержимого корзины и вызывает productrows.set вторым запросом - сразу двумя вызовами в batch, чтобы не терять время на round-trip.
При синхронизации заказов из WooCommerce в Bitrix24 логика немного другая: заказ уже содержит товарные позиции с ID из внешнего магазина, и перед вызовом set приходится сопоставлять внешние ID с ID товаров в каталоге Битрикс24 - обычно через таблицу соответствий, которую храню в самой CRM в виде пользовательского поля товара.
В n8n такие сценарии собираются без единой строчки бэкенд-кода: вебхук от Tilda или CDEK-калькулятора триггерит workflow, Function-нода превращает данные заказа в массив rows нужного формата, а HTTP Request-нода отправляет его в productrows.set. Формат ноды на JavaScript для сборки строк выглядит так:
const rows = items.map(item => ({
PRODUCT_ID: item.json.bitrixProductId || 0,
PRODUCT_NAME: item.json.title,
PRICE: item.json.price,
QUANTITY: item.json.qty,
TAX_RATE: 20,
TAX_INCLUDED: 'Y'
}));
return [{ json: { id: dealId, rows } }];
Готовые заготовки под такие связки - от синхронизации заказов до сборки товарных строк для ботов - я собираю в библиотеке готовых скриптов, чтобы не переписывать одну и ту же обвязку на каждом новом проекте.
Чтобы сайт работал без сбоев
Техподдержка
от 15 000 ₽/мес
Подробнее →Частые вопросы
Можно ли обновить количество одного товара, не трогая остальные позиции сделки?
Напрямую нет: метод всегда заменяет весь список строк. Сначала читаете текущие позиции через crm.deal.productrows.get, меняете нужное поле в массиве на стороне вашего кода, потом отправляете весь массив обратно через productrows.set.
Как добавить в сделку позицию, которой нет в каталоге товаров?
Передайте PRODUCT_ID равным 0 и обязательно укажите PRODUCT_NAME с ценой и количеством. Так удобно добавлять доставку, сборку или разовую услугу, которую заводить в каталог нет смысла.
Что вернёт метод, если в одной из строк массива ошибка?
Запрос отклоняется целиком - частичного сохранения нет. Если из десяти строк в одной неверный тип поля или недопустимое значение DISCOUNT_TYPE_ID, не сохранится ни одна позиция, и в сделке останется прежний список товаров.Почему сумма сделки не совпадает с суммой по товарным строкам?
Обычно расхождение из-за налогов: TAX_INCLUDED влияет на то, входит ли TAX_RATE в PRICE, и если это поле проставлено неверно, Битрикс24 считает итог иначе, чем ожидает интеграция. Второй частый источник - скидка, добавленная одновременно и на уровне сделки, и на уровне отдельной строки товара, из-за чего она применяется дважды.