1С и Wildberries: интеграция остатков, цен и заказов

Дата: 09.10.2026 17:28:22
Wildberries
Маркетплейсы
Интеграция 1С
HTTP и JSON
Программирование 1С
Комментарий:
0
Платформа:
8.3
Конфигурация:
Любая
Формы:
Любые

1С и Wildberries связывают тремя способами: типовым механизмом конфигурации (если он есть в вашем релизе), готовым коннектором или своим модулем на WB API. Ниже — что выгружать из 1С на маркетплейс и что забирать обратно, код своего модуля (токен, сопоставление по баркодам, остатки, цены, сборочные задания FBS) и типичные проблемы с баркодами, складами и лимитами. Методы сверены с документацией dev.wildberries.ru в октябре 2026 года; WB часто меняет API, перед запуском проверьте их ещё раз.

Интеграция 1С и Wildberries: три варианта

ВариантКогда подходитЧто проверить
Типовой механизмработа с WB заявлена в описании релизакакие модели и данные поддержаны
Готовый коннекторстандартные процессы, нужен быстрый стартрасширение вместо правки типовой, поддержка вашей конфигурации, скорость обновлений под изменения API
Свой модульнесколько кабинетов и складов, своя схема резервовгде хранится токен, где видны ошибки, кто следит за изменениями API

С типовым механизмом осторожнее. В выгрузке УТ 11.4.14 объектов для маркетплейсов нет. В КА 2.5.20 интеграция с маркетплейсами есть, но в перечислении «Виды маркетплейсов» только Ozon и Яндекс Маркет (подробно — в статье про 1С и Ozon). Есть ли Wildberries в более новых релизах УТ 11.5, КА 2.5, ERP, БП и УНФ, смотрите в описании релиза на its.1c.ru. Лимиты WB считаются на аккаунт продавца, поэтому коннектор и ваши доработки делят один бюджет запросов.

Общая схема своего модуля — справочник кабинетов, регистры сопоставления, регламентное задание, создание заказа клиента — та же, что в статье про 1С и Яндекс Маркет. Здесь — только специфика Wildberries.

Что выгружать в WB и что загружать в 1С

ДанныеНаправлениеМетод WB API
Карточки, размеры, баркодыWB → 1СPOST /content/v2/get/cards/list
Остатки на складах продавца1С → WBPUT /api/v3/stocks/{warehouseId}
Цены и скидки1С → WBPOST /api/v2/upload/task
Новые сборочные задания FBSWB → 1СGET /api/v3/orders/new
Поставки FBS1С → WBPOST /api/v3/supplies и другие методы поставок
Продажи, финансовые отчётыWB → 1Скатегории «Статистика» и «Финансы»

Главная путаница — в идентификаторах: артикул продавца vendorCode, артикул WB nmID (карточка), ID размера chrtID, баркоды размера skus. Цены задаются на карточку, остатки — на размер, причём в карточке пишут chrtID, а в остатках и заданиях — chrtId. Поэтому сопоставление в 1С ведут по размеру. Остатки на складах WB продавец не выгружает: метод работает только со складами продавца.

Токен WB API и обёртка для вызовов

Токен создают в личном кабинете продавца, отмечая категории: модулю нужны «Контент», «Цены и скидки» и «Маркетплейс». У каждой категории свой хост: content-api.wildberries.ru, discounts-prices-api.wildberries.ru, marketplace-api.wildberries.ru. Продлить токен нельзя — по окончании срока выпускают новый. В запросе он передаётся в заголовке Authorization как есть, без Bearer.

Объекты расширения условные: справочник КабинетыWB (вид цен, скидка), регистры сведений ТоварыWB (кабинет и ID размера → номенклатура, характеристика, артикул WB, баркод, артикул продавца), СкладыWB (ID склада WB → склад 1С) и СборочныеЗаданияWB (ID задания → заказ клиента). Токен лежит в безопасном хранилище БСП, форма кабинета пишет его через ОбщегоНазначения.ЗаписатьДанныеВБезопасноеХранилище(Кабинет, Токен, "ТокенWB"). Расширение подключайте без безопасного режима или с профилем безопасности. Основы — в статье HTTP-запрос из 1С.

// Общий модуль ОбменWildberries (условное имя): флажок «Сервер», остальные сняты.
// Справочник КабинетыWB и регистры сведений ТоварыWB, СкладыWB, СборочныеЗаданияWB —
// условные объекты расширения, их структура описана в тексте статьи.

// Вызывает метод WB API. Хост — домен категории: content-api.wildberries.ru,
// discounts-prices-api.wildberries.ru, marketplace-api.wildberries.ru.
// Исключений не вызывает: продолжать ли обмен, решает вызывающий код.
Функция ВызватьWB(Кабинет, Хост, HTTPМетод, АдресРесурса, Данные = Неопределено) Экспорт

    Результат = Новый Структура("Успех, КодСостояния, Ответ, ТекстОшибки, МожноПродолжить",
        Ложь, 0, Неопределено, "", Ложь);
    Операция = HTTPМетод + " " + АдресРесурса;

    УстановитьПривилегированныйРежим(Истина);
    Токен = ОбщегоНазначения.ПрочитатьДанныеИзБезопасногоХранилища(Кабинет, "ТокенWB");
    УстановитьПривилегированныйРежим(Ложь);
    Если Не ЗначениеЗаполнено(Токен) Тогда
        Результат.ТекстОшибки = "Не задан токен WB API";
        ЗаписатьЖурналWB(Кабинет, Операция + ": " + Результат.ТекстОшибки);
        Возврат Результат;
    КонецЕсли;

    Заголовки = Новый Соответствие;
    Заголовки.Вставить("Authorization", Токен); // сам токен, без префикса Bearer
    Заголовки.Вставить("Accept", "application/json");
    ЗапросHTTP = Новый HTTPЗапрос(АдресРесурса, Заголовки);
    Если Данные <> Неопределено Тогда
        ЗапросHTTP.Заголовки.Вставить("Content-Type", "application/json");
        ЗапросHTTP.УстановитьТелоИзСтроки(ЗначениеВJSON(Данные), КодировкаТекста.UTF8,
            ИспользованиеByteOrderMark.НеИспользовать);
    КонецЕсли;

    Попытка
        Соединение = Новый HTTPСоединение(Хост, 443, , , , 60, Новый ЗащищенноеСоединениеOpenSSL());
        Ответ = Соединение.ВызватьHTTPМетод(HTTPМетод, ЗапросHTTP);
    Исключение
        Результат.ТекстОшибки = КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
        ЗаписатьЖурналWB(Кабинет, Операция + ": " + Результат.ТекстОшибки);
        Возврат Результат;
    КонецПопытки;

    Результат.КодСостояния = Ответ.КодСостояния;
    Результат.Успех = (Ответ.КодСостояния >= 200 И Ответ.КодСостояния < 300);
    ТелоОтвета = Строка(Ответ.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
    Если ЗначениеЗаполнено(ТелоОтвета) Тогда
        Попытка
            Результат.Ответ = ЗначениеИзJSON(ТелоОтвета);
        Исключение
            Результат.Ответ = Неопределено; // не JSON: начало тела попадет в текст ошибки
        КонецПопытки;
    КонецЕсли;

    // 400 и 409 — ошибка в данных этой пачки, следующую отправлять можно.
    // 401, 403, 429 и 5xx повторятся на любом запросе: обмен ждет следующего запуска.
    Результат.МожноПродолжить = Результат.Успех
        Или Ответ.КодСостояния = 400 Или Ответ.КодСостояния = 409;
    Если Результат.Успех Тогда
        Возврат Результат;
    КонецЕсли;

    Результат.ТекстОшибки = ТекстОшибкиWB(Результат.Ответ, ТелоОтвета);
    Если Ответ.КодСостояния = 429 Тогда
        ПаузаСекунд = ЗначениеЗаголовка(Ответ.Заголовки, "X-Ratelimit-Retry");
        Результат.ТекстОшибки = СтрШаблон("Лимит запросов, повтор через %1 с. %2",
            ?(ПустаяСтрока(ПаузаСекунд), "?", ПаузаСекунд), Результат.ТекстОшибки);
    КонецЕсли;
    ЗаписатьЖурналWB(Кабинет, СтрШаблон("%1: HTTP %2. %3", Операция,
        Формат(Ответ.КодСостояния, "ЧГ=0"), Результат.ТекстОшибки));
    Возврат Результат;

КонецФункции

// У сервисов WB разный формат ошибки: title и detail, code и message, errorText,
// а при 409 в остатках — массив объектов, в data которых chrtId или баркод (sku).
Функция ТекстОшибкиWB(Ответ, ТелоОтвета)

    Описания = Новый Массив;
    Если ТипЗнч(Ответ) = Тип("Массив") Тогда
        Описания = Ответ;
    ИначеЕсли ТипЗнч(Ответ) = Тип("Соответствие") Тогда
        Описания.Добавить(Ответ);
    КонецЕсли;

    Части = Новый Массив;
    Для Каждого Описание Из Описания Цикл
        Если ТипЗнч(Описание) <> Тип("Соответствие") Тогда
            Продолжить;
        КонецЕсли;
        Для Каждого Поле Из СтрРазделить("title,detail,code,message,errorText", ",") Цикл
            Значение = Описание.Получить(Поле);
            Если ТипЗнч(Значение) = Тип("Строка") И ЗначениеЗаполнено(Значение) Тогда
                Части.Добавить(Значение);
            КонецЕсли;
        КонецЦикла;
        Позиции = Описание.Получить("data");
        Если ТипЗнч(Позиции) = Тип("Массив") Тогда
            Для Каждого Позиция Из Позиции Цикл
                Если ТипЗнч(Позиция) <> Тип("Соответствие") Тогда
                    Продолжить;
                КонецЕсли;
                Если Позиция.Получить("chrtId") <> Неопределено Тогда
                    Части.Добавить("chrtId " + Формат(Позиция.Получить("chrtId"), "ЧГ=0"));
                ИначеЕсли ЗначениеЗаполнено(Позиция.Получить("sku")) Тогда
                    Части.Добавить("баркод " + Позиция.Получить("sku"));
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
    КонецЦикла;

    Если Части.Количество() = 0 Тогда
        Возврат Лев(ТелоОтвета, 300);
    КонецЕсли;
    Возврат Лев(СтрСоединить(Части, "; "), 1000);

КонецФункции

// Значение заголовка ответа без учета регистра имени.
Функция ЗначениеЗаголовка(Заголовки, Имя)

    Для Каждого Заголовок Из Заголовки Цикл
        Если НРег(Заголовок.Ключ) = НРег(Имя) Тогда
            Возврат Строка(Заголовок.Значение);
        КонецЕсли;
    КонецЦикла;
    Возврат "";

КонецФункции

Функция ЗначениеВJSON(Значение)

    Запись = Новый ЗаписьJSON;
    Запись.УстановитьСтроку();
    ЗаписатьJSON(Запись, Значение);
    Возврат Запись.Закрыть();

КонецФункции

Функция ЗначениеИзJSON(Текст)

    Чтение = Новый ЧтениеJSON;
    Чтение.УстановитьСтроку(Текст);
    Значение = ПрочитатьJSON(Чтение, Истина); // Соответствие: доступ через Получить()
    Чтение.Закрыть();
    Возврат Значение;

КонецФункции

Процедура ЗаписатьЖурналWB(Кабинет, Текст, Уровень = Неопределено) Экспорт

    ЗаписьЖурналаРегистрации("Wildberries.Обмен",
        ?(Уровень = Неопределено, УровеньЖурналаРегистрации.Ошибка, Уровень),
        Метаданные.Справочники.КабинетыWB, Кабинет, Текст);

КонецПроцедуры

Что важно в обёртке:

  • Хост — параметр: у категорий разные домены.
  • Ответ читается в соответствие (почему не в структуру), тело уходит без BOM.
  • При коде 429 WB сообщает в заголовке X-Ratelimit-Retry, через сколько секунд повторять; заголовок ищем без учёта регистра.
  • Формат ошибки у сервисов разный: title и detail, code и message, errorText, а при 409 в остатках — массив с ID размеров. ТекстОшибкиWB собирает всё в одну строку журнала.
  • МожноПродолжить: после 400 и 409 следующую пачку отправлять можно, после остальных ошибок — нет.

/ping показывает, доходит ли запрос, действует ли токен и подходит ли его категория сервису. Лимит — 3 запроса за 30 секунд на хост, автоматические вызовы WB временно блокирует, поэтому проверку вешают на кнопку в форме кабинета.

// Проверка токена кнопкой из формы кабинета (через серверный вызов):
// /ping каждого хоста, с которым работает обмен. Лимит /ping — 3 запроса
// за 30 секунд на хост, поэтому в регламентном обмене его не вызывают.
Функция ПроверитьТокенWB(Кабинет) Экспорт

    Итог = Новый Массив;
    Хосты = СтрРазделить("content-api,discounts-prices-api,marketplace-api", ",");
    Для Каждого Хост Из Хосты Цикл
        Результат = ВызватьWB(Кабинет, Хост + ".wildberries.ru", "GET", "/ping");
        Если Результат.Успех Тогда
            Итог.Добавить(Хост + ": OK");
        Иначе
            Итог.Добавить(СтрШаблон("%1: HTTP %2. %3", Хост,
                Формат(Результат.КодСостояния, "ЧН=0; ЧГ=0"), Результат.ТекстОшибки));
        КонецЕсли;
    КонецЦикла;
    Возврат СтрСоединить(Итог, Символы.ПС);

КонецФункции

Сопоставление по баркодам: ID размера и номенклатура

Карточки отдаются страницами до 100: следующую запрашивают с updatedAt и nmID из курсора ответа, пока total не станет меньше limit. Лимит категории «Контент» — 100 запросов в минуту. Баркоды ищем в регистре ШтрихкодыНоменклатуры (УТ 11.4, КА 2.5: штрихкод → номенклатура, характеристика, упаковка), подробнее — в статье про штрихкоды товаров.

// Читает карточки кабинета и сопоставляет новые размеры (chrtID) с номенклатурой
// по баркодам. Уже сопоставленные размеры, в том числе вручную, не трогает.
Процедура ОбновитьТоварыWB(Кабинет) Экспорт

    РазмерСтраницы = 100; // больше карточек за один запрос метод не отдает
    ТипЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 0));
    ТипАртикул = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(100));
    ТипБаркод = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(200));
    Размеры = Новый ТаблицаЗначений; // колонки с типами: таблица уйдет в запрос
    Размеры.Колонки.Добавить("ИдентификаторРазмера", ТипЧисло);
    Размеры.Колонки.Добавить("АртикулWB", ТипЧисло);
    Размеры.Колонки.Добавить("АртикулПродавца", ТипАртикул);
    Размеры.Колонки.Добавить("Баркод", ТипБаркод);

    Курсор = Новый Структура("limit", РазмерСтраницы);
    Для НомерСтраницы = 1 По 1000 Цикл // предохранитель: не больше 100 000 карточек за запуск
        Настройки = Новый Структура;
        Настройки.Вставить("sort", Новый Структура("ascending", Истина));
        Настройки.Вставить("filter", Новый Структура("withPhoto", -1)); // все карточки
        Настройки.Вставить("cursor", Курсор);
        Результат = ВызватьWB(Кабинет, "content-api.wildberries.ru", "POST",
            "/content/v2/get/cards/list", Новый Структура("settings", Настройки));
        Если Не Результат.Успех Или ТипЗнч(Результат.Ответ) <> Тип("Соответствие") Тогда
            Прервать; // причина в журнале, уже прочитанное сопоставим
        КонецЕсли;

        Карточки = Результат.Ответ.Получить("cards");
        Если ТипЗнч(Карточки) = Тип("Массив") Тогда
            Для Каждого Карточка Из Карточки Цикл
                ДобавитьРазмерыКарточки(Размеры, Карточка);
            КонецЦикла;
        КонецЕсли;

        // Следующая страница — с updatedAt и nmID из курсора ответа, пока total не меньше limit.
        КурсорОтвета = Результат.Ответ.Получить("cursor");
        Всего = ?(ТипЗнч(КурсорОтвета) = Тип("Соответствие"), КурсорОтвета.Получить("total"), 0);
        Если ТипЗнч(Всего) <> Тип("Число") Или Всего < РазмерСтраницы Тогда
            Прервать;
        КонецЕсли;
        Курсор = Новый Структура("limit, updatedAt, nmID", РазмерСтраницы,
            КурсорОтвета.Получить("updatedAt"), КурсорОтвета.Получить("nmID"));
    КонецЦикла;

    СопоставитьРазмерыПоБаркодам(Кабинет, Размеры);

КонецПроцедуры

// По строке на каждый баркод размера. Размер без баркодов — строка с пустым баркодом,
// чтобы он попал в журнал несопоставленных.
Процедура ДобавитьРазмерыКарточки(Размеры, Карточка)

    РазмерыКарточки = Карточка.Получить("sizes");
    Если ТипЗнч(РазмерыКарточки) <> Тип("Массив") Тогда
        Возврат;
    КонецЕсли;
    Для Каждого Размер Из РазмерыКарточки Цикл
        Баркоды = Размер.Получить("skus");
        Если ТипЗнч(Баркоды) <> Тип("Массив") Или Баркоды.Количество() = 0 Тогда
            Баркоды = Новый Массив;
            Баркоды.Добавить("");
        КонецЕсли;
        Для Каждого Баркод Из Баркоды Цикл
            НоваяСтрока = Размеры.Добавить();
            НоваяСтрока.ИдентификаторРазмера = Размер.Получить("chrtID"); // в карточке ID заглавными
            НоваяСтрока.АртикулWB = Карточка.Получить("nmID");
            НоваяСтрока.АртикулПродавца = Карточка.Получить("vendorCode");
            НоваяСтрока.Баркод = Баркод;
        КонецЦикла;
    КонецЦикла;

КонецПроцедуры

// Все штучные штрихкоды размера (без упаковки) должны вести к одной номенклатуре
// и характеристике. Остальное — в журнал для ручного разбора.
Процедура СопоставитьРазмерыПоБаркодам(Кабинет, Размеры)

    Запрос = Новый Запрос(
    "ВЫБРАТЬ
    |    Размеры.ИдентификаторРазмера КАК ИдентификаторРазмера,
    |    Размеры.АртикулWB КАК АртикулWB,
    |    Размеры.АртикулПродавца КАК АртикулПродавца,
    |    Размеры.Баркод КАК Баркод
    |ПОМЕСТИТЬ ВтРазмеры
    |ИЗ
    |    &Размеры КАК Размеры
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ВтРазмеры.ИдентификаторРазмера КАК ИдентификаторРазмера,
    |    МАКСИМУМ(ВтРазмеры.АртикулWB) КАК АртикулWB,
    |    МАКСИМУМ(ВтРазмеры.АртикулПродавца) КАК АртикулПродавца,
    |    ЕСТЬNULL(МАКСИМУМ(Штрихкоды.Штрихкод), МАКСИМУМ(ВтРазмеры.Баркод)) КАК Баркод,
    |    МАКСИМУМ(Штрихкоды.Номенклатура) КАК Номенклатура,
    |    МАКСИМУМ(Штрихкоды.Характеристика) КАК Характеристика,
    |    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Штрихкоды.Номенклатура) КАК ВариантовНоменклатуры,
    |    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Штрихкоды.Характеристика) КАК ВариантовХарактеристик
    |ИЗ
    |    ВтРазмеры КАК ВтРазмеры
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ШтрихкодыНоменклатуры КАК Штрихкоды
    |        ПО ВтРазмеры.Баркод = Штрихкоды.Штрихкод
    |            И (ВтРазмеры.Баркод <> """")
    |            И (Штрихкоды.Упаковка = ЗНАЧЕНИЕ(Справочник.УпаковкиЕдиницыИзмерения.ПустаяСсылка))
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ТоварыWB КАК Сопоставленные
    |        ПО (Сопоставленные.Кабинет = &Кабинет)
    |            И ВтРазмеры.ИдентификаторРазмера = Сопоставленные.ИдентификаторРазмера
    |ГДЕ
    |    Сопоставленные.ИдентификаторРазмера ЕСТЬ NULL
    |
    |СГРУППИРОВАТЬ ПО
    |    ВтРазмеры.ИдентификаторРазмера");
    Запрос.УстановитьПараметр("Размеры", Размеры);
    Запрос.УстановитьПараметр("Кабинет", Кабинет);
    Выборка = Запрос.Выполнить().Выбрать();

    Несопоставленные = Новый Массив;
    Пока Выборка.Следующий() Цикл
        // 0 — штрихкод не найден, больше 1 — баркоды размера ведут к разным товарам.
        Если Выборка.ВариантовНоменклатуры <> 1 Или Выборка.ВариантовХарактеристик <> 1 Тогда
            Несопоставленные.Добавить(СтрШаблон("%1 (%2)", Выборка.АртикулПродавца, Выборка.Баркод));
            Продолжить;
        КонецЕсли;
        Запись = РегистрыСведений.ТоварыWB.СоздатьМенеджерЗаписи();
        ЗаполнитьЗначенияСвойств(Запись, Выборка);
        Запись.Кабинет = Кабинет;
        Запись.Записать();
    КонецЦикла;

    Если Несопоставленные.Количество() > 0 Тогда
        ЗаписатьЖурналWB(Кабинет, "Не сопоставлены по баркодам, артикул продавца (баркод): "
            + СтрСоединить(Несопоставленные, ", "), УровеньЖурналаРегистрации.Предупреждение);
    КонецЕсли;

КонецПроцедуры

Берутся только штучные штрихкоды, без упаковки. Ручное сопоставление код не перезапишет: размеры с записью в ТоварыWB отсекает ЕСТЬ NULL. КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …) ловит неоднозначность: если баркоды размера ведут к разным товарам, размер уходит в журнал. Карточки из корзины WB метод списка не возвращает.

Выгрузка остатков на склады продавца

Склад продавца в WB — не склад 1С: его создают в кабинете или методом POST /api/v3/warehouses и привязывают к складу WB (officeId), куда сдают заказы FBS. GET /api/v3/warehouses отдаёт ID, тип товара cargoType (МГТ, СГТ, КГТ+), тип доставки deliveryType и флаг isProcessing: пока склад обновляется, менять его остатки нельзя.

Остатки уходят парами chrtId и amount, включая нули, — иначе WB продолжит продавать старый остаток. Поэтому запрос строится от регистра сопоставления с левым соединением к виртуальной таблице остатков; отладить его удобно в бесплатной консоли запросов. Лимит — 300 запросов в минуту на аккаунт (интервал 200 мс, всплеск до 20), а любой ответ с кодом 4xx, в том числе 409, считается за 10 запросов.

// Выгружает свободные остатки на склады продавца WB (соответствие складов —
// регистр СкладыWB). Нули тоже: иначе WB продолжит продавать старый остаток.
Процедура ВыгрузитьОстаткиWB(Кабинет) Экспорт

    РазмерПачки = 1000; // не больше 1000 позиций в одном запросе
    Доступные = ДоступныеСкладыWB(Кабинет);
    Если Доступные = Неопределено Тогда
        Возврат; // список складов не получен, причина в журнале
    КонецЕсли;

    // УТ 11.4: в наличии минус резервы — как «Доступно сейчас» в отчете
    // «Остатки и доступность товаров», без резервов по графику.
    Запрос = Новый Запрос(
    "ВЫБРАТЬ
    |    СкладыWB.ИдентификаторСклада КАК ИдентификаторСклада,
    |    Товары.ИдентификаторРазмера КАК ИдентификаторРазмера,
    |    ЕСТЬNULL(Остатки.ВНаличииОстаток, 0)
    |        - ЕСТЬNULL(Остатки.ВРезервеСоСкладаОстаток, 0)
    |        - ЕСТЬNULL(Остатки.ВРезервеПодЗаказОстаток, 0) КАК Остаток
    |ИЗ
    |    РегистрСведений.СкладыWB КАК СкладыWB
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ТоварыWB КАК Товары
    |        ПО СкладыWB.Кабинет = Товары.Кабинет
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СвободныеОстатки.Остатки(
    |                ,
    |                Склад В
    |                    (ВЫБРАТЬ
    |                        Склады.Склад
    |                    ИЗ
    |                        РегистрСведений.СкладыWB КАК Склады
    |                    ГДЕ
    |                        Склады.Кабинет = &Кабинет)) КАК Остатки
    |        ПО СкладыWB.Склад = Остатки.Склад
    |            И Товары.Номенклатура = Остатки.Номенклатура
    |            И Товары.Характеристика = Остатки.Характеристика
    |ГДЕ
    |    СкладыWB.Кабинет = &Кабинет
    |
    |УПОРЯДОЧИТЬ ПО
    |    ИдентификаторСклада
    |ИТОГИ ПО
    |    ИдентификаторСклада");
    Запрос.УстановитьПараметр("Кабинет", Кабинет);
    ПоСкладам = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

    Пока ПоСкладам.Следующий() Цикл
        НомерСклада = Формат(ПоСкладам.ИдентификаторСклада, "ЧГ=0");
        Если Доступные.Найти(ПоСкладам.ИдентификаторСклада) = Неопределено Тогда
            ЗаписатьЖурналWB(Кабинет, "Склад WB " + НомерСклада
                + " не найден, обновляется или удаляется: остатки не отправлены",
                УровеньЖурналаРегистрации.Предупреждение);
            Продолжить;
        КонецЕсли;

        АдресРесурса = "/api/v3/stocks/" + НомерСклада;
        Пачка = Новый Массив;
        Строки = ПоСкладам.Выбрать();
        Пока Строки.Следующий() Цикл
            Пачка.Добавить(Новый Структура("chrtId, amount",
                Строки.ИдентификаторРазмера, Макс(0, Цел(Строки.Остаток))));
            Если Пачка.Количество() = РазмерПачки Тогда
                Если Не ОтправитьОстаткиWB(Кабинет, АдресРесурса, Пачка) Тогда
                    Возврат;
                КонецЕсли;
                Пачка = Новый Массив;
            КонецЕсли;
        КонецЦикла;
        Если Пачка.Количество() > 0 И Не ОтправитьОстаткиWB(Кабинет, АдресРесурса, Пачка) Тогда
            Возврат; // токен, лимит или связь: остальное уйдет при следующем запуске
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

// PUT остатков одной пачки, успех — ответ 204 без тела.
// Ложь — продолжать обмен до следующего запуска бессмысленно.
Функция ОтправитьОстаткиWB(Кабинет, АдресРесурса, Пачка)

    Результат = ВызватьWB(Кабинет, "marketplace-api.wildberries.ru", "PUT", АдресРесурса,
        Новый Структура("stocks", Пачка));
    Возврат Результат.МожноПродолжить;

КонецФункции

// ID складов продавца, по которым сейчас можно менять остатки.
// Пока у склада isProcessing = true, обновление его остатков недоступно.
Функция ДоступныеСкладыWB(Кабинет)

    Результат = ВызватьWB(Кабинет, "marketplace-api.wildberries.ru", "GET", "/api/v3/warehouses");
    Если Не Результат.Успех Или ТипЗнч(Результат.Ответ) <> Тип("Массив") Тогда
        Возврат Неопределено;
    КонецЕсли;

    Доступные = Новый Массив;
    Для Каждого СкладWB Из Результат.Ответ Цикл
        Если ТипЗнч(СкладWB) = Тип("Соответствие")
            И СкладWB.Получить("isProcessing") <> Истина
            И СкладWB.Получить("isDeleting") <> Истина Тогда
            Доступные.Добавить(СкладWB.Получить("id"));
        КонецЕсли;
    КонецЦикла;
    Возврат Доступные;

КонецФункции

Остаток считается как «Доступно сейчас» в отчёте «Остатки и доступность товаров» УТ 11.4, без резервов по графику. В КА 2.5.20 регистра СвободныеОстатки нет — там берут регистр «Запасы и потребности» по своей схеме резервов.

Цены и скидки

Цена задаётся на карточку: nmID, целая цена price и скидка продавца discount в процентах, до 1000 товаров за запрос. Разные цены размеров одной карточки ставит отдельный метод /api/v2/upload/task/size, код такие карточки пропускает. Ответ возвращает ID загрузки, ошибки по товарам смотрят методами состояния и детализации загрузки. Если новая цена со скидкой хотя бы в 3 раза меньше старой, товар попадает в карантин и продаётся по старой цене. Лимит — 10 запросов за 6 секунд, поэтому цены отправляют по изменению, а не каждые пять минут. Реквизиты кабинета читает ЗначенияРеквизитовОбъекта.

// Выгружает цены вида цен кабинета. Цена WB ставится на карточку (nmID), поэтому
// карточки без цены или с разными ценами размеров в 1С пропускаем.
Процедура ВыгрузитьЦеныWB(Кабинет) Экспорт

    РазмерПачки = 1000; // максимум товаров в одном запросе по документации метода
    Реквизиты = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Кабинет, "ВидЦены, СкидкаПроцент");

    // УТ 11.4: РегистрСведений.ЦеныНоменклатуры. Цена за упаковку не подходит —
    // берем только цену за единицу хранения (пустая упаковка).
    Запрос = Новый Запрос(
    "ВЫБРАТЬ
    |    Товары.АртикулWB КАК АртикулWB,
    |    МИНИМУМ(ЕСТЬNULL(Цены.Цена, 0)) КАК ЦенаМин,
    |    МАКСИМУМ(ЕСТЬNULL(Цены.Цена, 0)) КАК ЦенаМакс
    |ИЗ
    |    РегистрСведений.ТоварыWB КАК Товары
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
    |                ,
    |                ВидЦены = &ВидЦены) КАК Цены
    |        ПО Товары.Номенклатура = Цены.Номенклатура
    |            И Товары.Характеристика = Цены.Характеристика
    |            И (Цены.Упаковка = ЗНАЧЕНИЕ(Справочник.УпаковкиЕдиницыИзмерения.ПустаяСсылка))
    |ГДЕ
    |    Товары.Кабинет = &Кабинет
    |    И Товары.АртикулWB > 0
    |
    |СГРУППИРОВАТЬ ПО
    |    Товары.АртикулWB");
    Запрос.УстановитьПараметр("Кабинет", Кабинет);
    Запрос.УстановитьПараметр("ВидЦены", Реквизиты.ВидЦены);
    Выборка = Запрос.Выполнить().Выбрать();

    Пачка = Новый Массив;
    Пропущенные = Новый Массив;
    Продолжать = Истина;
    Пока Продолжать И Выборка.Следующий() Цикл
        Если Выборка.ЦенаМин = 0 Или Выборка.ЦенаМин <> Выборка.ЦенаМакс Тогда
            Пропущенные.Добавить(Формат(Выборка.АртикулWB, "ЧГ=0"));
            Продолжить;
        КонецЕсли;
        // price — целое число, discount — скидка продавца в процентах, тоже целая.
        Пачка.Добавить(Новый Структура("nmID, price, discount",
            Выборка.АртикулWB, Окр(Выборка.ЦенаМакс, 0), Цел(Реквизиты.СкидкаПроцент)));
        Если Пачка.Количество() = РазмерПачки Тогда
            Продолжать = ОтправитьЦеныWB(Кабинет, Пачка);
            Пачка = Новый Массив;
        КонецЕсли;
    КонецЦикла;
    Если Продолжать И Пачка.Количество() > 0 Тогда
        ОтправитьЦеныWB(Кабинет, Пачка);
    КонецЕсли;

    Если Пропущенные.Количество() > 0 Тогда
        ЗаписатьЖурналWB(Кабинет, "Цены не отправлены (нет цены или у размеров разные цены), nmID: "
            + СтрСоединить(Пропущенные, ", "), УровеньЖурналаРегистрации.Предупреждение);
    КонецЕсли;

КонецПроцедуры

// Отправляет пачку цен. Ответ подтверждает прием и дает ID загрузки; ошибки по товарам,
// в том числе карантин цены, смотрят методами состояния и детализации загрузки.
Функция ОтправитьЦеныWB(Кабинет, Пачка)

    Результат = ВызватьWB(Кабинет, "discounts-prices-api.wildberries.ru", "POST",
        "/api/v2/upload/task", Новый Структура("data", Пачка));
    Если Результат.Успех И ТипЗнч(Результат.Ответ) = Тип("Соответствие") Тогда
        ДанныеОтвета = Результат.Ответ.Получить("data");
        Если ТипЗнч(ДанныеОтвета) = Тип("Соответствие") Тогда
            ЗаписатьЖурналWB(Кабинет, СтрШаблон("Цены: загрузка %1, товаров %2",
                Формат(ДанныеОтвета.Получить("id"), "ЧГ=0"), Пачка.Количество()),
                УровеньЖурналаРегистрации.Информация);
        КонецЕсли;
    КонецЕсли;
    Возврат Результат.МожноПродолжить;

КонецФункции

В КА 2.5.20 рядом с ЦеныНоменклатуры есть ЦеныНоменклатуры25 с измерением ХарактеристикаЦО — проверьте, какой регистр использует ваша база. Условие на пустую упаковку не даёт отправить цену коробки вместо цены штуки.

Новые сборочные задания FBS

Заказ FBS приходит как сборочное задание на одну единицу товара: один chrtId, поля количества нет. Три одинаковых товара у покупателя — три задания с разными id. GET /api/v3/orders/new отдаёт задания, пока они новые, поэтому принятые отсекаются по регистру СборочныеЗаданияWB.

// Новые сборочные задания FBS, которых еще нет в регистре СборочныеЗаданияWB.
// Одно задание — одна единица товара. Задание с несопоставленным размером
// не загружается и придет снова при следующем запуске.
Функция НовыеЗаданияWB(Кабинет) Экспорт

    ТипЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 0));
    Список = Новый ТаблицаЗначений;
    Список.Колонки.Добавить("ИдентификаторЗадания", ТипЧисло);
    Список.Колонки.Добавить("ИдентификаторСклада", ТипЧисло);
    Список.Колонки.Добавить("ИдентификаторРазмера", ТипЧисло);
    Список.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(100)));

    Результат = ВызватьWB(Кабинет, "marketplace-api.wildberries.ru", "GET", "/api/v3/orders/new");
    ЗаданияWB = ?(Результат.Успех И ТипЗнч(Результат.Ответ) = Тип("Соответствие"),
        Результат.Ответ.Получить("orders"), Неопределено);
    Если ТипЗнч(ЗаданияWB) = Тип("Массив") Тогда
        Для Каждого ЗаданиеWB Из ЗаданияWB Цикл
            НоваяСтрока = Список.Добавить();
            НоваяСтрока.ИдентификаторЗадания = ЗаданиеWB.Получить("id");
            НоваяСтрока.ИдентификаторСклада = ЗаданиеWB.Получить("warehouseId");
            НоваяСтрока.ИдентификаторРазмера = ЗаданиеWB.Получить("chrtId"); // здесь Id строчными
            НоваяСтрока.Артикул = ЗаданиеWB.Получить("article");
        КонецЦикла;
    КонецЕсли;

    // Отсекаем принятые раньше задания и подставляем номенклатуру одним запросом.
    Запрос = Новый Запрос(
    "ВЫБРАТЬ
    |    Список.ИдентификаторЗадания КАК ИдентификаторЗадания,
    |    Список.ИдентификаторСклада КАК ИдентификаторСклада,
    |    Список.ИдентификаторРазмера КАК ИдентификаторРазмера,
    |    Список.Артикул КАК Артикул
    |ПОМЕСТИТЬ ВтЗадания
    |ИЗ
    |    &Список КАК Список
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ВтЗадания.ИдентификаторЗадания КАК ИдентификаторЗадания,
    |    ВтЗадания.ИдентификаторСклада КАК ИдентификаторСклада,
    |    ВтЗадания.Артикул КАК Артикул,
    |    ЕСТЬNULL(Товары.Номенклатура,
    |        ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)) КАК Номенклатура,
    |    ЕСТЬNULL(Товары.Характеристика,
    |        ЗНАЧЕНИЕ(Справочник.ХарактеристикиНоменклатуры.ПустаяСсылка)) КАК Характеристика
    |ИЗ
    |    ВтЗадания КАК ВтЗадания
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ТоварыWB КАК Товары
    |        ПО (Товары.Кабинет = &Кабинет)
    |            И ВтЗадания.ИдентификаторРазмера = Товары.ИдентификаторРазмера
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СборочныеЗаданияWB КАК Принятые
    |        ПО (Принятые.Кабинет = &Кабинет)
    |            И ВтЗадания.ИдентификаторЗадания = Принятые.ИдентификаторЗадания
    |ГДЕ
    |    Принятые.ИдентификаторЗадания ЕСТЬ NULL");
    Запрос.УстановитьПараметр("Список", Список);
    Запрос.УстановитьПараметр("Кабинет", Кабинет);
    Задания = Запрос.Выполнить().Выгрузить();

    Несопоставленные = Новый Массив;
    Отбор = Новый Структура("Номенклатура", Справочники.Номенклатура.ПустаяСсылка());
    Для Каждого СтрокаЗадания Из Задания.НайтиСтроки(Отбор) Цикл
        Несопоставленные.Добавить(СтрШаблон("%1 (%2)",
            Формат(СтрокаЗадания.ИдентификаторЗадания, "ЧГ=0"), СтрокаЗадания.Артикул));
        Задания.Удалить(СтрокаЗадания);
    КонецЦикла;
    Если Несопоставленные.Количество() > 0 Тогда
        ЗаписатьЖурналWB(Кабинет, "Не загружены задания с несопоставленным размером, "
            + "задание (артикул): " + СтрСоединить(Несопоставленные, ", "),
            УровеньЖурналаРегистрации.Предупреждение);
    КонецЕсли;

    Возврат Задания;

КонецФункции

// Строки заказа клиента по одному складу WB: сколько заданий, столько единиц товара.
Функция СтрокиЗаказаWB(Задания, ИдентификаторСклада) Экспорт

    Отбор = Новый Структура("ИдентификаторСклада", ИдентификаторСклада);
    ТипКоличество = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3));
    СтрокиЗаказа = Задания.Скопировать(Отбор, "Номенклатура, Характеристика");
    СтрокиЗаказа.Колонки.Добавить("Количество", ТипКоличество);
    СтрокиЗаказа.ЗаполнитьЗначения(1, "Количество");
    СтрокиЗаказа.Свернуть("Номенклатура, Характеристика", "Количество");
    Возврат СтрокиЗаказа;

КонецФункции

// Запоминает задания склада, вошедшие в заказ клиента, чтобы не загрузить их повторно.
// Вызывайте в той же транзакции, в которой записывается заказ.
Процедура ЗарегистрироватьЗаданияWB(Кабинет, Задания, ИдентификаторСклада, ЗаказКлиента) Экспорт

    Отбор = Новый Структура("ИдентификаторСклада", ИдентификаторСклада);
    Для Каждого СтрокаЗадания Из Задания.НайтиСтроки(Отбор) Цикл
        Запись = РегистрыСведений.СборочныеЗаданияWB.СоздатьМенеджерЗаписи();
        Запись.Кабинет = Кабинет;
        Запись.ИдентификаторЗадания = СтрокаЗадания.ИдентификаторЗадания;
        Запись.ЗаказКлиента = ЗаказКлиента;
        Запись.Записать();
    КонецЦикла;

КонецПроцедуры

Для каждого склада WB: СтрокиЗаказаWB даёт строки заказа клиента, заказ создаётся, как в статье про Яндекс Маркет, а ЗарегистрироватьЗаданияWB вызывается в той же транзакции. Если загрузку можно запустить вручную параллельно с заданием, добавьте управляемую блокировку по кабинету. Цену в заказ берите из вида цен 1С, фактическую цену продажи — из отчётов WB.

Затем задания собирают в поставку: POST /api/v3/supplies возвращает ID вида WB-GI-…. В поставку попадают задания одного габаритного типа (cargoType первого задания), добавленное задание переходит из новых в подтверждённые. Если у задания заполнен requiredMeta (например, uin или sgtin), эти данные передают до отгрузки.

Типичные ошибки: баркоды, склады, лимиты

  • Остатки отправлены по баркоду. Старые примеры шлют в остатках sku. Текущий метод ждёт chrtId и имена полей не проверяет: ответ 204, а остатки на WB прежние.
  • Баркод WB не найден в 1С. Его сгенерировал WB или ввели с опечаткой, а в 1С заведён штрихкод производителя. Добавьте баркод WB в штрихкоды номенклатуры или сопоставьте размер вручную.
  • Набор вместо штуки. На WB продаётся упаковка из нескольких штук, а 1С отдаёт остаток в единицах хранения — делите на коэффициент упаковки.
  • Нет характеристик в 1С. Все размеры карточки сопоставляются с одной номенклатурой и получают полный остаток.
  • Один склад 1С на несколько складов WB. Каждый получает полный остаток, последнюю единицу продают дважды.
  • 409 «Ошибка обновления остатков». В ответе — массив с ID размеров; такой ответ считается за 10 запросов и быстро съедает лимит.
  • 429 Too Many Requests. Коннектор, своя доработка и сервис аналитики тратят один лимит аккаунта. Повторяйте не раньше, чем через X-Ratelimit-Retry секунд.
  • Одни хосты отвечают, другие нет. У токена нет нужной категории; если отказывают все — проверьте срок действия: токен действует 180 дней с момента создания.
  • Копия базы шлёт остатки в рабочий кабинет. Безопасное хранилище копируется вместе с базой. В тестовой копии сразу выключайте обмен у кабинетов.

Частые вопросы

Есть ли в 1С типовая интеграция с Wildberries?

В выгрузках УТ 11.4.14 и КА 2.5.20 её нет: типовой механизм КА 2.5.20 работает с Ozon и Яндекс Маркетом. Для новых релизов и других конфигураций смотрите описание релиза на its.1c.ru.

Какие категории нужны токену?

Для кода из статьи — «Контент», «Цены и скидки» и «Маркетплейс». Для загрузки продаж и финансовых отчётов добавьте «Статистику» и «Финансы». Лишние категории не включайте: меньше прав — меньше вреда от утечки.

Где проверить обмен без риска для кабинета?

В песочнице — хосты с суффиксом -sandbox: content-api-sandbox, discounts-prices-api-sandbox и marketplace-api-sandbox в домене wildberries.ru. Для неё нужен тестовый токен, а лимит там — один запрос в секунду на все методы сервиса, поэтому пачки отправляйте с паузой.

Почему цена на WB не изменилась после выгрузки?

Проверьте состояние загрузки по ID из журнала: товар мог уйти в карантин из-за резкого снижения цены. Если nmID есть в журнале пропущенных, у размеров карточки в 1С разные цены или нет цены за штуку.

Когда проще заказать

Если нужно подключить несколько кабинетов и складов, учесть свою схему резервов или доработать коннектор, закажите интеграцию 1С с маркетплейсами — Wildberries, Ozon или Яндекс Маркет. Укажите конфигурацию, релиз и модели работы с WB и разместите задание: оценка бесплатная, ответ — в течение рабочего дня.

По теме

Не хотите разбираться сами?

Адаптируем это решение под вашу базу. Оценка бесплатно.

Оставить задачу

Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.


1С, 1С:Предприятие — товарные знаки ООО «1С». Сайт не является официальным представительством фирмы «1С» и не аффилирован с ней.