# Vizen API — быстрый старт

> **Резюме.** Практический гайд для внешнего разработчика или ИИ-агента: выпустить
> ключ, прочитать его паспорт, завести карточки товара, загрузить картинки, не
> упереться в лимиты и сдать работу владельцу. Всё — по реальному контракту
> `api.vizen.shop`; источник правды — proto в `api/*/*.proto` и схема
> `/openapi.json`. Это ЕДИНСТВЕННАЯ редакция быстрого старта: она же отдаётся
> бэком по `GET /docs/quickstart` и показывается на сайте `vizen.shop/docs/quickstart`.

**Status:** current · **Verified:** 2026-09-10, сверено с кодом (`domain/api_scopes.go`,
`domain/api_access.go`, лимиты `cmd/core/main.go`, ручки `api/*/*.proto`) ·
**Owner:** backend · **Serves:** `GET /docs/quickstart`

## 0. Что ещё читать (машиночитаемая поверхность)

| Адрес | Что это |
|---|---|
| `GET /docs/index.json` | индекс всей документации: путь, заголовок, зачем читать, статус |
| `GET /llms.txt` | порядок чтения для ИИ-агента — начинать отсюда |
| `GET /v1/account/token` | паспорт вашего ключа (см. §1.3) |
| `GET /docs/skills` | платформа, объяснённая по путям работы; начинать с `vizen-start` |
| `GET /openapi.json` | полная OpenAPI-схема (то же на `/v1/openapi.json`, `/compiled.swagger.json`); `GET /partner/openapi.json` — только то, что открыто ключу |
| `GET /docs/scopes.json` | все права ключа, с пометкой чувствительных — генерится из кода |
| `GET /docs/events.json` | все события вебхуков и параметры доставки — генерится из кода |
| `GET /docs/catalog-import` | гайд наполнения каталога: категории, характеристики, варианты, склейки |
| `GET /docs/webcoding` | гайд вёрстки страниц магазина кодом |
| `GET /docs/vz-keys` | ключи `vz-`: своя вёрстка на данных магазина |
| Области (`/docs/catalogue`, `/docs/widgets-area`, `/docs/own-markup`, `/docs/chrome`, `/docs/pricing`, `/docs/promotions`, `/docs/orders`, `/docs/stock`, `/docs/webhooks`, `/docs/troubleshooting`) | поведение, которое не описывается одной ручкой |
| `GET /` | карта корня API в JSON |

## 1. База и аутентификация

- **База API:** `https://api.vizen.shop`
- **Ключ (PAT):** заголовок `Authorization: Bearer vz_pat_<…>` в каждом запросе.
- **Где выпустить:** кабинет магазина → «API-доступ» (`https://admin.vizen.shop/api-access`)
  → «Выпустить токен». Ключ показывается **один раз** — скопируйте сразу.
- **Права (scopes)** отмечаются при выпуске, до 32 на ключ. Существующий ключ
  новые права автоматически не получает.

### 1.1 Права

Полный машинный список — `GET /docs/scopes.json`. Человеческая таблица:

| scope | что открывает |
|---|---|
| `catalog:read` | чтение товаров/категорий/характеристик **+ владельческий вид**: черновики, скрытые категории, очередь модерации отзывов, дев-контур ключа. Без него публичные ручки отвечают как анониму |
| `catalog:write` | создание/правка товаров, категорий, характеристик |
| `orders:read` / `orders:write` ⚠️ | заказы (ПДн покупателей) |
| `stats:read` | статистика |
| `storage:read` | чтение медиатеки |
| `storage:write` | **загрузка файлов** (нужно для картинок) |
| `domain:read` / `domain:write` | свой домен магазина |
| `promotions:read` / `promotions:write` | акции, купоны, наборы |
| `pricing:read` / `pricing:write` ⚠️ | ценовые правила по количеству (закрытая коммерческая операционка) |
| `customers:read` / `customers:write` ⚠️ | клиентская база и покупательские группы (ПДн) |
| `access:read` / `access:write` ⚠️ | политики доступа к закрытым ресурсам, парольные выдачи |
| `publish:write` ⚠️ | публикация черновика на живой сайт — галочка «Разрешить публиковать черновик» при выпуске ключа-черновика |
| `forms:read` / `forms:write` | конструктор лид-форм (схемы полей) |
| `leads:read` / `leads:write` ⚠️ | заявки покупателей. **ПДн** — `catalog:*` и `forms:*` их не открывают |

⚠️ — **чувствительное право**: выдаёт только владелец или администратор магазина.
Сотрудник с ролью редактора такой ключ выпустить не может, а при понижении роли
уже выданные ключи с этими правами отзываются автоматически. Рабочие права
(каталог, медиатека, акции, статистика, домен, формы) этим гейтом не закрыты.

Для «завести каталог с картинками» нужны минимум **`catalog:write` + `storage:write`**
(и `catalog:read` для проверки).

### 1.2 Префиксы путей (неоднородность, знайте заранее)

- Каталог (**товары, категории, атрибуты, акции, заказы**) — **без** `/v1`:
  `POST /products`, `GET /products/{id}`.
- Хранилище / аккаунт / организация — **под** `/v1`: `POST /v1/storages/files`,
  `GET /v1/account/token`, `GET /v1/orgs/current`.

### 1.3 Первый запрос — паспорт ключа

Делать это **до первой записи**:

```bash
curl https://api.vizen.shop/v1/account/token \
  -H "Authorization: Bearer vz_pat_ВАШ_КЛЮЧ"
```

- **200** → ключ рабочий. В ответе:
  - `contour` и `writes_to_live` — уйдут ли правки сразу на живой сайт. `prod`
    пишет на витрину, `dev` — в черновик, который публикует человек. **Контур —
    свойство самого ключа**: заголовком или `company_id` не переключается.
    Если `writes_to_live: true` — предупредите владельца до массовой записи.
  - `store.currency` — валюта магазина. Цена товара пишется целым числом в ней;
    присланная `price.currency`, не совпавшая с магазином, отклоняется —
    `400 CURRENCY_IS_STORE_LEVEL`. Сменить валюту ключом нельзя.
  - `store.storefront_url` — адрес витрины, где проверять результат (учитывает
    подключённый свой домен). Не угадывайте его.
  - `capabilities` — что ключ **реально** может. Верить нужно этому, а не набору
    scopes: наличие тематического права не значит, что метод открыт ключам.
  - `warnings[]` — передать владельцу целиком.
  - `guides` — адреса гайдов для этого ключа.
- **401** → неверный или просроченный ключ.
- **403 `PAT_SCOPE_MISSING`** на приватных ручках → нет нужного права (в тексте
  ошибки — `required_scope`). **403 `PAT_METHOD_NOT_ALLOWED`** → метод закрыт для
  ключей вообще (настройки витрины, члены компании, сами ключи и вебхуки) —
  это делает человек в кабинете.

Чтение каталога для проверки ключа не годится: публичные ручки отвечают 200 и
без `catalog:read` — проекцией анонима (только опубликованное, без черновиков),
так что «200 на `/products`» ничего не доказывает.

Дальше по знакомству: `GET /categories` (узнать `category_id`),
`GET /products/{id}` (одна карточка целиком).

## 2. Карточка товара

### Создать — `POST /products`

```json
{
  "item": {
    "name": "Кровать двухъярусная Аврора 120×90",
    "description": "Массив сосны, цвет Сэнди",
    "price": { "price": 45892, "currency": "RUB" },
    "category_id": 12,
    "sku": "AVRORA-120-90-SANDY",
    "tags": ["кровати", "детская"],
    "seo": { "slug": "avrora-120-90-sandy" }
  }
}
```

- `price.price` — **целые единицы валюты** (рубли, не копейки), валюта — магазина (§1.3).
- `category_id` обязателен, чтобы товар показывался в разделе витрины. Это
  **основная** категория — от неё строится канонический URL карточки.
- `categories` (массив id) — **дополнительные** категории: товар виден и в их
  разделах, на URL не влияют. В `PUT`: поле не прислано — не трогается; прислано
  непустым — полная замена; снять все — `"categories": [], "categories_replace": true`.
- `slug` живёт **внутри `seo`** (`seo.slug`) — это адрес страницы товара. Можно
  не присылать — сервер сгенерирует из `name` (транслитерация + дедуп). Ручной
  слаг с кириллицей/пробелами не отвергается — транслитерируется в
  `[a-z][a-z0-9_-]*` (итог виден в ответе). Отказы только для безнадёжных форм:
  чисто числовой → `SLUG_NUMERIC_FORBIDDEN`, не с буквы → `SLUG_INVALID`,
  служебные слова (`cart`, `admin`…) → `SLUG_RESERVED`.
- `sku` — уникален в магазине среди неудалённых товаров (пустой — можно, в
  уникальности не участвует). Дубль → `409 already_exists`; ответ пока не
  говорит, какое поле конфликтует (`sku` или слаг) — при импорте проверяйте
  занятость заранее. Занятый слаг в прод-контуре отвечает `SLUG_TAKEN`.

### Получить / список

- `GET /products` — список; фильтры — query-параметрами **с префиксом `filter.`**
  (`?filter.category_id=12`). Без префикса параметр молча проглатывается и
  возвращается **весь каталог с 200** — сравнивайте `total` до и после. Подробно —
  `/docs/catalogue`.
- `GET /products/{id}` — один товар со всеми полями (галерея, цена, рейтинг,
  варианты, остаток).

### Редактировать — `PUT /products/{id}`

Partial по полям **верхнего уровня**: присылаете только то, что меняете.

```json
{ "item": { "price": { "price": 43990, "currency": "RUB" } } }
```

> ⚠️ **ГЛАВНАЯ ЛОВУШКА: вложенный `seo` заменяется ЦЕЛИКОМ.** Пришлёте `seo`
> только с мета-тегами и без `slug` — slug обнулится, и у товара пропадёт адрес
> страницы. Правило: трогаете `seo` — сперва `GET`, поменяйте нужное поле,
> отправьте `seo` целиком (read-modify-write).

### Удалить / опубликовать

- `DELETE /products/{id}` — мягкое удаление (восстановимо 30 дней).
- `is_published: true` (в `item`) — товар появляется в магазине сразу
  (в прод-контуре; в дев-контуре — после публикации черновика человеком).
- `is_shared` — отдельный флаг «в общий маркетплейс» (с модерацией).

### Импорт пачкой — `POST /products/batch`

Заводите каталог целиком — не гоняйте `POST /products` в цикле, шлите до
**100 товаров за раз**. Позиция в `items` — тот же объект, что `item` в
одиночной ручке, и те же проверки.

```json
{ "items": [
  { "name": "Кровать Аврора", "price": {"price": 45892, "currency": "RUB"}, "category_id": 12 },
  { "name": "Стол Лофт",      "price": {"price": 18900, "currency": "RUB"}, "category_id": 12 }
] }
```

> ⚠️ **ЧАСТИЧНЫЙ УСПЕХ — разбирайте `results`, а не только HTTP-код.** Ответ —
> 200, даже если часть позиций не создалась: одна кривая строка прайса не
> отменяет 99 хороших. Судьба каждой — в своём элементе `results` по `index`
> (позиция в `items`, с нуля).

```json
{
  "results": [
    { "index": 0, "id": 511, "slug": "krovat-avrora" },
    { "index": 1, "error": "CATEGORY_NOT_FOUND", "message": "" }
  ],
  "created": 1,
  "failed": 1
}
```

- Успех позиции: `id > 0`, `error` пустой. Провал: `id = 0`, `error` — код
  (`PRODUCT_NAME_REQUIRED`, `CATEGORY_NOT_FOUND`, `CATEGORY_TYPE_MISMATCH`,
  `SLUG_TAKEN`, `already_exists` для sku, `PRODUCT_FILE_NOT_FOUND`,
  `PRODUCT_LIMIT_REACHED`, `INTERNAL`).
- Повторяйте только упавшие позиции: перезалив всей пачки создаст дубли
  слагов/sku у тех, что прошли.
- Квота тарифа (`products_max`) считается на **каждой** позиции — пачкой потолок
  не перепрыгнуть; лишние вернут `PRODUCT_LIMIT_REACHED`.
- Транзакция — на позицию, не на пачку: частично созданное остаётся созданным.
- HTTP-ошибкой (не `results`) отвечают только «весь запрос негоден»: нет ключа,
  нет магазина, `items` пустой или длиннее 100.

## 3. Картинки — то, на чём спотыкаются

Картинка грузится **не** одним запросом с файлом, а через presigned-URL: сервер
даёт временную ссылку, вы кладёте байты напрямую в хранилище, потом
подтверждаете. Итог — **UUID файла**, который цепляется к товару.

1. **Попросить ссылку на загрузку** — `POST /v1/storages/files` (scope `storage:write`)

   ```json
   { "section": "content", "file_type": "image", "original_name": "avrora.jpg" }
   ```

   - `file_type` — это **категория, НЕ MIME**: `image | video | audio | model | file`
     (мин. 3 символа). Пришлёте MIME (`image/jpeg`) — сервер сам нормализует в
     категорию, чтобы файл не выпадал из фильтров медиатеки.
   - `section` — папка-раздел (мин. 3 символа); канон для фото товаров и
     картинок страниц — `content`.

   Ответ:

   ```json
   {
     "result": { "id": "95fe4e96-5903-4dc3-873f-2d6e54de3047", "public_path": "/files/…" },
     "upload": { "mode": "presign", "id": "<UPLOAD_ID>", "url": "<PRESIGNED_PUT_URL>" }
   }
   ```

   > ⚠️ **Два разных id.** `result.id` — UUID файла (его цепляете к товару).
   > `upload.id` — идентификатор загрузки для шага 3. Их путают чаще всего.

2. **Залить байты напрямую** — `PUT <upload.url>`, тело = **сырые байты файла**,
   заголовок `Content-Type: image/jpeg` (должен совпадать с типом файла). Это
   прямой PUT в хранилище **без** `Authorization` — ссылка сама авторизует.

3. **Подтвердить** — `POST /v1/storages/files/<UPLOAD_ID>/done` (scope
   `storage:write`). Здесь используется `upload.id`, а **не** `result.id`.

> ⚠️ **PNG/JPEG-оригиналы контента конвертируются в WebP.** Для контент-секций
> (`content`, `products`) сервер на шаге `/done` конвертирует растровый оригинал
> в WebP q90 и не хранит исходный PNG/JPEG. `id` файла стабилен, но URL меняется
> (расширение станет `.webp`) — `public_path` из ответа шага 1 после `/done`
> может устареть. Правило: финальный URL читайте **после** `/done` —
> `GET /v1/storages/files/{id}` → `result.public_path`. К товару файл цепляйте
> по `id` — там ничего не ломается.

### Быстрый путь для импорта — картинка по ссылке

Если исходник уже лежит в интернете (выгрузка поставщика, старый сайт), три
шага не нужны: `POST /v1/storages/files/from-url` (scope `storage:write`) —
сервер скачает файл сам и вернёт готовый файл. На каталоге в пару тысяч фото
это разница в часы.

```json
{ "source_url": "https://cdn.postavshik.ru/foto/avrora.jpg",
  "section": "content", "file_type": "image", "original_name": "avrora.jpg" }
```

```json
{ "result": { "id": "95fe4e96-…", "public_path": "https://…/avrora.webp" },
  "file_size": 148213 }
```

- `result.id` — тот же UUID файла, что и в presign-пути: сразу цепляйте к
  товару. Никакого `/done` — конвертация в WebP и предгенерация нарезки уже
  сделаны, `public_path` финальный.
- `original_name` можно не слать — возьмётся из адреса; расширение приводится к
  фактическому формату.
- Форматы: `jpeg | png | gif | webp | bmp | tiff`; тип определяется по байтам, а
  не по `Content-Type` ответа. 3D-модели и прочие файлы — presign-путём.
- ⚠️ Только публичные http/https-адреса. Внутренняя сеть (`localhost`, `127.*`,
  `10.*`, `172.16–31.*`, `192.168.*`, `169.254.*`, `::1`, `fc00::/7`) — `400
  URL_NOT_ALLOWED`; проверяется каждый хоп редиректа, их не больше трёх. Не
  скачалось / не картинка / не 200 → `400 URL_FETCH_FAILED`; больше 10 МБ →
  `400 FILE_TOO_LARGE`; таймаут — 30 с.

### Прицепить к товару

Фото товара кладём в **`gallery`** (слайдер карточки), используя `result.id`:

```json
PUT /products/{id}
{ "item": { "gallery": [ { "id": "<UUID1>" }, { "id": "<UUID2>" } ] } }
```

Миниатюру в каталоге система берёт из первого фото галереи. `preview` —
**необязательное** отдельное поле (особая промо-превьюшка): не заполняйте без
нужды и **не дублируйте** в него фото из галереи, иначе на витрине оно покажется
дважды. `preview_id` — только если нужна миниатюра, отличная от галереи; снять —
`"preview_id": ""`. Галерея по группам и варианты — `/docs/catalog-import`.

### Как картинка отдаётся на витрине

- Оригинал: по `public_path` → `https://<магазин>.vizen.shop/files/…`
- Нарезка под ширину (быстрее, webp): `https://<магазин>.vizen.shop/w/{ширина}/webp/<путь>`
  — `/w/320/webp/…` (миниатюра), `/w/1600/webp/…` (крупно).
- Лестница ширин (предгенерируются и кэшируются — берите их):
  `32 · 64 · 128 · 320 · 640 · 1024 · 1600 · 2048 · 2560`. Правило слота: ступень
  ≥ 2× CSS-размера (ретина). Произвольные ширины тоже работают (клэмп 16–4096),
  но режутся на лету — медленнее первого показа.
- Качество: по умолчанию — политика ступеней сервера; переопределение —
  `?q=40…95`.

## 4. Полный пример (bash): товар + фото

```bash
API=https://api.vizen.shop
TOKEN="vz_pat_XXXX"

# 0) паспорт ключа — контур и валюта ДО первой записи
curl -s "$API/v1/account/token" -H "Authorization: Bearer $TOKEN"

# 1) создать товар
PID=$(curl -s -X POST "$API/products" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"item":{"name":"Кровать Аврора","price":{"price":45892,"currency":"RUB"},"category_id":12,"seo":{"slug":"avrora"}}}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')

# 2) попросить ссылку на загрузку
RESP=$(curl -s -X POST "$API/v1/storages/files" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"section":"content","file_type":"image","original_name":"avrora.jpg"}')
FILE_ID=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')  # result.id
UP_ID=$(echo "$RESP"  | python3 -c 'import sys,json;print(json.load(sys.stdin)["upload"]["id"])')   # upload.id
UP_URL=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["upload"]["url"])')

# 3) залить байты и подтвердить (в /done — upload.id!)
curl -s -X PUT "$UP_URL" -H "Content-Type: image/jpeg" --data-binary @avrora.jpg
curl -s -X POST "$API/v1/storages/files/$UP_ID/done" -H "Authorization: Bearer $TOKEN"

# 3.1) финальный URL — ПОСЛЕ /done (оригинал контент-секций конвертируется в .webp)
curl -s "$API/v1/storages/files/$FILE_ID" -H "Authorization: Bearer $TOKEN"

# 4) прицепить фото в галерею (preview НЕ трогаем — миниатюра берётся из галереи)
curl -s -X PUT "$API/products/$PID" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"item\":{\"gallery\":[{\"id\":\"$FILE_ID\"}]}}"

# --- При импорте шаги 2–3.1 схлопываются в один запрос ---
FILE_ID=$(curl -s -X POST "$API/v1/storages/files/from-url" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source_url":"https://cdn.postavshik.ru/foto/avrora.jpg","section":"content","file_type":"image"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')

# ...а товары — пачкой до 100 штук (разбирайте results по index!)
curl -s -X POST "$API/products/batch" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"name":"Стол Лофт","price":{"price":18900,"currency":"RUB"},"category_id":12}]}'
```

## 5. Лимиты и скорость (для импортёров — обязательно)

- **Размер картинки** — до 10 МБ. Превышение отклоняется на шаге `/done`:
  `400 FILE_TOO_LARGE`, файл в медиатеку не попадает. Сжимайте перед загрузкой
  (для веба хватает ~1600–2560 px).
- **Rate-limit по компании** (все ключи и сотрудники магазина делят один
  бюджет), окно — минута:

  | класс | что входит | лимит/мин |
  |---|---|---|
  | чтение | GET / List / Filter / Resolve… | 600 |
  | запись | создание / правка / удаление | 240 |
  | хранилище | мутации `/v1/storages/*` (загрузка файлов) | 180 |

  Превышение → **HTTP 429 `RATE_LIMITED`** с заголовком **`Retry-After`** (секунды
  до сброса окна) — уважайте его: ретраи без паузы только продлевают блокировку.
- **Практика импорта:** лейте файлы последовательно (загрузка одной картинки
  presign-путём = 3–4 запроса класса «хранилище» → ~45–60 картинок в минуту в
  потолке; `from-url` — один запрос); на 429/503 — пауза по `Retry-After` и повтор.
- **Товары** — квота тарифа `products_max`; исчерпание → `PRODUCT_LIMIT_REACHED`.

## 6. Известные ограничения (честно, чтобы не искать зря)

- **Остаток** — число на товаре плюс журнал движений; `NULL` значит «не
  учитывается», а не «ноль». Как читать и менять — `/docs/stock`.
- **Валюта — на уровне магазина**, не товара; `price.currency` не совпала →
  `400 CURRENCY_IS_STORE_LEVEL` (§1.3).
- **`seo` при `PUT` заменяется целиком** — §2.
- **Цена рядом со скидкой считается сервером, а не хранится** — не пересчитывайте
  её у себя, читайте `/docs/promotions` до вывода любой цены.
- `option.value` у характеристик — машинный код: только `[a-z0-9][a-z0-9_-]*`
  (до 64 символов), иначе `400 INVALID_OPTION_VALUE`. Кириллица живёт в `label`.
- Опцию, которая уже проставлена товарам, удалить нельзя — `PUT /attributes/{id}`
  без неё ответит `400 OPTION_IN_USE:<value>` (набор опций только дополняется).
  Ручки «кто использует значение» пока нет.
- **Настройки витрины, члены компании, ключи и вебхуки** ключу закрыты
  (`PAT_METHOD_NOT_ALLOWED`) — это делает человек в кабинете.

## 7. События (вебхуки)

Опрос API — не единственный путь: магазин сам сообщает о происходящем `POST`-ом
на ваш адрес с подписью HMAC-SHA256. Подписку заводит **человек в кабинете**
(«API-доступ» → «Вебхуки»), ключом её создать нельзя; события с персональными
данными и деньгами (`order.*`, `lead.created`, `key.issued`, цены и акции)
подключает только владелец или администратор.

Конверт, заголовки, проверка подписи, гарантии доставки (не менее одного раза,
8 попыток, порядок не гарантирован), полный список событий с полями —
**`/docs/webhooks`**; машинный список событий и параметров доставки —
`/docs/events.json`. Событие — оптимизация, а не источник истины: критичное
сверяйте периодическим диффом по `updated_at` через обычные ручки.

## 8. Как сдавать работу владельцу

- **Ключ-черновик не публикует.** После вашей работы страница живёт в черновой
  версии сайта: на живом домене будет 404, пока владелец не нажмёт
  «Опубликовать». Это нормальный результат, а не ошибка.
- **Самопроверка:** `GET /categories/by-slug/{slug}?company_id={id}` с ключом →
  200, без ключа → 404. Такая пара = всё сделано верно.
- **Ссылки в отчёте** — только полные: `https://{slug}.vizen.shop/{страница}`;
  относительный `/promo` в чате не кликается. Домен магазина — из
  `store.storefront_url` паспорта, не выдумывайте. Ссылок на черновой поддомен
  не давайте — черновик открывается только из кабинета (сессия владельца), по
  прямой ссылке недоступен намеренно. Рядом со ссылкой пишите «заработает после
  публикации».
- **Финальный отчёт — четыре пункта:** что создано (названиями, не id); куда
  смотреть (полные URL); что нажать (кабинет → режим ДЕВ → «Опубликовать»); что
  не вышло — честно. `PAT_METHOD_NOT_ALLOWED` на настройках витрины (логотип,
  цвета, валюта, язык) — норма: попросите владельца сделать это в кабинете.
- По ходу работы: короткие статусы вместо молчания, тексты ошибок API дословно
  (код вроде `SLUG_TAKEN` владелец передаст разработчику), данные — только из
  каталога магазина. **Ключ никуда не печатать** — ни в ответы, ни в файлы, ни
  в разметку.

## 9. Смежные документы

- Наполнение каталога целиком (категории, характеристики, варианты, склейки,
  галерея по группам) — `/docs/catalog-import`.
- Сверстать **страницу** магазина кодом (лендинг, промо, квиз) — `/docs/webcoding`,
  своя вёрстка на данных магазина — `/docs/vz-keys`, `/docs/own-markup`.
- Скидки и цены — `/docs/promotions`, `/docs/pricing`; заказы — `/docs/orders`.
- «Ответило 200, а на сайте ничего не изменилось» — `/docs/troubleshooting`.

---

*Гайд ведёт бэкенд-команда Vizen; обновляется тем же коммитом, что и контракт.
Байт-в-байт копия отдаётся по `GET /docs/quickstart`; расхождение ловит
`TestQuickstartGuideInSync`.*
