{
  "openapi": "3.1.0",
  "info": {
    "title": "Dead Brush Storefront API",
    "version": "1.0.0",
    "description": "Публичный read-oriented REST API магазина Dead Brush (каталог, карточка товара, категории, headless-корзина) для ИИ-агентов и программных клиентов. Ключ/авторизация не требуются, действуют минутные rate-limit'ы (read: 60/мин, изменение корзины: 30/мин на IP).\n\n**Формат URL — критично:** все параметры передаются в PATH (сегментами), НЕ в query-string — сайт-wide правило перезаписи безусловно 301-редиректит любой запрос с `?...` на версию без него, поэтому query-параметры до API не доходят. Все URL заканчиваются слэшем (`/`); версия без слэша получает 301.\n\n**URL-кодирование — критично:** движок НЕ разбирает percent-encoding в пути — любой `%XX`-октет в сегменте ломает разбор URL (страница отдаёт текст «500: Ошибка обработки URL» вместо JSON). НЕ используйте `encodeURIComponent`/`urllib.parse.quote` для сегментов пути. Значения из безопасного набора `[A-Za-z0-9_.-]` (латинские `slug` товара, `path` категории, ASCII-термы поиска) передавайте как есть, без кодирования. Поисковый `term` с кириллицей, пробелами или спецсимволами передавайте ТОЛЬКО через base64url-форму `/api/v1/products/search/b64/{term_b64}/`: `term_b64 = base64url(UTF-8-байты запроса)` без padding (алфавит `[A-Za-z0-9_-]`, RFC 4648 §5; в JS — `btoa` UTF-8-байтов с заменой `+/`→`-_` и обрезкой `=`, в Python — `base64.urlsafe_b64encode(s.encode()).rstrip(b'=')`).\n\n**Оплата:** этот API не создаёт заказы и не проводит оплату (Этап 2, вне текущего контракта) — корзина только собирается программно; оформление и оплату всегда финализирует человек по ссылке в интерфейсе магазина.\n\nБренд: **Dead Brush** (два слова).",
    "contact": {
      "url": "https://www.deadbrush.ru/agent-docs/"
    }
  },
  "servers": [
    { "url": "https://www.deadbrush.ru", "description": "Продакшн" }
  ],
  "paths": {
    "/api/v1/ping/": {
      "get": {
        "operationId": "ping",
        "summary": "Проверка доступности API",
        "description": "Не требует параметров. Используется для health-check перед серией запросов.",
        "responses": {
          "200": {
            "description": "API доступен",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "const": "ok" },
                    "version": { "type": "string", "const": "v1" }
                  },
                  "required": ["status", "version"]
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/": {
      "get": {
        "operationId": "listProducts",
        "summary": "Список товаров (первая страница)",
        "description": "Только опубликованные товары (`goods_in_market=1`). Без параметров — первая страница (0). Для постраничного просмотра или поиска используйте `/api/v1/products/page/{page}/`, `/api/v1/products/search/{term}/` или их комбинацию.",
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/page/{page}/": {
      "get": {
        "operationId": "listProductsPage",
        "summary": "Список товаров, конкретная страница",
        "parameters": [
          { "$ref": "#/components/parameters/PageParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/search/{term}/": {
      "get": {
        "operationId": "searchProducts",
        "summary": "Поиск товаров по подстроке названия",
        "description": "`term` — подстрока для регистронезависимого поиска по названию товара (`goods_name`), ищется по ВСЕМУ каталогу опубликованных товаров (не только текущей странице), `total` в ответе — истинное число совпадений. Эта форма — ТОЛЬКО для термов из безопасного ASCII-набора `[A-Za-z0-9_.-]`, как есть, БЕЗ percent-encoding. Кириллица/пробелы/спецсимволы — только через `/api/v1/products/search/b64/{term_b64}/` (см. `info.description`).",
        "parameters": [
          { "$ref": "#/components/parameters/TermParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/search/{term}/page/{page}/": {
      "get": {
        "operationId": "searchProductsPage",
        "summary": "Поиск товаров, конкретная страница результатов",
        "parameters": [
          { "$ref": "#/components/parameters/TermParam" },
          { "$ref": "#/components/parameters/PageParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/search/b64/{term_b64}/": {
      "get": {
        "operationId": "searchProductsB64",
        "summary": "Поиск товаров, base64url-форма терма (кириллица/пробелы/спецсимволы)",
        "description": "Семантика идентична `/api/v1/products/search/{term}/`, но `term_b64` — base64url(UTF-8-байты запроса) без padding. Единственный способ передать не-ASCII терм: движок не разбирает percent-encoding в пути (см. `info.description`). Некорректный base64url трактуется как пустой запрос (общий список, не ошибка).",
        "parameters": [
          { "$ref": "#/components/parameters/TermB64Param" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/search/b64/{term_b64}/page/{page}/": {
      "get": {
        "operationId": "searchProductsB64Page",
        "summary": "Поиск товаров (base64url-терм), конкретная страница результатов",
        "parameters": [
          { "$ref": "#/components/parameters/TermB64Param" },
          { "$ref": "#/components/parameters/PageParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/ProductList" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/products/{slug}/": {
      "get": {
        "operationId": "getProduct",
        "summary": "Карточка товара",
        "description": "`slug` — `goods_url` товара (латинский идентификатор, БЕЗ percent-encoding). Возвращает 404, если товар не существует или не опубликован (`goods_in_market=0`) — оба случая неразличимы намеренно (нет утечки существования неопубликованных карточек).",
        "parameters": [
          { "$ref": "#/components/parameters/SlugParam" }
        ],
        "responses": {
          "200": {
            "description": "Карточка товара",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ProductDetail" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/categories/{path}/": {
      "get": {
        "operationId": "listCategory",
        "summary": "Товары категории",
        "description": "`path` — 1-3 URL-сегмента иерархии категорий (родитель[/ребёнок[/внук]]), каждый сегмент — `group_url` (латинский идентификатор, БЕЗ percent-encoding; пример пути с тремя уровнями: `filmi-i-seriali/chugoyi/kovriki`). Несуществующий узел дерева категорий → 404. Существующий, но пустой узел → 200 с `items: []`, `total: 0` (не 404 — узел есть, товаров в нём просто нет).",
        "parameters": [
          { "$ref": "#/components/parameters/CategoryPathParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/CategoryList" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/categories/{path}/page/{page}/": {
      "get": {
        "operationId": "listCategoryPage",
        "summary": "Товары категории, конкретная страница",
        "parameters": [
          { "$ref": "#/components/parameters/CategoryPathParam" },
          { "$ref": "#/components/parameters/PageParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/CategoryList" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/carts/": {
      "post": {
        "operationId": "createCart",
        "summary": "Создать корзину",
        "description": "Без тела запроса. Возвращает `cart_token` (64 hex-символа) — bearer-идентификатор корзины. Передавайте его в последующих запросах либо заголовком `X-Cart-Token`, либо PATH-сегментом `{token}`. Токен не привязан к cookie/сессии браузера — это самостоятельный секрет, храните его на стороне клиента.",
        "responses": {
          "200": {
            "description": "Корзина создана",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cart_token": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
                  },
                  "required": ["cart_token"]
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/carts/{token}/": {
      "get": {
        "operationId": "getCart",
        "summary": "Показать содержимое корзины",
        "parameters": [
          { "$ref": "#/components/parameters/CartTokenParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Cart" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/carts/{token}/items/": {
      "post": {
        "operationId": "addCartItem",
        "summary": "Добавить товар в корзину",
        "description": "Тело запроса — JSON. Укажите ЛИБО `slug`, ЛИБО `goods_id` (числовой) для идентификации товара. Добавить можно только опубликованный товар — иначе 400. `size` — короткий алфацифровой токен (напр. `S`, `M`, `XL`, до 8 символов); пустая строка допустима для товаров без размерной сетки.",
        "parameters": [
          { "$ref": "#/components/parameters/CartTokenParam" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AddCartItemRequest" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Cart" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/carts/{token}/items/{goods_id}/": {
      "delete": {
        "operationId": "removeCartItem",
        "summary": "Убрать товар из корзины",
        "description": "Удаляет ВСЕ строки корзины с указанным `goods_id` (все размеры/партии этого товара разом).",
        "parameters": [
          { "$ref": "#/components/parameters/CartTokenParam" },
          { "$ref": "#/components/parameters/GoodsIdParam" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Cart" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "PageParam": {
        "name": "page",
        "in": "path",
        "required": true,
        "description": "Номер страницы результата, 0-based (0 = первая страница).",
        "schema": { "type": "integer", "minimum": 0 }
      },
      "TermParam": {
        "name": "term",
        "in": "path",
        "required": true,
        "description": "Поисковая подстрока из безопасного ASCII-набора, как есть, БЕЗ percent-encoding. Кириллица/пробелы — через b64-форму (TermB64Param).",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_.-]+$" }
      },
      "TermB64Param": {
        "name": "term_b64",
        "in": "path",
        "required": true,
        "description": "base64url(UTF-8-байты поискового запроса) без padding (RFC 4648 §5, алфавит [A-Za-z0-9_-]). Для термов с кириллицей/пробелами/спецсимволами — единственный рабочий транспорт через PATH.",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" }
      },
      "SlugParam": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "goods_url товара — латинский идентификатор ([a-z0-9-]), как есть, БЕЗ percent-encoding.",
        "schema": { "type": "string" }
      },
      "CategoryPathParam": {
        "name": "path",
        "in": "path",
        "required": true,
        "description": "1-3 URL-сегмента дерева категорий через `/`. Сегменты — латинские group_url ([a-z0-9-]), как есть, БЕЗ percent-encoding.",
        "schema": { "type": "string" }
      },
      "CartTokenParam": {
        "name": "token",
        "in": "path",
        "required": true,
        "description": "cart_token, полученный от POST /api/v1/carts/.",
        "schema": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
      },
      "GoodsIdParam": {
        "name": "goods_id",
        "in": "path",
        "required": true,
        "description": "Числовой ID товара (goods_id).",
        "schema": { "type": "integer", "minimum": 1 }
      }
    },
    "responses": {
      "ProductList": {
        "description": "Страница списка/поиска товаров",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "items": { "type": "array", "items": { "$ref": "#/components/schemas/Product" } },
                "page": { "type": "integer" },
                "total": { "type": "integer", "description": "Полное число совпадений по всему каталогу (не только текущей странице)." }
              },
              "required": ["items", "page", "total"]
            }
          }
        }
      },
      "CategoryList": {
        "description": "Товары категории",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "category": {
                  "type": "object",
                  "properties": {
                    "path": { "type": "string" },
                    "title": { "type": "string" }
                  }
                },
                "items": { "type": "array", "items": { "$ref": "#/components/schemas/Product" } },
                "page": { "type": "integer" },
                "total": { "type": "integer" }
              },
              "required": ["category", "items", "page", "total"]
            }
          }
        }
      },
      "Cart": {
        "description": "Текущее содержимое корзины",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                { "$ref": "#/components/schemas/Cart" },
                {
                  "type": "object",
                  "properties": { "cart": { "$ref": "#/components/schemas/Cart" } },
                  "required": ["cart"]
                }
              ]
            }
          }
        }
      },
      "NotFound": {
        "description": "Не найдено",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BadRequest": {
        "description": "Некорректный запрос",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Превышен лимит запросов (read: 60/мин, изменение корзины: 30/мин на IP)",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Product": {
        "type": "object",
        "description": "Краткая карточка товара — форма, общая для списка/поиска/категории.",
        "properties": {
          "slug": { "type": "string", "description": "goods_url — используется в /api/v1/products/{slug}/." },
          "title": { "type": "string" },
          "price": { "type": "integer", "description": "Цена в минимальных единицах валюты currency (рубли, целое число)." },
          "currency": { "type": "string", "const": "RUB" },
          "in_stock": { "type": "boolean" },
          "cover_url": { "type": ["string", "null"], "format": "uri" },
          "category": { "type": ["string", "null"], "description": "URL родительской категории (group_url) либо null." }
        },
        "required": ["slug", "title", "price", "currency", "in_stock", "cover_url", "category"]
      },
      "ProductDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/Product" },
          {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "sizes": { "type": "array", "items": { "type": "string" } },
              "images": { "type": "array", "items": { "type": "string", "format": "uri" } },
              "url": { "type": "string", "format": "uri", "description": "Публичная страница товара на deadbrush.ru." }
            },
            "required": ["description", "sizes", "images", "url"]
          }
        ]
      },
      "CartItem": {
        "type": "object",
        "properties": {
          "goods_id": { "type": "integer" },
          "slug": { "type": ["string", "null"] },
          "title": { "type": ["string", "null"] },
          "size": { "type": ["string", "null"] },
          "qty": { "type": "integer" },
          "price": { "type": "integer" }
        },
        "required": ["goods_id", "qty", "price"]
      },
      "Cart": {
        "type": "object",
        "properties": {
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/CartItem" } },
          "count": { "type": "integer" },
          "total": { "type": "integer" }
        },
        "required": ["items", "count", "total"]
      },
      "AddCartItemRequest": {
        "type": "object",
        "description": "Укажите slug ИЛИ goods_id (взаимозаменяемо, хотя бы одно обязательно).",
        "properties": {
          "slug": { "type": "string", "description": "goods_url товара." },
          "goods_id": { "type": "integer", "description": "Числовой ID товара." },
          "size": { "type": "string", "pattern": "^[A-Za-z0-9]{0,8}$", "description": "Короткий алфацифровой токен размера (S/M/L/XL/...), пусто если товар без размеров." },
          "qty": { "type": "integer", "minimum": 1, "maximum": 99, "default": 1 }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "message": { "type": "string" }
            },
            "required": ["message"]
          }
        },
        "required": ["error"]
      }
    }
  }
}
