Коротко. Автоматизация товаров на Wildberries — это pipeline, а не одиночный запрос. Сначала получаем справочники и характеристики, валидируем карточку, создаём её через Content API, проверяем появление nmID, разбираем ошибки через /content/v2/cards/error/list, отдельно грузим медиа, цены и остатки, а затем используем поисковую аналитику WB и рекламные кластеры для SEO-обновлений. Самые опасные места: асинхронное создание карточек, полная перезапись при /content/v2/cards/update, карантин цен, 429 по лимитам и отсутствие проверки остатков после 204.
Проблема ручного ведения карточек
Когда товаров 10–20, карточки можно вести руками. Когда товаров 300, 1000 или 10 000, ручное ведение ломается по предсказуемым причинам:
- характеристики по предметам меняются;
- карточка может не создаться, хотя API вернул
200; - обновление карточки может затереть поля, которые вы не собирались менять;
- фото загружаются отдельным этапом — после появления
nmID; - цены и остатки нельзя ставить сразу после создания, пока карточка синхронизируется;
- поисковая семантика живёт отдельно от карточки, рекламы и аналитики;
- лимиты API превращают массовую загрузку в хаос без очереди запросов.
Решение: рабочий pipeline
Базовый production-процесс идёт строго по порядку: сначала справочники и валидация, потом создание карточки, потом проверка, и только затем медиа, цены, остатки и SEO-итерации.
- Источник товаров — ERP / 1С / Google Sheets / PIM.
- Нормализация данных под формат WB.
- WB-справочники: предметы, характеристики, цвета, НДС, ТН ВЭД.
- Валидация карточки до отправки.
- Создание карточки —
POST /content/v2/cards/upload. - Ожидание синхронизации (до 30 минут).
- Проверка карточки —
POST /content/v2/get/cards/list. - Если nmID найден → загрузка медиа. Если нет → разбор
/content/v2/cards/error/list. - Цены и скидки — Prices and Discounts API.
- Остатки FBS / склад продавца.
- Аналитика поиска и продаж.
- SEO-рекомендации и рекламные кластеры → безопасное обновление карточки → возврат к проверке.
Что реально автоматизируется через WB API
Автоматизация строится вокруг пяти API-категорий: Content, Prices and Discounts, Marketplace, Analytics, Promotion. Ниже — карта задач и подводных камней.
| Задача | Категория | Метод / раздел | Что учитывать |
|---|---|---|---|
| Получить категории | Content | GET /content/v2/object/parent/all | Нужен токен категории Content. |
| Получить предметы | Content | GET /content/v2/object/all | Предмет определяет набор характеристик. |
| Характеристики предмета | Content | GET /content/v2/object/charcs/{subjectId} | Важны required, hasFilter, тип значения и справочники. |
| Сгенерировать баркоды | Content | POST /content/v2/barcodes | Используются для размеров карточки. |
| Создать карточки | Content | POST /content/v2/cards/upload | До 100 карточек или 100 групп; до 30 nmID в группе; до 10 МБ; создание асинхронное. |
| Список карточек | Content | POST /content/v2/get/cards/list | Проверка nmID, imtID, chrtID, фото, размеров. |
| Ошибки карточек | Content | POST /content/v2/cards/error/list | Обязательный шаг, если карточка не появилась после 200. |
| Обновить карточки | Content | POST /content/v2/cards/update | Перезаписывает карточку целиком. Нужен read-merge-update. |
| Медиа ссылками | Content | POST /content/v3/media/save | Нужен nmID; медиа не смешивать с созданием карточки. |
| Медиа файлом | Content | POST /content/v3/media/file | Для прямой загрузки файла. |
| Привязать теги | Content | POST /content/v2/tag/nomenclature/link | До 15 тегов на карточку. |
| Цены и скидки | Prices and Discounts | POST /api/v2/upload/task | До 1000 товаров; цена может уйти в карантин. |
| Цены по размерам | Prices and Discounts | POST /api/v2/upload/task/size | Для товаров с редактируемой ценой размера. |
| Обновить остатки | Marketplace | PUT /api/v3/stocks/{warehouseId} | До 1000 chrtId; после 204 желательно проверять остатки. |
| Поисковые запросы по товарам | Analytics | /api/v2/search-report/... | Нужна подписка «Джем». |
| Поисковые кластеры рекламы | Promotion | /adv/v0/normquery/... | Ставки, минус-фразы, кластеры. |
Контроль поставок и остатков (FBS/склад продавца) держится на категории Marketplace, а связка SEO+реклама — на Analytics и Promotion.
Архитектура автоматизации
Минимальная схема для MVP
Подходит для каталога до нескольких сотен товаров.
Схема для production
Главное отличие: у каждого товара есть статус, а у каждого API-запроса — журнал выполнения.
Минимальная база данных для интеграции
Не начинайте с HTTP-запросов. Начните с таблиц — они дают статусы, защиту от повторов и журнал ошибок.
products_source
| Поле | Пример | Зачем |
|---|---|---|
vendor_code | TSHIRT-BLACK-001 | Главный ключ на стороне продавца. |
subject_id | 192 | Предмет WB. |
brand | BrandName | Бренд. |
title | Футболка мужская хлопковая | Наименование. |
description | … | Описание. |
dimensions_json | {…} | Габариты с упаковкой. |
characteristics_json | […] | Характеристики по WB id. |
sizes_json | […] | Размеры, баркоды. |
media_json | […] | Ссылки на фото/видео. |
price | 1990 | Базовая цена. |
discount | 35 | Скидка. |
stock | 42 | Остаток. |
wb_cards
wb_jobs
Создание карточки через API
Последовательность перед созданием
- Получить родительские категории:
GET /content/v2/object/parent/all. - Получить предметы:
GET /content/v2/object/all. - Выбрать
subjectID. - Получить характеристики предмета:
GET /content/v2/object/charcs/{subjectId}. - Для характеристик из справочников использовать значения WB: цвета, пол, страна, сезон, НДС, ТН ВЭД.
- Сгенерировать или подготовить баркоды.
- Собрать payload.
- Отправить
POST /content/v2/cards/upload. - Подождать и проверить карточку через список карточек.
- Если карточки нет — проверить
/content/v2/cards/error/list.
Важные ограничения
- В одном запросе — максимум 100 индивидуальных карточек или 100 групп объединённых карточек.
- В одной объединённой группе — до 30 карточек.
- Максимальный размер запроса — 10 МБ.
- Габариты — в сантиметрах, вес с упаковкой — в килограммах.
- Создание асинхронное; синхронизация может занимать до 30 минут.
- Во время синхронизации нельзя добавлять остатки и задавать цены.
Пример payload для создания карточки
Это шаблон. subjectID, characteristics.id и допустимые значения нужно получать из WB-справочников, а не копировать из примера.
[
{
"subjectID": 192,
"variants": [
{
"vendorCode": "TSHIRT-BLACK-001",
"brand": "BrandName",
"title": "Футболка мужская хлопковая",
"description": "Базовая мужская футболка из хлопка. Подходит для повседневной носки, спорта и дома.",
"dimensions": {
"length": 30,
"width": 25,
"height": 3,
"weightBrutto": 0.25
},
"characteristics": [
{
"id": 14177449,
"value": ["черный"]
},
{
"id": 88952,
"value": ["хлопок"]
},
{
"id": 90630,
"value": ["мужской"]
}
],
"sizes": [
{
"techSize": "M",
"wbSize": "48",
"price": 1990,
"skus": ["4600000000001"]
},
{
"techSize": "L",
"wbSize": "50",
"price": 1990,
"skus": ["4600000000002"]
}
]
}
]
}
]
Что валидировать до отправки
| Проверка | Почему важно |
|---|---|
subjectID найден в WB | Нельзя создавать карточку в несуществующем или неверном предмете. |
Все required характеристики заполнены | Иначе карточка уйдёт в ошибки. |
hasFilter:true заполнены, если критичны для выдачи | Фильтруемые характеристики влияют на поиск и подбор товара. |
| Значения справочников взяты из WB | Особенно цвет, пол, страна, сезон, НДС, ТН ВЭД. |
| Название без запрещённых символов, email, телефона, ссылок | Иначе карточка попадёт в ошибки. |
| Габариты в см, вес в кг | Неверные единицы ломают логистику и стоимость. |
| Цена не ноль | При новых размерах без цены цена может стать 0. |
| Баркоды уникальны | Дубли создают конфликты. |
| Размер запроса меньше 10 МБ | Иначе 413. |
| Партия не больше лимита метода | Иначе часть процесса придётся повторять. |
Code node для n8n: валидация и сбор payload
const row = $input.first().json;
const required = [
'vendorCode',
'subjectID',
'brand',
'title',
'description',
'length',
'width',
'height',
'weightBrutto',
'barcode'
];
const missing = required.filter((key) => row[key] === undefined || row[key] === null || row[key] === '');
if (missing.length) {
throw new Error(`Не заполнены поля: ${missing.join(', ')}`);
}
if (String(row.title).length > 60) {
throw new Error('Название больше 60 символов. Сократите title до отправки в WB.');
}
const dimensions = {
length: Number(row.length),
width: Number(row.width),
height: Number(row.height),
weightBrutto: Number(row.weightBrutto)
};
for (const [key, value] of Object.entries(dimensions)) {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`Некорректное значение dimensions.${key}: ${value}`);
}
}
const characteristics = JSON.parse(row.characteristicsJson || '[]');
const sizes = JSON.parse(row.sizesJson || '[]');
if (!characteristics.length) {
throw new Error('characteristicsJson пустой. Сначала получите характеристики subjectID.');
}
if (!sizes.length) {
sizes.push({
techSize: String(row.techSize || '0'),
wbSize: String(row.wbSize || ''),
price: Number(row.price),
skus: [String(row.barcode)]
});
}
return [
{
json: {
vendorCode: row.vendorCode,
createPayload: [
{
subjectID: Number(row.subjectID),
variants: [
{
vendorCode: String(row.vendorCode),
brand: String(row.brand),
title: String(row.title),
description: String(row.description),
dimensions,
characteristics,
sizes
}
]
}
]
}
}
];
Проверка ошибок после создания
Ошибка новичка: отправить /content/v2/cards/upload, получить 200 и считать, что карточка создана. Правильный порядок:
- Сохраняем
vendorCodeи payload вwb_jobs. - Ждём паузу. Для MVP — 2–5 минут, для production — отдельная задача
check_cardс повтором. - Запрашиваем
/content/v2/get/cards/listсtextSearchпоvendorCodeили barcode. - Нашли карточку — сохраняем
nmID,imtID,chrtID. - Не нашли — запрашиваем
/content/v2/cards/error/list. - Ошибки пишем в БД и не двигаем товар дальше в медиа / цены / остатки.
Запрос списка карточек
{
"settings": {
"sort": {
"ascending": false
},
"filter": {
"textSearch": "TSHIRT-BLACK-001",
"allowedCategoriesOnly": true,
"tagIDs": [],
"objectIDs": [],
"brands": [],
"imtID": 0,
"withPhoto": -1
},
"cursor": {
"limit": 100
}
}
}
Запрос ошибок карточек
{
"cursor": {
"limit": 100
},
"order": {
"ascending": false
}
}
Как маппить ошибки на товар
В ответе /content/v2/cards/error/list есть vendorCodes и объект errors, где ключом может быть артикул продавца. Поэтому в БД нужен vendor_code, а не только nmID: для неуспешно созданной карточки nmID может ещё не существовать.
const response = $input.first().json;
const items = response.data?.items || [];
const result = [];
for (const batch of items) {
const errors = batch.errors || {};
for (const [vendorCode, messages] of Object.entries(errors)) {
result.push({
json: {
vendorCode,
batchUUID: batch.batchUUID,
updatedAt: batch.updatedAt,
errors: messages
}
});
}
}
return result;
Безопасное обновление карточек
POST /content/v2/cards/update — опасный метод. Он не обновляет одно поле как PATCH: карточка перезаписывается целиком. Поэтому нельзя отправлять только новое описание или новое название.
Правильный паттерн — read-merge-update:
- Получить текущую карточку из списка карточек.
- Собрать изменения (только то, что меняем).
- Слить current + changes.
- Проверить обязательные поля.
- Проверить, что не потеряли размеры и характеристики.
- Отправить
/content/v2/cards/update. - Проверить
/cards/error/list. - Сравнить итоговую карточку с ожидаемой.
Что нельзя обновлять через /content/v2/cards/update
- нельзя обновлять или удалять баркоды размеров;
- нельзя обновлять
photos,video,tags; - нельзя менять цены существующих товаров этим методом;
- цену можно указать только при добавлении нового размера; существующие цены обновляются через Prices and Discounts API.
Пример payload для обновления
[
{
"nmID": 123456789,
"vendorCode": "TSHIRT-BLACK-001",
"brand": "BrandName",
"title": "Футболка мужская хлопок",
"description": "Обновлённое описание без потери характеристик и размеров.",
"dimensions": {
"length": 30,
"width": 25,
"height": 3,
"weightBrutto": 0.25
},
"characteristics": [
{
"id": 14177449,
"value": ["черный"]
},
{
"id": 88952,
"value": ["хлопок"]
},
{
"id": 90630,
"value": ["мужской"]
}
],
"sizes": [
{
"chrtID": 111111111,
"techSize": "M",
"wbSize": "48",
"skus": ["4600000000001"]
},
{
"chrtID": 222222222,
"techSize": "L",
"wbSize": "50",
"skus": ["4600000000002"]
}
]
}
]
Read-merge-update в Code node
const currentCard = $('Get WB Card').first().json.cards[0];
const changes = $('Prepare Changes').first().json;
if (!currentCard) {
throw new Error('Карточка не найдена. Нельзя обновлять без текущего состояния.');
}
const updated = {
nmID: currentCard.nmID,
vendorCode: currentCard.vendorCode,
brand: changes.brand ?? currentCard.brand,
title: changes.title ?? currentCard.title,
description: changes.description ?? currentCard.description,
dimensions: changes.dimensions ?? currentCard.dimensions,
characteristics: changes.characteristics ?? currentCard.characteristics,
sizes: currentCard.sizes
};
if (!updated.sizes?.length) {
throw new Error('Защита от потери sizes: в payload нет размеров.');
}
if (!updated.characteristics?.length) {
throw new Error('Защита от потери characteristics: в payload нет характеристик.');
}
return [{ json: { updatePayload: [updated] } }];
Медиа, цены, скидки и остатки
Медиа
После создания карточки получите nmID, затем загрузите фото ссылками:
{
"nmId": 123456789,
"data": [
"https://example.com/images/tshirt-black-1.jpg",
"https://example.com/images/tshirt-black-2.jpg",
"https://example.com/images/tshirt-black-3.jpg"
]
}
Практический контроль:
- проверяйте, что ссылки открываются без авторизации;
- используйте стабильные URL, а не временные ссылки CDN;
- храните порядок изображений;
- после загрузки снова получите карточку и проверьте
photos.
Цены и скидки
Пример для POST /api/v2/upload/task:
{
"data": [
{
"nmID": 123456789,
"price": 1990,
"discount": 35
}
]
}
Перед отправкой цен нужен защитный валидатор — он спасает от карантина и продаж в минус:
const item = $input.first().json;
const oldFinalPrice = Number(item.oldPrice) * (1 - Number(item.oldDiscount || 0) / 100);
const newFinalPrice = Number(item.price) * (1 - Number(item.discount || 0) / 100);
if (!Number.isFinite(newFinalPrice) || newFinalPrice <= 0) {
throw new Error('Итоговая цена некорректна.');
}
if (oldFinalPrice > 0 && newFinalPrice <= oldFinalPrice / 3) {
throw new Error('Новая цена со скидкой в 3+ раза ниже прежней. Риск карантина цены.');
}
if (newFinalPrice < Number(item.minMarginPrice)) {
throw new Error(`Цена ниже минимальной маржинальной цены: ${newFinalPrice}`);
}
return [{ json: item }];
Остатки и контроль поставок
Для остатков FBS / склада продавца используется chrtId, а не nmID. Пример для PUT /api/v3/stocks/{warehouseId}:
{
"stocks": [
{
"chrtId": 111111111,
"amount": 42
},
{
"chrtId": 222222222,
"amount": 18
}
]
}
После 204 не считайте задачу завершённой. Сразу или через короткую паузу проверьте остатки:
{
"chrtIds": [111111111, 222222222]
}
204, но остатки не обновятся. Поэтому в production обязателен verify-step — иначе контроль поставок «врёт».Лимиты API и retry
WB API использует token bucket. В интеграции нельзя отправлять тысячи запросов простым циклом — нужна очередь.
Минимальная логика retry
function getRetryDelaySeconds(responseHeaders, attempt) {
const retryHeader = responseHeaders['x-ratelimit-retry'] || responseHeaders['X-Ratelimit-Retry'];
const retryAfter = Number(retryHeader);
if (Number.isFinite(retryAfter) && retryAfter > 0) {
return retryAfter;
}
return Math.min(60, 2 ** attempt);
}
function shouldRetry(status) {
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
}
Правила очереди
| Ситуация | Что делать |
|---|---|
429 | Читать X-Ratelimit-Retry, ждать, повторять. |
409 | Не спамить повтором: один такой запрос может стоить больше лимита. Проверить payload. |
400 | Не retry. Ошибка запроса — нужно исправить данные. |
401 | Проверить токен и категорию токена. |
403 | Проверить доступ, подписку, роль, категорию токена. |
413 | Уменьшить размер партии. |
422 | Проверить противоречивые параметры. |
5xx | Retry с backoff, но не бесконечно. |
Семантическое ядро для Wildberries
Семантика WB должна собираться не из «ключевых слов вообще», а из данных, которые влияют на карточку и продажи. Принцип тот же, что в SEO карточек товаров и в SEO-текстах.
Источники семантики
| Источник | Что брать | Как использовать |
|---|---|---|
| Отчёт «Поисковые запросы на WB» | запрос, количество, динамика, среднее в день, предмет с заказами | Сбор спроса по нише и первичная карта запросов. |
| Отчёт «Поисковые запросы: ваши товары» | позиция, видимость, переходы, корзина, заказы, конверсии | Оптимизация конкретных карточек. |
Analytics API /search-report/product/search-texts | топ поисковых текстов по товару | Автообновление ядра по nmID. |
Analytics API /search-report/product/orders | заказы и позиции по поисковым текстам | Понимание, какие запросы реально продают. |
Promotion API /adv/v0/normquery/list | активные и неактивные кластеры | Связка SEO и рекламы. |
Promotion API /normquery/get-minus, /set-minus | минус-фразы | Исключение нерелевантного спроса в рекламе. |
Классификация запросов
| Класс | Пример | Куда использовать |
|---|---|---|
| Основной товарный | футболка мужская | Название, предмет, реклама. |
| Атрибутный | футболка хлопковая, футболка черная | Характеристики, описание, фото. |
| Сезонный | футболка летняя | Название, описание, кампании по сезону. |
| Сценарный | футболка для спорта | Описание, инфографика, rich-контент. |
| Брендовый | brandname футболка | Название, реклама, защита бренда. |
| Мусорный | выкройка футболки, бесплатно | Минус-фразы. |
| Сомнительный | много показов, нет корзины/заказов | Тестировать отдельно, не пихать в title. |
Как не испортить карточку ключами
Плохой title — простыня из всех ключей:
Футболка мужская хлопковая черная летняя спортивная повседневная базовая одежда
Лучше — короткий и читаемый:
Футболка мужская хлопковая черная
Остальное уходит в описание, характеристики и изображения.
Практическая формула title
[предмет] + [пол/назначение] + [материал/ключевой атрибут] + [цвет/сезон]
| Ниша | Title |
|---|---|
| Одежда | Футболка мужская хлопковая черная |
| Дом | Органайзер для белья в шкаф |
| Детские товары | Пижама детская хлопковая |
| Косметика | Крем для рук увлажняющий |
title ограничено 60 символами.Алгоритм семантического обновления карточки
- Получить поисковые запросы по
nmID. - Посчитать метрики (позиция, переходы, корзина, заказы, конверсии).
- Есть заказы? → кандидаты в title / характеристики / описание.
- Нет заказов, но есть корзины? → кандидаты в описание и фото.
- Ни заказов, ни корзин? → минус-фразы или тест рекламы.
- Проверить релевантность предмету.
- Сформировать изменения и применить read-merge-update карточки.
Структура семантической таблицы
Code node: классификация запросов
const rows = $input.all().map(i => i.json);
function classify(q) {
const query = String(q.query || q.searchText || '').toLowerCase().trim();
const orders = Number(q.orders || q.orderCount || 0);
const carts = Number(q.carts || q.cartCount || 0);
const clicks = Number(q.clicks || q.openCardCount || 0);
const position = Number(q.avgPosition || q.position || 999);
const trashPatterns = ['бесплатно', 'скачать', 'выкройка', 'ремонт', 'бу'];
if (trashPatterns.some(p => query.includes(p))) {
return 'minus';
}
if (orders > 0 && position <= 50) {
return 'title_or_characteristic';
}
if (orders > 0) {
return 'ads_or_description';
}
if (carts > 0 || clicks > 20) {
return 'description_or_media';
}
return 'ignore';
}
return rows.map(row => ({
json: {
...row,
decision: classify(row)
}
}));
n8n workflow
Ниже skeleton workflow для импорта в n8n. Он показывает рабочую последовательность: взять товар → собрать payload → создать карточку → подождать → проверить карточку → если nmID найден, загрузить медиа; если нет — получить ошибки.
Перед импортом замените: WB_API_TOKEN на credentials или env-переменную; тестовые значения товара; subjectID, characteristics, warehouseId, URL фото; endpoints — при изменении документации WB.
{
"name": "WB Product Create Pipeline - Content Media Errors",
"nodes": [
{
"parameters": {},
"id": "manual-trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-900,
0
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "vendorCode",
"name": "vendorCode",
"value": "TSHIRT-BLACK-001",
"type": "string"
},
{
"id": "subjectID",
"name": "subjectID",
"value": 192,
"type": "number"
},
{
"id": "brand",
"name": "brand",
"value": "BrandName",
"type": "string"
},
{
"id": "title",
"name": "title",
"value": "Футболка мужская хлопковая",
"type": "string"
},
{
"id": "description",
"name": "description",
"value": "Базовая мужская футболка из хлопка для повседневной носки.",
"type": "string"
},
{
"id": "length",
"name": "length",
"value": 30,
"type": "number"
},
{
"id": "width",
"name": "width",
"value": 25,
"type": "number"
},
{
"id": "height",
"name": "height",
"value": 3,
"type": "number"
},
{
"id": "weightBrutto",
"name": "weightBrutto",
"value": 0.25,
"type": "number"
},
{
"id": "barcode",
"name": "barcode",
"value": "4600000000001",
"type": "string"
},
{
"id": "price",
"name": "price",
"value": 1990,
"type": "number"
},
{
"id": "characteristicsJson",
"name": "characteristicsJson",
"value": "[{\"id\":14177449,\"value\":[\"черный\"]},{\"id\":88952,\"value\":[\"хлопок\"]}]",
"type": "string"
},
{
"id": "sizesJson",
"name": "sizesJson",
"value": "[]",
"type": "string"
},
{
"id": "mediaJson",
"name": "mediaJson",
"value": "[\"https://example.com/tshirt-1.jpg\",\"https://example.com/tshirt-2.jpg\"]",
"type": "string"
}
]
},
"options": {}
},
"id": "set-sample-product",
"name": "Set Sample Product",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-680,
0
]
},
{
"parameters": {
"jsCode": "const row = $input.first().json;\nconst required = ['vendorCode','subjectID','brand','title','description','length','width','height','weightBrutto','barcode'];\nconst missing = required.filter((key) => row[key] === undefined || row[key] === null || row[key] === '');\nif (missing.length) throw new Error(`Не заполнены поля: ${missing.join(', ')}`);\nif (String(row.title).length > 60) throw new Error('Название больше 60 символов');\nconst dimensions = { length: Number(row.length), width: Number(row.width), height: Number(row.height), weightBrutto: Number(row.weightBrutto) };\nfor (const [key, value] of Object.entries(dimensions)) { if (!Number.isFinite(value) || value <= 0) throw new Error(`Некорректное dimensions.${key}`); }\nconst characteristics = JSON.parse(row.characteristicsJson || '[]');\nconst sizes = JSON.parse(row.sizesJson || '[]');\nif (!characteristics.length) throw new Error('Нет characteristics');\nif (!sizes.length) { sizes.push({ techSize: String(row.techSize || '0'), wbSize: String(row.wbSize || ''), price: Number(row.price), skus: [String(row.barcode)] }); }\nreturn [{ json: { vendorCode: row.vendorCode, media: JSON.parse(row.mediaJson || '[]'), createPayload: [{ subjectID: Number(row.subjectID), variants: [{ vendorCode: String(row.vendorCode), brand: String(row.brand), title: String(row.title), description: String(row.description), dimensions, characteristics, sizes }] }] } }];"
},
"id": "build-create-payload",
"name": "Build Create Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-440,
0
]
},
{
"parameters": {
"method": "POST",
"url": "https://content-api.wildberries.ru/content/v2/cards/upload",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{$env.WB_API_TOKEN}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{JSON.stringify($json.createPayload)}}",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
}
},
"id": "create-card",
"name": "Create WB Card",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-180,
0
]
},
{
"parameters": {
"amount": 120,
"unit": "seconds"
},
"id": "wait-sync",
"name": "Wait Sync",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
60,
0
]
},
{
"parameters": {
"method": "POST",
"url": "https://content-api.wildberries.ru/content/v2/get/cards/list?locale=ru",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{$env.WB_API_TOKEN}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{JSON.stringify({settings:{sort:{ascending:false},filter:{textSearch:$('Build Create Payload').first().json.vendorCode,allowedCategoriesOnly:true,tagIDs:[],objectIDs:[],brands:[],imtID:0,withPhoto:-1},cursor:{limit:100}}})}}"
},
"id": "get-cards-list",
"name": "Get Cards List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
300,
0
]
},
{
"parameters": {
"jsCode": "const vendorCode = $('Build Create Payload').first().json.vendorCode;\nconst cards = $input.first().json.cards || [];\nconst card = cards.find(c => String(c.vendorCode) === String(vendorCode));\nreturn [{ json: { found: Boolean(card), vendorCode, nmID: card?.nmID, imtID: card?.imtID, sizes: card?.sizes || [], media: $('Build Create Payload').first().json.media } }];"
},
"id": "extract-card",
"name": "Extract Card Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
540,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "card-found",
"leftValue": "={{$json.found}}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-card-found",
"name": "IF Card Found",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
760,
0
]
},
{
"parameters": {
"method": "POST",
"url": "https://content-api.wildberries.ru/content/v3/media/save",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{$env.WB_API_TOKEN}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{JSON.stringify({nmId:$json.nmID,data:$json.media})}}"
},
"id": "upload-media",
"name": "Upload Media Links",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1020,
-120
]
},
{
"parameters": {
"method": "POST",
"url": "https://content-api.wildberries.ru/content/v2/cards/error/list?locale=ru",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{$env.WB_API_TOKEN}}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{JSON.stringify({cursor:{limit:100},order:{ascending:false}})}}"
},
"id": "get-card-errors",
"name": "Get Card Errors",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1020,
120
]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Set Sample Product",
"type": "main",
"index": 0
}
]
]
},
"Set Sample Product": {
"main": [
[
{
"node": "Build Create Payload",
"type": "main",
"index": 0
}
]
]
},
"Build Create Payload": {
"main": [
[
{
"node": "Create WB Card",
"type": "main",
"index": 0
}
]
]
},
"Create WB Card": {
"main": [
[
{
"node": "Wait Sync",
"type": "main",
"index": 0
}
]
]
},
"Wait Sync": {
"main": [
[
{
"node": "Get Cards List",
"type": "main",
"index": 0
}
]
]
},
"Get Cards List": {
"main": [
[
{
"node": "Extract Card Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Card Data": {
"main": [
[
{
"node": "IF Card Found",
"type": "main",
"index": 0
}
]
]
},
"IF Card Found": {
"main": [
[
{
"node": "Upload Media Links",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Card Errors",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {}
}
Что добавить к workflow для production
| Блок | Что добавить |
|---|---|
| Источник данных | Google Sheets, PostgreSQL, Airtable, ERP webhook. |
| Очередь | Split In Batches + Wait или отдельный queue mode. |
| Rate limit | Обработка 429 по headers. |
| Логи | Запись request/response в БД. |
| Ошибки | Отдельная таблица wb_card_errors. |
| Повтор | attempt, next_retry_at, max 3–5 попыток. |
| Сравнение payload | Хэш payload, чтобы не обновлять карточку без изменений. |
| Уведомления | Telegram/Email при 400, 409, 413, 422. |
Production-чек-лист
Перед созданием карточек
- Токен имеет категорию
Contentи права Read and Write. - Есть локальная копия справочников WB.
- Для каждого товара определён
subjectID. - Все
requiredхарактеристики заполнены. - Значения справочников взяты из WB.
- Размеры и баркоды валидны.
- Габариты в сантиметрах, вес в килограммах.
- Payload меньше 10 МБ и партия не превышает лимит метода.
После создания карточек
- Не ставить цены и остатки сразу.
- Дождаться синхронизации.
- Проверить карточку через
/content/v2/get/cards/list. - Сохранить
nmID,imtID,chrtID. - Если карточки нет — получить
/content/v2/cards/error/list. - Не загружать медиа, пока нет
nmID.
Перед обновлением карточек
- Получить текущую карточку.
- Сделать merge текущих данных и изменений.
- Проверить, что не потерялись
sizesиcharacteristics. - Не обновлять
photos,video,tagsчерез/cards/update. - Не менять цены существующих товаров через
/cards/update.
Перед ценами и остатками
- Проверить минимальную маржу.
- Проверить, что цена не стала в 3+ раза ниже старой цены со скидкой.
- Проверить, что есть
nmIDдля цены иchrtIDдля остатка. - После
204по остаткам выполнить проверочный запрос.
Для семантики
- Разделить запросы на title / характеристики / описание / реклама / минус-фразы.
- Не добавлять все ключи в title.
- Проверять, есть ли по запросу заказы, корзины или хотя бы переходы.
- Не обновлять SEO чаще, чем можно оценить эффект.
- Хранить историю изменений title и описания.
Частые ошибки
1. Отправили карточку и сразу ставите остатки
Так нельзя. После создания карточка синхронизируется. До завершения синхронизации цены и остатки могут не примениться.
2. Используете /cards/update как PATCH
Метод перезаписывает карточку. Если отправить только новое описание, можно потерять данные. Делайте read-merge-update.
3. Не храните chrtID
Для остатков нужен chrtID. Если хранить только nmID, автоматизация остатков и контроль поставок будут неполными.
4. Не проверяете /cards/error/list
200 не означает, что карточка появилась в каталоге. Если карточка не найдена — смотрите ошибки.
5. Семантика собирается вручную один раз
Запросы меняются. Нужна регулярная выгрузка: хотя бы раз в неделю для активных товаров и рекламных кампаний.
6. Все ключи идут в название
Title должен быть коротким и читаемым. Частотные, но второстепенные запросы лучше отправлять в описание, характеристики, инфографику и рекламу.
FAQ
Можно ли полностью автоматизировать создание карточек на Wildberries?
Да, если данные подготовлены: предмет, характеристики, размеры, баркоды, габариты, описание и цены. Но карточки создаются асинхронно, поэтому нужен этап проверки через список карточек и список ошибок.
Можно ли загружать фото сразу при создании карточки?
Практичнее сначала создать текстовую часть карточки, получить nmID, затем загрузить медиа через методы медиа. Это надёжнее и проще отлаживается.
Почему карточка не появилась, хотя API вернул 200?
Потому что создание пакетное и асинхронное. Если карточка не появилась в /content/v2/get/cards/list, нужно смотреть /content/v2/cards/error/list.
Можно ли менять только описание карточки?
Технически можно, но payload обновления должен содержать и остальные параметры карточки, которые нельзя потерять. Иначе есть риск перезаписать карточку неполными данными.
Где брать ключевые слова для карточек WB?
Основные источники: отчёт «Поисковые запросы на WB», отчёт «Поисковые запросы: ваши товары», Analytics API с подпиской «Джем», рекламные поисковые кластеры Promotion API.
Подходит ли n8n для автоматизации Wildberries?
Да, для MVP и средних процессов. Для больших каталогов лучше использовать n8n как оркестратор, а хранение статусов, очереди и логов вынести в PostgreSQL/Redis или отдельный backend.
Источники
- WB API — Work with products: dev.wildberries.ru/docs/openapi/work-with-products
- WB API — General API information, tokens, rate limits: api-information
- WB API — Analytics and Data: openapi/analytics
- WB API — Marketing and Promotions: openapi/promotion
- WB API Knowledge Base — Token Bucket, лимиты и ошибка 429: limity-zaprosov-wb-api
- WB Partners — Отчёт «Поисковые запросы на WB»: search-analytics-report
- WB Partners — Отчёт «Поисковые запросы: ваши товары»: search-queries-report
- WB API Forum — Медиа, создание и обновление карточек: dev.wildberries.ru/forum/1971