Импорт каталога Vizen по API
/docs/catalog-importcurrentRU
Гайд для внешнего разработчика или ИИ-агента, который наполняет магазин товарами по персональному ключу (PAT).
Соседний документ /docs/webcoding учит вёрстке страниц. Этот — данным: категории, товары, картинки, характеристики, варианты и связанные товары. Полная схема — /openapi.json.
Документ написан по итогам разбора реального импорта: каждый раздел «Подводный камень» — ошибка, которая уже стоила кому-то повторной работы.
0. Тридцать секунд: что нужно знать до первого запроса
| Факт | Следствие |
|---|---|
| Магазин определяется ключом, не параметром | company_id в запросе игнорируется; чужой тенант не виден |
| Цена — целое число в валюте магазина | price.currency в запросе игнорируется; сменить валюту ключом нельзя |
| Ключ пишет либо в живой сайт, либо в черновик | Свойство ключа, заголовком не переключается |
category_id — основная категория, categories[] — дополнительные | Дублировать основную в categories[] не нужно |
| Галерея — отдельный ресурс магазина, а не поле товара | Заводится в /galleries; товар, категория и статья ссылаются на неё gallery_ids[] (§7) |
| Списки требуют обе части пагинации | page.limit без page.number → 400 |
Каталог живёт без префикса /v1 | /products, /categories; файлы и организация — под /v1 |
1. Шаг 0 — паспорт ключа (обязателен)
GET /v1/account/token
Authorization: Bearer vz_pat_…{
"company_id": 7,
"contour": "prod",
"writes_to_live": true,
"dev_header_allowed": false,
"publish_allowed": false,
"expires_at": null,
"scopes": ["catalog:read", "catalog:write", "storage:read", "storage:write"],
"store": {
"name": "promo",
"slug": "promo",
"storefront_url": "https://promo.vizen.shop",
"currency": "USD",
"language": "ru",
"custom_domain": ""
},
"capabilities": {
"catalog_read": true, "catalog_write": true,
"storage_read": true, "storage_write": true,
"publish_dev": false, "store_settings_write": false
},
"warnings": [
"Ключ пишет в ЖИВОЙ магазин: изменения видны посетителям сразу…",
"Цена товара — целое число в валюте магазина (USD)…",
"Сменить валюту, язык или название магазина этим ключом НЕЛЬЗЯ…"
]
}Что с этим делать перед массовой записью:
writes_to_live: true→ сказать владельцу прямым текстом: *«записи сразу увидят посетители»* — и дождаться подтверждения.store.currency→ сверить с валютой прайса. Не совпало — остановиться. Валюту меняет владелец в админке; ключом это невозможно (capabilities.store_settings_write: false).capabilities→ проверить, что нужные права есть. Наличие тематического scope ещё не значит, что метод открыт для ключа: списокcapabilitiesсчитается по фактической карте доступа, ему и верить.warnings→ передать владельцу целиком, не пересказывая.
Подводный камень. Раньше контур приходилось выяснять опытом: записать товар и пойти смотреть витрину. Теперь этого делать не нужно — и не надо.
2. Пять правил, которые чаще всего ломают импорт
2.1. Валюта магазина сильнее присланной
// запрос
{ "item": { "price": { "price": 6400, "currency": "RUB" } } }
// ответ: currency = "USD" — валюта магазинаЦена — целое число в валюте магазина. Копейки/центы не отделяются отдельным полем: 6400 — это 6400 единиц валюты.
price.currency — read-only по смыслу: сервер вернёт валюту магазина независимо от присланного значения. Не полагайтесь на это поле при записи и сверяйте валюту на шаге 0.
2.2. Пагинация — обе части или ни одной
GET /products?page.limit=100 → 400 (Number must be >= 1)
GET /products?page.limit=100&page.number=1 → 200
GET /products → 200 (дефолтная страница)Передаёте page.limit — обязательно передавайте и page.number (нумерация с 1).
2.3. Основная и дополнительные категории — разные поля
| Поле | Что значит |
|---|---|
category_id | Основная категория. Определяет основной URL товара и его листинг |
categories[] | Дополнительные категории (id). Товар покажется и в них |
categories_replace: true | Полная замена набора дополнительных. [] + флаг = очистить |
Основную категорию не нужно повторять в categories[] — это создаёт лишнюю связь, а не «усиливает» привязку.
// создание — только основная
POST /products { "item": { "category_id": 85 } }
// создание — основная + две дополнительные
POST /products { "item": { "category_id": 85, "categories": [86, 87] } }
// правка — полная замена дополнительных
PUT /products/808 { "item": { "categories": [86, 87], "categories_replace": true } }
// правка — очистить дополнительные, основную оставить
PUT /products/808 { "item": { "categories": [], "categories_replace": true } }categories_replace работает только при правке. Без него categories при правке дополняет набор, а пустой массив означает «не менять», а не «снять все». При создании флаг не нужен — набор и так задаётся с нуля.
2.4. Тройные флаги — это строки, а не boolean
Часть переключателей наследуется от магазина, поэтому у них три состояния, и в JSON они приходят строкой:
| Поле | Значения | Смысл |
|---|---|---|
catalog_hidden | "" / "true" / "false" | "" — как настроено выше; "true" — скрыть из сеток витрины |
comments_enabled, questions_enabled | "" / "true" / "false" | то же |
reviews_scope | "" / own / category / shop / off |
is_published — обычный boolean. Не перепутайте: catalog_hidden: false (boolean) — ошибка типа, нужно "false" строкой либо "" для сброса.
Безопасная последовательность: создать товар с is_published: false → проверить чтением → опубликовать.
2.5. Порядок категорий задаётся перемещением, а не полем
order в теле создания игнорируется: новая категория всегда встаёт ПОСЛЕДНЕЙ среди соседей. Прочитанное значение поэтому закономерно отличается от отправленного — не считайте это ошибкой и не пытайтесь «додавить» нужное число повторной записью.
Так сделано, чтобы позиции соседей оставались плотным набором 0..n-1 без повторов: на нём держится арифметика перемещений before/after. Раньше поле писалось как прислано, и каталог быстро набирал группы, где у всех соседей order = 0 — тогда порядок фактически определялся внутренним id, а перетаскивание разделов в админке переставало работать.
Нужен конкретный порядок — расставьте его явно после создания всех разделов:
POST /categories/move
{ "from_id": "85", "to_id": "86", "type": "before" } // before | after | inside3. Модель каталога
Магазин (ключ)
├── Галерея (именованный набор медиа; своя ручка /galleries)
│ ↑ ссылаются товар, категория, статья и набор варианта
└── Категория (раздел витрины; дерево через parent_id)
└── Товар ── основная категория + дополнительные
├── Характеристики (значения атрибутов: материал, страна, …)
├── Варианты внутри товара (одна карточка, переключатели)
└── Членство в склейке (несколько карточек, связанных осями)Галерея не принадлежит товару: она живёт сама по себе, и одну и ту же галерею можно поставить на несколько товаров, на категорию и на статью сразу. Это и есть штатный способ «взять галерею из другого товара» (§7).
Атрибут (/attributes) — общий для магазина справочник: код, тип, виджет, список опций. Он используется и как характеристика, и как ось переключателя.
4. Варианты или связанные товары: как выбрать
Платформа поддерживает обе модели. Выбор делается один раз и меняется дорого — сравните по таблице.
| Что нужно | Варианты внутри товара | Склейка отдельных товаров |
|---|---|---|
| Отдельный URL у каждой комбинации | ✗ один URL + параметр | ✓ у каждого свой |
| Отдельные SEO-заголовки и описание | ✗ | ✓ |
| Отдельная галерея у комбинации | ✓ ссылка набора на галерею (gallery_id, §5 шаг 4б) | ✓ свой набор галерей у каждого |
| Свой SKU и цена | ✓ | ✓ |
| Отдельный складской остаток | ✓ | ✓ |
| Отдельная карточка в сетке каталога | ✗ одна | ✓ каждая (лишние прячутся catalog_hidden) |
| Стоимость заведения | ниже: 1 товар + набор | выше: N товаров + группа |
Правило выбора одной строкой: различаются только цена, SKU и остаток → варианты; различаются ещё и фотографии → варианты + своя галерея у набора; различается текст, URL или место в сетке каталога → склейка.
Показать «другой цвет» можно тремя способами, и они не заменяют друг друга:
| Если нужно… | Механизм | Куда пишется | Цена решения |
|---|---|---|---|
| у варианта другой чип и фото строки в корзине, слайдер общий | картинка набора image_id | PUT /products/{id}/variants | одна картинка на набор |
| при выборе варианта другие фотографии в слайдере, товар один, URL один | галерея + gallery_id набора | POST /galleries → PUT /products/{id} → PUT /products/{id}/variants | ≤10 галерей на объект × ≤24 элемента; SEO общий |
| у каждого варианта свой URL, SEO, описание и карточка в сетке | склейка товаров | N × POST /products + /product-groups | N товаров вести; лишние карточки прячутся catalog_hidden |
image_id и gallery_id — независимые поля набора: первый отвечает за чип и строку корзины, второй — только за слайдер.
Промежуточный случай (нужны отдельные URL, но контент одинаковый) — берите склейку: добавить контент потом дёшево, разрезать один товар на девять — нет.
5. Сценарий А — один товар с вариантами
Когда: футболка в трёх размерах, отличается только цена и остаток.
# 1) Справочник: атрибут-ось с опциями
POST /attributes
{ "item": {
"code": "size", "name": "Размер",
"data_type": "select", # ось переключателя — всегда select
"widget": "select", # text | select | icon | swatch | photo
"options": [ { "value": "s", "label": "S" },
{ "value": "m", "label": "M" },
{ "value": "l", "label": "L" } ] } }
# → result.id = 12, у опций свои id
# 2) Товар
POST /products
{ "item": { "name": "Футболка TERRA", "category_id": 85,
"price": { "price": 2400 }, "sku": "TERRA-TSHIRT",
"is_published": false } }
# → result.id = 900
# 3) Оси товара (replace-set)
PUT /products/900/variant-axes
{ "items": [ { "attribute_id": 12, "display": "text", "sort_order": 0 } ] }
# 4) Наборы (replace-set): values = attribute_id → value опции
PUT /products/900/variants
{ "items": [
{ "values": { "12": "s" }, "price": 2400, "sku": "TERRA-TSHIRT-S" },
{ "values": { "12": "m" }, "price": 2400, "sku": "TERRA-TSHIRT-M" },
{ "values": { "12": "l" }, "price": 2600, "sku": "TERRA-TSHIRT-L" } ] }
# 4б) галереи и привязка набора (опционально)
# Галерея — самостоятельный ресурс: сначала заводим её, потом ссылаемся.
POST /galleries
{ "item": { "name": "Синие",
"items": [ { "file": { "id": "<file_id_blue_1>" } },
{ "file": { "id": "<file_id_blue_2>" } } ] } }
# → result.id = "31" (id галереи — СТРОКА: в JSON int64 приходит строкой)
POST /galleries
{ "item": { "name": "Чёрные", "items": [ { "file": { "id": "<file_id_black_1>" } } ] } }
# → result.id = "32"
# галереи товара — это ССЫЛКИ, полный набор в нужном порядке
PUT /products/900
{ "item": { "gallery_ids": ["31", "32"], "gallery_ids_replace": true } }
PUT /products/900/variants
{ "items": [
{ "values": { "12": "s" }, "price": 2400, "sku": "TERRA-TSHIRT-S",
"gallery_id": "31" },
{ "values": { "12": "m" }, "price": 2400, "sku": "TERRA-TSHIRT-M" },
{ "values": { "12": "l" }, "price": 2600, "sku": "TERRA-TSHIRT-L" } ] }
# → variants_info.variants[].gallery_id в ответе GET /products/900 подтверждает привязку
# 5) Проверка и публикация
GET /products/900/variants
PUT /products/900 { "item": { "is_published": true } }Подводные камни:
variant-axesиvariants— replace-set: присылайте полный набор, отсутствующее удаляется.displayу оси уже:""/text/icon/swatch. Значениеselectдопустимо у атрибута, но не у оси товара.priceу набора необязателен: пусто → берётся базовая цена товара.- Дубль комбинации значений отклоняется — сервер считает подпись набора.
- Максимум: 20 осей, 500 наборов на товар.
gallery_idsвPUT /products/{id}— набор ссылок по паттернуcategories_replace: непустой массив → полная замена в присланном порядке; пустой массив или поле не прислано → привязки не трогаются (безопасно копировать телоGETвPUT); пустой массив +"gallery_ids_replace": true→ отвязать все. Содержимое галереи правится своей ручкойPUT /galleries/{id}, а не через товар.gallery_idнабора — ссылка на любую живую галерею магазина, не обязательно привязанную к этому товару. Это и есть «взять галерею из другого товара». Чужой или несуществующий id →404 GALLERY_NOT_FOUND. Галерею удалили позже — набор не ломается: на чтенииgallery_idвернётся"0", витрина покажет первую галерею товара.- Одна галерея может стоять на нескольких товарах, на категории и на статье одновременно —
GET /galleries/{id}/usageпокажет весь список. - Картинка набора (
image_id, шаг 4) и галерея набора (gallery_id) — независимые поля:image_idостаётся чипом и превью строки корзины,gallery_idотвечает только за слайдер витрины.
6. Сценарий Б — отдельные товары, связанные склейкой
Когда: тарелка в 3 цветах × 3 размерах, у каждой свои фото и свой URL.
# 1) Две оси-атрибута
POST /attributes
{ "item": { "code": "color", "name": "Цвет", "data_type": "select",
"widget": "swatch",
"options": [ { "value": "light", "label": "Светлый", "color_hex": "#E8DCC8" },
{ "value": "brown", "label": "Коричневый","color_hex": "#8B5A3C" },
{ "value": "black", "label": "Чёрный", "color_hex": "#2B2B2B" } ] } }
POST /attributes
{ "item": { "code": "diameter", "name": "Диаметр", "data_type": "select",
"widget": "select", "unit": "см",
"options": [ { "value": "20" }, { "value": "26" }, { "value": "32" } ] } }
# 2) Девять товаров — обычные POST /products, у каждого свой slug, фото, цена
# (или один POST /products/batch, см. §8)
# 3) Каждому товару — значения по обеим осям (replace-set)
PUT /products/808/attributes
{ "items": [ { "attribute_id": 13, "value": "light" },
{ "attribute_id": 14, "value": "20" } ] }
# 4) Группа
POST /product-groups
{ "item": { "name": "Тарелка TERRA", "group_type": "variant_group" } }
# → result.id = 5
# 5) Оси группы (replace-set) — порядок переключателей на витрине
PUT /product-groups/5/axes
{ "items": [ { "attribute_id": 13, "display": "swatch", "sort_order": 0 },
{ "attribute_id": 14, "display": "text", "sort_order": 1 } ] }
# 6) Участники (replace-set)
PUT /product-groups/5/members
{ "items": [ { "product_id": 808, "sort_order": 0 },
{ "product_id": 809, "sort_order": 1 } ] }
# 7) Проверка: карточка отдаёт готовую связь
GET /products/808
# → group_info: оси, значения, слаги соседей, текущий участникПодводные камни:
- Значения осей ставятся товару (
PUT /products/{id}/attributes), а не участнику группы: вmembersполеvalues— только для чтения. - Все три
PUT— replace-set. Добавляете десятый товар в группу — присылайте всех десятерых. - Чтобы в сетке каталога была одна карточка вместо девяти, остальным поставьте
catalog_hidden: "true". Из склейки они не выпадут — переключатель на карточке продолжит на них вести. group_infoв ответе товара — сосед поляresult, а не его вложение. Там же лежатvariants_info,attribute_sections,categories.
7. Картинки товара
Загрузка — три шага (или один, если файл уже в интернете); подробности и правила нарезки — в /docs/webcoding, §3.
POST /v1/storages/files
{ "section": "product", "file_type": "image", "original_name": "plate.jpg" }
# → result.id = <file_id>, upload.id, upload.url
PUT <upload.url> # байты, без Authorization
POST /v1/storages/files/<upload.id>/done { }
# файл уже в сети — одним запросом
POST /v1/storages/files/from-url
{ "source_url": "https://…/plate.jpg", "section": "product", "file_type": "image" }
# галерея заводится ОТДЕЛЬНО и один раз
POST /galleries
{ "item": { "name": "Основная",
"items": [ { "file": { "id": "<file_id_2>" } },
{ "file": { "id": "<file_id_3>" } } ] } }
# → result.id = "31"
# привязка при СОЗДАНИИ — превью объектом, галереи ссылками
POST /products
{ "item": { "name": "Тарелка", "preview": { "id": "<file_id>" },
"gallery_ids": ["31"] } }
# привязка при ПРАВКЕ — главная картинка строкой-идентификатором
PUT /products/808
{ "item": { "preview_id": "<file_id>",
"gallery_ids": ["31"], "gallery_ids_replace": true } }Подводные камни:
- Создание и правка принимают главную картинку по-разному: при создании —
preview: { "id": … }, при правке —preview_id: "…"строкой. Галереи в обоих случаях —gallery_ids, массив id-строк. Прислать при правкеpreviewвместоpreview_id— тихо ничего не изменить: запрос ответит200, картинка останется прежней. Всегда перечитывайте товар после записи. - Снять главную картинку при правке —
preview_id: ""(пустая строка). - В
/doneидёт `upload.id`, а к товару цепляется `result.id`. Это разные значения. file_type— категория (image | video | audio | model | file), не MIME.- Один
file_idможно использовать в нескольких галереях и товарах — повторно тот же файл не загружайте. public_pathв ответе хранилища — полный адрес объектного хранилища. На витрине ставьте относительный путь витрины с нарезкой (/w/{ширина}/webp{путь}), а не этот URL и не оригинал.gallery_ids— replace-set ссылок: присылайте полный список в нужном порядке.
7.1. Галерея — самостоятельный ресурс
Галерея не лежит внутри товара. Это отдельная сущность магазина со своим CRUD; товар, категория (любого типа) и статья только ссылаются на неё.
| Ручка | Что делает |
|---|---|
GET /galleries | живые галереи магазина вместе с элементами, свежие сверху; страницами — ?page.number=&page.limit= (1..100), без параметров первые 50; в ответе рядом с result[] есть total и currentPage |
GET /galleries/{id} | одна галерея с элементами |
POST /galleries | создать галерею вместе с элементами |
PUT /galleries/{id} | правка: имя и/или элементы |
DELETE /galleries/{id} | мягкое удаление; занятую удалить нельзя |
GET /galleries/{id}/usage | где используется: товары, категории, статьи и наборы вариантов |
Форма галереи:
{ "id": "31", "name": "Синие", "source": "images", "canvas_id": "0",
"items": [
{ "item_key": "8231acd9-…", "kind": "image",
"file": { "id": "<file_id>", "url": "…" }, "poster": null,
"html": "", "caption": "вид спереди" },
{ "item_key": "4fba917d-…", "kind": "html", "file": null,
"html": "<b>Схема сборки</b>", "caption": "" } ] }Что важно знать про элементы:
- роль (`kind`) ставит сервер, а не вы. Она выводится из типа файла:
image | video | audio. Вручную задаётся только"html"— у него нет файла, есть разметка. Прислалиkind, не совпавший с типом файла (например"video"на картинке) →400 GALLERY_ITEM_KIND_MISMATCH. Тихой починки нет: молчаливая подмена роли даёт витрину, не похожую на то, что вы отправляли; - у не-html элемента файл обязателен (
400 GALLERY_ITEM_FILE_REQUIRED), у html обязателен непустойhtml(400 GALLERY_ITEM_HTML_REQUIRED) не длиннее 64 КБ (400 GALLERY_ITEM_HTML_TOO_LONG); poster— постер видео или обложка звука, тот же{ "id": … };item_key— стабильный ключ элемента. Пусто на входе → сервер выдаст uuid; прислали свой — сохранится. По нему элемент остаётся «тем же» при перестановке, поэтому при повторе запроса после таймаута присылайте собственные ключи;itemsвPUT /galleries/{id}— replace-set: непустой список заменяет все элементы целиком; пустой список (или отсутствие поля) означает «не трогать». Чтобы очистить галерею — пустой список плюс"items_replace": true: без флага удалить последний элемент нельзя вовсе. Имя правится отдельно и независимо:{ "item": { "name": "…" } }элементы не тронет.
Привязка к объектам — одинаковая у товара, категории и статьи:
PUT /products/808 { "item": { "gallery_ids": ["31","32"], "gallery_ids_replace": true } }
PUT /categories/85 { "item": { "gallery_ids": ["31"], "gallery_ids_replace": true } }
PUT /articles/12 { "item": { "gallery_ids": ["31"], "gallery_ids_replace": true } }На чтении карточки приходят оба поля: gallery_ids — набор ссылок по порядку, galleries — их содержимое. Одна галерея законно стоит на нескольких объектах — правка её содержимого меняет вид всех сразу, и это ожидаемо.
⚠️ galleries может быть длиннее gallery_ids. Если набор варианта ссылается на галерею, которая к самому товару не привязана, её содержимое всё равно приезжает в карточку — в конце galleries, но не в gallery_ids. Так сделано нарочно: вернув тело GET обратно в PUT, вы не привяжете к товару чужую галерею. Из этого следует правило: набор привязок читайте из gallery_ids, а содержимое для показа ищите в galleries по id, а не по позиции. Фолбэк превью считается только по ПРИВЯЗАННЫМ галереям — товар, у которого галерея есть только по ссылке набора, отдаёт preview: null и preview_auto: false. Проверено вживую.
Удалить занятую галерею нельзя: 400 GALLERY_IN_USE: used in N places. Сначала снимите ссылки (у объектов — gallery_ids: [] + gallery_ids_replace, у наборов — gallery_id: "0"), сверьтесь по GET /galleries/{id}/usage, и только потом удаляйте.
Лимиты: ≤24 элемента в галерее, имя 1..64 символа, ≤10 галерей на один объект.
7.2. Что игнорируется молча — и как проверить, что запись состоялась
Сервер отвечает 200 и в этих случаях, но делает не то, что вы ожидали:
| Что прислали | Что произошло на самом деле |
|---|---|
preview вместо preview_id при правке | картинка не изменилась |
gallery_ids: [] без gallery_ids_replace | прочитано как «не трогать», а не «отвязать все» |
categories: [] без categories_replace | то же самое: дополняем, не заменяем |
price.currency | отброшено, валюта берётся из магазина |
company_id в теле запроса | отброшено, магазин определяет ключ |
order при создании категории | отброшено, раздел встаёт последним |
items у галереи, которой управляет канвас-документ (canvas_id ≠ 0) | отброшены; хранимые элементы остаются резервной копией до отвязки — включая запрос с canvas_id: 0 и элементами вместе: отвязка сработает, элементы нет |
items: [] у галереи без items_replace | прочитано как «не трогать»; очистить — только с флагом |
чтение gallery_ids / galleries из списков | у GET /products полей нет вовсе, у GET /categories они приходят пустыми — списки галереи не отдают, там есть только preview |
Проверка после каждой записи — одна и та же: перечитать объект и сверить поля, которые отправляли. Ответ 200 доказывает только то, что запрос разобран.
# записали
PUT /products/808 { "item": { "gallery_ids": ["31"], "gallery_ids_replace": true } }
# перечитали и сверили
GET /products/808 # result.gallery_ids == ["31"], result.galleries[0].id == "31"
GET /galleries/31 # items — те, что отправляли, в том же порядке
GET /galleries/31/usage # в списке есть product 808Три поля, которые особенно легко прочитать неверно:
preview_auto: true— превью показано фолбэком: своего у товара нет, и сервер взял первый элемент-изображение первой галереи. Не записывайте это превью обратно вpreview_id— получите дубль;gallery_idнабора равен"0"— ссылки нет либо галерею удалили; витрина покажет первую галерею товара;source: "canvas"у галереи — её элементы ведёт канвас-документ (canvas_id), и присланные вами элементы туда не попадут;galleriesдлиннееgallery_ids— в конце стоят галереи, на которые ссылаются наборы вариантов, а к товару они не привязаны.
8. Массовый импорт
POST /products/batch
{ "items": [ { … }, { … } ] }Ответ — частичный успех: по каждой позиции отдельно приходит результат или ошибка. Разбирайте ответ поштучно; общий 200 не значит, что прошли все.
Порядок безопасного импорта:
- Паспорт ключа (§1) → сверка валюты и контура, подтверждение владельца.
- Категории → зафиксировать порядок через
/categories/move. - Атрибуты и опции → запомнить их
id, они нужны везде дальше. - Файлы → собрать карту
локальное имя → file_id. - Галереи (
POST /galleries) → собрать картунабор фото → gallery_id. Галерея заводится до товара: товар ссылается на неё, а не наоборот. - Один товар-канарейка,
is_published: false→ прочитать обратно → сверить каждое поле, которое отправляли → показать владельцу. - Остальной каталог батчами.
- Связи (варианты или склейки).
- Публикация:
is_published: true. - Проверка витрины: страница категории и карточка товара отвечают
200и содержат ожидаемое.
Идемпотентность. Сервер не хранит ваш внешний ключ товара. Ведите локальный манифест внешний артикул → id товара → gallery_id → file_id → slug и перед созданием проверяйте наличие: GET /products/by-slug/{slug}. Иначе повторный запуск создаст дубли.
Откат. Не удаляйте — скрывайте: is_published: false + catalog_hidden: "true". Это обратимо и не рвёт ссылки.
9. Ошибки
Основная форма — RPC-конверт:
{ "error": "rpc error: code = InvalidArgument desc = INVALID_WIDGET" }Машинный код — первое слово после desc = . Частые:
| Код | Что значит |
|---|---|
PAT_METHOD_NOT_ALLOWED | Метод вообще недоступен ключам этого типа |
PAT_SCOPE_MISSING | Не хватает права; требуемое указано в тексте |
AUTH_COMPANY_PROBLEM | Магазин не определён — проверьте ключ |
INVALID_WIDGET | Виджет вне списка text · select · icon · swatch · photo |
INVALID_DATA_TYPE | Тип вне списка string · number · bool · select · multiselect · date · color · url · group |
INVALID_CODE, INVALID_OPTION_VALUE | Код атрибута и value опции — латиница-слаг, не текст для людей |
FILE_IN_USE | Файл нельзя удалить: он привязан |
CATEGORY_FILE_NOT_FOUND (404) | preview_id или seo.og_image_id категории — не живой файл вашего магазина. Проверка появилась в волне «Галереи»: раньше такая запись проходила молча |
URL_NOT_ALLOWED, URL_FETCH_FAILED, FILE_TOO_LARGE | Загрузка по ссылке |
Галереи (§7) отвечают своими кодами:
| Код | HTTP | Что значит |
|---|---|---|
GALLERY_NOT_FOUND | 404 | Нет такой галереи — либо она чужая, либо удалена. Один код на оба случая: существование чужого ресурса сервер не подтверждает |
GALLERY_IN_USE: used in N places | 400 | Галерея занята; снимите ссылки, GET /galleries/{id}/usage покажет где |
GALLERY_NAME_REQUIRED / GALLERY_NAME_TOO_LONG | 400 | Имя обязательно, максимум 64 символа |
GALLERY_TOO_MANY_ITEMS | 400 | Больше 24 элементов в галерее |
GALLERY_DUPLICATE_ITEM_KEY | 400 | Один item_key встретился дважды |
GALLERY_ITEM_KIND_MISMATCH | 400 | Присланная роль не совпала с типом файла (или файл такой категории в галерее не показывается) |
GALLERY_ITEM_FILE_REQUIRED | 400 | У элемента без kind: "html" нет файла |
GALLERY_ITEM_HTML_REQUIRED | 400 | У kind: "html" пустая разметка |
GALLERY_ITEM_HTML_TOO_LONG | 400 | Разметка элемента больше 64 КБ |
GALLERY_FILE_NOT_FOUND | 404 | file_id или poster.id — не живой файл вашего магазина |
GALLERIES_TOO_MANY | 400 | Больше 10 галерей на один объект |
GALLERY_DUPLICATE_LINK | 400 | Одна галерея указана в gallery_ids дважды |
GALLERY_MANAGED_BY_CANVAS | 400 | Нельзя одним запросом включить канвас-режим и прислать элементы: сначала привязка, потом элементы |
GALLERY_DUPLICATE_CANVAS | 400 | Этот канвас-документ уже ведёт другую галерею магазина |
CANVAS_DOCUMENT_NOT_FOUND | 404 | canvas_id — не живой документ вашего магазина |
CANVAS_TYPE_MISMATCH | 400 | Документ есть, но он не того типа |
⚠️ Больше 10 значений в gallery_ids одиночная ручка отклоняет раньше машинного кода — сообщением валидатора (value must contain no more than 10 item(s)). Код GALLERIES_TOO_MANY в чистом виде приходит из POST /products/batch, где ошибка возвращается по каждой позиции отдельно.
Есть вторая форма — для неизвестного пути. Она приходит с 404 и содержит подсказку:
{ "error": "not_found", "path": "/v1/products/808",
"hint": "catalog is unprefixed (/products); storage/account/org under /v1",
"docs": "/openapi.json" }Клиент должен уметь разобрать обе. Признак: наличие поля path.
Ограничитель нагрузки отвечает 429 и сообщает время ожидания — уважайте его и повторяйте запрос после паузы.
10. Стоп-условия
Остановиться и спросить владельца, если:
- валюта магазина не совпала с валютой прайса;
- ключ пишет в живой магазин, а подтверждения на массовую запись не было;
- товар после создания не находится в фильтре своей категории;
- прочитанное значение отличается от отправленного, и это не описано здесь;
- публичная страница отвечает не
200; - пришёл
429, а повтор с паузой не помогает.
И правило, которое не обсуждается: ключ не попадает ни в один файл, лог, HTML, отчёт или сообщение. Он живёт в отдельном хранилище с правами 600.
11. Справочник полей с сюрпризами
| Поле | Тип | Что важно знать |
|---|---|---|
price.price | целое | В валюте магазина. Без дробной части |
price.currency | строка | Игнорируется при записи; ответ — валюта магазина |
category_id | целое | Основная категория |
categories[] | массив целых | Дополнительные. При правке дополняет набор; полная замена — только с categories_replace: true |
preview / preview_id | объект / строка | Создание — preview: {id}; правка — preview_id: "…". Перепутать = тихий 200 без изменения |
catalog_hidden | строка | "" / "true" / "false" — не boolean |
is_published | boolean | Обычный флаг |
order (категория) | целое | При создании игнорируется — раздел встаёт последним. Порядок задавать через /categories/move |
widget (атрибут) | строка | text · select · icon · swatch · photo |
display (ось) | строка | "" · text · icon · swatch — уже, чем widget |
data_type (атрибут) | строка | Ось переключателя — всегда select |
values (набор) | объект | attribute_id (строкой) → value опции |
gallery_ids | массив строк | Ссылки на галереи (int64 приходит строкой). Непустой → замена; пустой без gallery_ids_replace → «не трогать» |
galleries | массив объектов | Только на чтении карточки. В списках пуст. Может быть длиннее gallery_ids: в конце — галереи по ссылкам наборов |
items_replace (галерея) | boolean | Нужен, чтобы очистить элементы: пустой items без флага = «не трогать» |
gallery_id (набор) | строка | Любая живая галерея магазина, не только «своя». "0" = ссылки нет или галерею удалили |
kind (элемент галереи) | строка | Ставит сервер из типа файла. Вручную — только "html". Несовпадение = отказ |
item_key (элемент галереи) | строка | Стабильный ключ. Пусто → выдаст сервер; свой — сохранится при повторе запроса |
source, canvas_id (галерея) | строка | "canvas" = элементы ведёт канвас-документ, присланные игнорируются |
preview_auto | boolean | true = превью подставлено фолбэком из галереи. Не записывать обратно в preview_id |
public_path (файл) | строка | Полный адрес хранилища. На витрине — относительный путь с нарезкой |
page.number | целое | С 1. Обязателен, если передан page.limit |
group_info, variants_info, attribute_sections, categories | объекты | Соседи result в ответе товара, а не его поля |
12. Куда дальше
| Документ | О чём |
|---|---|
/docs/webcoding | Вёрстка страниц, HTML-блоки, картинки и нарезка, лимиты |
/openapi.json | Полная схема API |
/v1/account/token | Паспорт ключа: контур, магазин, валюта, права |
/llms.txt | Краткая карта API для ИИ-агента |