Выгрузка в Яндекс Маркет на Битриксе: ошибки, которые стоят офферов
Файл отдаётся, товары грузятся, а в кабинете Маркета половина офферов висит с несовпадающей ценой и пустыми категориями. Разбираем, где в связке «Битрикс — YML» рвётся чаще всего и чем платит каждый способ выгрузки.

Картина, из-за которой чаще всего приходят за помощью: магазин на 1С-Битрикс отдаёт YML, робот Маркета файл забирает, но в кабинете часть офферов отклонена из-за несовпадения цены, часть товаров уехала как доступная при нулевом остатке, а торговые предложения разъехались по отдельным карточкам вместо одной. Файл формально валиден. Проблема не в XML, а в том, какие данные Битрикс в него кладёт: базовый тип цены вместо той, что видит покупатель, остаток без учёта настроек покупки, ID раздела инфоблока в categoryId. Ориентируемся на 1С-Битрикс: Управление сайтом 23.x и новее с модулями iblock и catalog, PHP 8.1+.
Где рвётся чаще всего
Почти все инциденты с выгрузкой на Битриксе сводятся к шести причинам. Они не про формат — про расхождение файла и сайта.
- Цена из прайса не равна цене на сайте. Штатная выгрузка берёт тот тип цены, который указан в профиле. Скидки, акции и персональные цены при этом остаются на сайте. Маркет сверяет цену в файле с ценой на карточке товара и при расхождении снимает оффер с продажи.
- Наличие не связано с остатком. Если в карточке товара включено «Разрешить покупку при отсутствии товара», нулевой остаток не мешает выгрузке отдать товар как доступный. Дальше — отмены заказов и падение рейтинга продавца.
- Торговые предложения без
group_id. Красная и синяя футболка уезжают как два независимых оффера. Варианты не склеиваются в одну карточку, а цену и URL Битрикс при этом может взять из разных мест: цену у предложения, ссылку у родителя. - Относительные URL.
/upload/iblock/...вместо полного адреса — классика кастомных шаблонов выгрузки. То же с картинками и с доменом из тестового окружения, который остался в настройках профиля. - Генерация в момент запроса. Файл профиля в
/bitrix/catalog_export/собирает выгрузку на лету. На каталоге в десятки тысяч предложений это минуты работы PHP и MySQL: робот ловит таймаут или получает обрезанный XML, а сайт в это время тормозит для живых посетителей. - Кодировка и экранирование. Старые проекты на Битриксе часто живут в windows-1251. Если в объявлении XML написана одна кодировка, а в данных другая, парсер падает на первом же тексте с кириллицей. Неэкранированный амперсанд в названии товара ломает файл не хуже.
Остатки по нескольким складам и выгрузка сроков доставки — отдельная тема, здесь разбираем только базовую связку «цена — наличие — идентификаторы».
Четыре способа отдавать каталог Маркету
Штатный профиль экспорта
Настраивается в админке в разделе экспорта данных модуля «Интернет-магазин» (точное положение пункта меняется между редакциями), публичный файл профиля обычно лежит в /bitrix/catalog_export/ — конкретный URL виден в настройках профиля. Выигрыш: ноль кода, настраивается за полчаса, обновляется вместе с платформой. Цена: вы ограничены тем набором полей, который умеет профиль. Тонкая логика вроде «цену берём со скидкой, но только для той группы пользователей, под которой ходит робот» в настройках не выражается.
Свой скрипт на cron
Собственный генератор на XMLWriter, который пишет статический файл по расписанию. Выигрыш: полный контроль над каждым элементом, предсказуемое время генерации, нулевая нагрузка на отдачу — робот качает готовый файл с диска. Цена: это ваш код. Поменялись требования Маркета к формату — правите вы, а не вендор.
Готовый модуль из Маркетплейса
Выигрыш: автор следит за изменениями формата, обычно есть маппинг характеристик на параметры Маркета и фильтры по разделам. Цена: подписка, чужой код в проекте и зависимость от того, живой ли модуль. Перед покупкой смотрите дату последнего обновления и совместимость с вашей версией главного модуля и PHP.
Partner API вместо файла
Для схем FBS и DBS цены и остатки заливаются через API Яндекс Маркета, а не прайс-листом. Выигрыш: обновление остатка доходит до Маркета за минуты, а не за сутки. Цена: интеграция сложнее, нужны ретраи, очередь и журнал — и это уже не задача вебмастера. Актуальные имена методов и лимиты частоты сверяйте в документации Partner API: они менялись, и подставлять их по памяти здесь вреднее, чем не называть вовсе.
| Способ | Что выигрываете | Чем платите | Для каких каталогов |
|---|---|---|---|
| Штатный профиль | Настройка без кода, поддержка вендора | Жёсткий набор полей, генерация по запросу | До нескольких тысяч предложений, простые цены |
| Свой скрипт + cron | Контроль логики, статический файл, предсказуемая нагрузка | Своя поддержка при смене требований | Десятки тысяч SKU, скидки, несколько складов |
| Модуль из Маркетплейса | Маппинг параметров, обновления под формат | Подписка, чужой код, риск заброшенности | Типовой магазин без нестандартной логики цен |
| Partner API | Остатки и цены почти в реальном времени | Полноценная интеграция с очередью и логами | FBS/DBS, частые изменения цен |
Цена: главная причина снятых офферов
Не выгружайте цену напрямую из таблицы цен. Берите ту, которую посчитал бы Битрикс для анонимного посетителя — то есть для группы «Все пользователи» (по умолчанию ID 2).
<?php
// 1С-Битрикс 23.x+, модуль catalog. Legacy-метод, но рабочий и стабильный
CModule::IncludeModule('catalog');
$guestGroupId = 2; // «Все пользователи» — под этой группой каталог видит робот
$optimal = CCatalogProduct::GetOptimalPrice($offerId, 1, [$guestGroupId], 'N');
if (!$optimal || empty($optimal['RESULT_PRICE'])) {
return null; // без цены оффер в YML не отдаём вообще
}
$price = $optimal['RESULT_PRICE']['DISCOUNT_PRICE'];
$basePrice = $optimal['RESULT_PRICE']['BASE_PRICE'];
$currency = $optimal['RESULT_PRICE']['CURRENCY'];
// oldprice имеет смысл только если он реально выше итоговой цены
$oldPrice = ($basePrice > $price) ? $basePrice : null;
GetOptimalPrice учитывает скидки на товар. Скидки, которые считаются только в корзине — от суммы заказа, по купону, — в прайс попасть не могут и не должны: Маркет сверяет цену карточки, а не финальную цену заказа. Если на сайте такие скидки показываются прямо в карточке, у вас конфликт на уровне маркетинга, а не кода, и решать его придётся переносом скидки в цену товара.
Отдельная ловушка — валюта. В YML допустимы и RUR, и RUB, но код в <currencyId> обязан совпадать с одним из объявленных в блоке <currencies>. Расхождение здесь валит весь файл, а не отдельный оффер.
Маркет не читает ваш сайт — он читает файл. Если выгрузка собрана в 3:00, а акция включилась в 10:00, спор с модерацией вы проиграете: расхождение цены фиксируется по тому, что отдал прайс.
Идентификаторы и торговые предложения
В id оффера кладите ID элемента инфоблока, а не артикул. Артикул пустой у половины каталога, дублируется у поставщиков и меняется при обновлении из 1С — любой из этих случаев даёт либо потерю оффера, либо коллизию. В group_id идёт ID родительского товара: именно он склеивает варианты в одну карточку. Ограничения на длину значения смотрите в справке Маркета — оно должно быть целым числом, и длинные составные ключи вида 12345-RED-XL там не пройдут.
$xml->startElement('offer');
$xml->writeAttribute('id', (string)$offerId); // ID торгового предложения
$xml->writeAttribute('group_id', (string)$productId); // ID родительского товара
// XMLWriter сам экранирует &, < и > — руками htmlspecialchars делать не нужно
$xml->writeElement('name', $offerName);
$xml->writeElement('url', self::SITE_URL . $detailPageUrl); // только абсолютный адрес
$xml->writeElement('price', number_format($price, 2, '.', ''));
$xml->writeElement('currencyId', 'RUR');
$xml->writeElement('categoryId', (string)$sectionId);
$xml->writeElement('picture', self::SITE_URL . CFile::GetPath($pictureId));
$xml->writeElement('count', (string)$quantity);
$xml->endElement();
С categoryId есть неочевидная мина: это ID раздела инфоблока. При пересоздании структуры или переносе каталога с другого сервера ID меняются, и Маркет получает новое дерево категорий — привязка товаров к категориям в кабинете слетает. Если структура каталога нестабильна, выгружайте категории по собственному стабильному ключу, а не по IBLOCK_SECTION_ID напрямую.
Атрибут available и элемент <count> поддерживаются не во всех схемах работы одинаково, и требования к ним Яндекс правил. Перед релизом сверьтесь со справкой для своей модели продаж, а в коде заведите явное правило: товар с нулевым остатком не попадает в файл либо уходит с нулевым count, независимо от флага «Разрешить покупку при отсутствии».
Генерация: только из cron и только с атомарной подменой
Статический файл вместо генерации по запросу снимает сразу две проблемы: таймаут робота и нагрузку на сайт. Важная деталь, которую часто пропускают, — запись через временный файл. Если писать прямо в market.xml, робот рано или поздно скачает выгрузку на середине записи и получит битый XML.
<?php
// /home/bitrix/scripts/yml_export.php — запускается из crontab, не через веб
$_SERVER['DOCUMENT_ROOT'] = '/home/bitrix/www';
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
$targetFile = $_SERVER['DOCUMENT_ROOT'] . '/upload/yml/market.xml';
$tempFile = $targetFile . '.tmp';
$xml = new XMLWriter();
$xml->openUri($tempFile);
$xml->setIndent(true);
$xml->startDocument('1.0', 'UTF-8');
$xml->startElement('yml_catalog');
$xml->writeAttribute('date', date('Y-m-d H:i'));
$xml->startElement('shop');
// здесь пишем shop/name, company, url, currencies, categories и offers
$xml->endElement(); // shop
$xml->endElement(); // yml_catalog
$xml->endDocument();
$xml->flush();
// подмена одним системным вызовом: робот не увидит половину файла
rename($tempFile, $targetFile);
Кодировку в startDocument ставьте ту, в которой реально отдаёте данные. На сайте в windows-1251 либо объявляйте windows-1251, либо перекодируйте строки в UTF-8 перед записью — третьего варианта нет, и симптом у ошибки всегда один: not well-formed (invalid token) на первой строке с кириллицей.
Когда что выбирать
- До 2–3 тысяч предложений, цены меняются раз в неделю, скидок нет. Штатный профиль. Не изобретайте велосипед, просто проверьте в файле тип цены и наличие.
- Больше 10 тысяч предложений или генерация дольше 30 секунд. Свой скрипт на cron со статическим файлом. Критерий простой: если профиль не успевает отдать файл за таймаут вашего веб-сервера, обсуждать больше нечего.
- Нужен маппинг характеристик инфоблока на параметры Маркета, но своего разработчика нет. Модуль из Маркетплейса с актуальной датой обновления.
- FBS или DBS, остатки меняются в течение дня. Partner API. Прайс-лист здесь всегда будет отставать, сколько бы раз в сутки вы его ни собирали.
Что ломается после запуска
- Файл перестал обновляться, а Маркет молчит. Самая дорогая ошибка: робот сутками качает вчерашние цены. Проверяйте не факт существования файла, а дату в атрибуте
dateкорневого элемента и настройте оповещение, если она старше суток. - Права на запись. Скрипт из cron работает под другим пользователем, чем PHP на сайте. Классический финал —
rename(): Permission deniedв логе cron и старый файл на диске. - Память на больших каталогах. Выбирайте товары постранично, а не одним
GetListбез ограничений.XMLWriterс записью в файл память не копит, в отличие от сборки гигантской строки. - Товары-«призраки». Элемент снят с публикации или у него закончился срок активности, а из выгрузки он не исчез, потому что в фильтре забыли
ACTIVEиACTIVE_DATE. - Тестовый домен в ссылках. После переноса с stage в URL остаётся адрес разработки. Вынесите домен в одну константу и проверяйте её в выгрузке автотестом перед деплоем.
Что делать дальше
Прогоните готовый файл через проверку прайс-листа в кабинете Маркета — она показывает не только синтаксические ошибки, но и офферы без обязательных полей. Затем повесьте простой мониторинг: раз в час сравнивайте дату в yml_catalog с текущим временем и размер файла с прошлым значением. Падение размера на десятки процентов почти всегда означает сломанный фильтр, а не распродажу каталога. И раз в месяц выборочно сверяйте цены пяти случайных офферов из файла с карточками на сайте: это две минуты работы, которые ловят расхождение раньше, чем его найдёт модерация.
YML (Yandex Market Language) — XML-формат прайс-листа Яндекса на основе RSS: корневой элемент yml_catalog, внутри блоки магазина, валют, категорий и офферов.
Торговое предложение (SKU) — в терминах Битрикса отдельный элемент дочернего инфоблока, привязанный к товару: конкретный цвет и размер со своей ценой и остатком. В YML такие элементы объединяются через group_id.
Описание методов работы с ценами и каталогом — в документации разработчика 1С-Битрикс.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: Документация разработчика 1С-Битрикс: методы модулей catalog и iblock, Справка Яндекс Маркета для продавцов: требования к прайс-листу в формате YML
Частые вопросы
У меня Битрикс старой версии и PHP 7.4. Код из статьи заработает?
Маркет пишет, что цена в прайсе не совпадает с ценой на сайте, хотя визуально цены одинаковые. Где искать?
Файл генерируется, но робот скачивает его битым. Что проверить первым?
Как часто нужно обновлять выгрузку?
Можно отдавать Маркету только часть каталога?
Стоит ли выгружать товары с нулевым остатком?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.