Протокол парсера

Парсер торговой сети — это HTTP-сервис, реализующий несколько GET-эндпоинтов. Все ответы — JSON. shopmcp обращается к парсеру с заголовком Authorization: Bearer <parser_token>, если в настройках retailer'а указан токен.

⚠ Outlet — единая модель точки получения товара

shopmcp не различает «магазин» и «доставку» — для него всё это разные outlet'ы. У outlet'а есть kind:

  • store — физическая точка, цены per-store (Лента, Магнит). Capabilities на каждой OutletLocation: walkin/pickup.
  • network — сеть с общим каталогом, цена единая в регионе (Эльдорадо, Леруа). Несколько физических точек отдаются как один outlet с массивом locations[].
  • delivery — курьерская доставка с фиксированной зоной.
  • marketplace — маркетплейс (Ozon/WB), варианты получения per-товар в product.delivery[].

Один SKU может быть в нескольких outlet'ах сети с разными ценами/сроками. Магнит: store + быстрый delivery (99 ₽, 1-2 ч) + медленный delivery (бесплатно от 1500 ₽, следующий день).

Версия для LLM. Та же спецификация в одном Markdown-файле — можно скормить ассистенту (Claude/Cursor/ChatGPT), чтобы он сгенерировал парсер.

Скачать parser-protocol.md Открыть в браузере

1. Эндпоинты

GET /shops

Статичный полный список физических точек сети (kind=store или kind=network). Без параметров. shopmcp кэширует на 24 часа, индексирует каждую OutletLocation в R*-tree, потом фильтрует под area запроса. Адреса без координат геокодит через Yandex.

Per-store сеть (kind=store) — каждый магазин отдельный outlet:

[
  {
    "id": "msk-tverskaya-1",
    "kind": "store",
    "name": "Магнит — Тверская",
    "locations": [
      {
        "address": "Москва, Тверская 1",
        "lat": 55.7589, "lon": 37.6109,
        "hours": "08:00–22:00",
        "walkin": true,
        "pickup": true,
        "promotions": [ "При чеке от 2000 ₽ — скидка 10%" ]
      }
    ]
  }
]

Network-сеть (kind=network) — один outlet, единая цена в регионе, массив физических точек:

[
  {
    "id": "eldorado",
    "kind": "network",
    "name": "Эльдорадо",
    "description": "Единая цена в каталоге. Магазины для самовывоза и проверки наличия.",
    "locations": [
      { "id": "msk-marosey", "address": "Москва, Маросейка 9",      "lat": 55.7575, "lon": 37.6364, "walkin": true, "pickup": true },
      { "id": "msk-nikitsk", "address": "Москва, Б. Никитская 14",  "lat": 55.7561, "lon": 37.6043, "walkin": false, "pickup": true,
        "promotions": [ "−20% сегодня только в этой точке" ]
      },
      { "id": "spb-nevsky",  "address": "СПб, Невский 30",          "lat": 59.9343, "lon": 30.3351, "walkin": true, "pickup": true }
    ]
  }
]

Для kind=network: парсер отдаёт все locations по всем регионам, shopmcp фильтрует по area. OutletLocation.id обязателен (для связки с Product.stock_by_location). shopmcp подменяет payload target'а на координаты пользователя — парсер при /products?outlet=<lat,lon> сам определяет регион.

GET /delivery?lat=...&lon=...

Live, не кэшируется. Варианты получения для конкретной точки доставки. kind=delivery — курьерская доставка с фиксированной ценой/зоной; kind=marketplace — маркетплейс, варианты получения per-товар. Если у сети нет доставки или координаты вне зоны — отдайте [] или 404.

[
  {
    "id": "fast",
    "kind": "delivery",
    "name": "Магнит у дома (курьер)",
    "description": "Курьер 1-2 часа",
    "delivery_fee": "99.00",
    "min_order": "300",
    "fee_note": "99 ₽ при заказе от 300 ₽",
    "min_days": 0, "max_days": 1
  },
  {
    "id": "slow",
    "kind": "delivery",
    "name": "Магнит Экспресс (распред-центр)",
    "delivery_tiers": [
      { "price": "199", "min_order": "1000" },
      { "price": "109", "min_order": "2599" },
      { "price": "0",   "min_order": "4099" }
    ],
    "min_days": 1, "max_days": 2
  },
  {
    "id": "ozon-msk@55.755,37.617",
    "kind": "marketplace",
    "name": "Доставка Ozon в Москве",
    "description": "Варианты доставки зависят от товара"
  }
]

id в обоих эндпоинтах — opaque payload, что-угодно нужное парсеру (для дарксторов с зонной доставкой или marketplace внутри может быть закодирована точка пользователя). shopmcp передаёт этот id обратно в ?outlet=<id> при /products//products/{id}.

Для kind=marketplace: парсер при /products?outlet=... возвращает товары + delivery[] на каждый SKU (typepickup / partner_pickup / courier; поля name, delivery_fee, service_fee, min_days, max_days, description).

GET /promotions опционально, сетевые

Без параметров — акции, действующие на всю сеть целиком. Если не реализовано — 404.

[
  { "id": "dobrii", "title": "...", "description": "...", "image": "..." },
  "Двойные баллы по средам"
]

GET /promotions?lat=...&lon=... опционально, региональные

Только для kind=network-ретейлеров. Live, не кэшируется. Парсер по координатам определяет регион и возвращает акции, действующие только в этом регионе. [] или 404 если нет.

[
  { "title": "Весенняя распродажа техники в Москве", "description": "−15% на крупную БТ. До 31 мая." },
  "Скидка 10% на товары для дома — только в Петербурге"
]

⚠ Региональные не должны пересекаться с сетевыми: если акция везде — в /promotions; если только в одном регионе — в /promotions?lat&lon. Иначе LLM покажет дубль.

Акция — строка или объект (title обязателен). Лимиты: сеть — 5, регион — 5, точка (OutletLocation.promotions) — 3, товар — 3.

GET /products?outlet=<id>&q=...

Листинг товаров в контексте outlet'а. Цена/наличие/акции — специфичны для этого контекста. Live, не кэшируется.

Если outlet недоступен/устарел — 404 outlet_unavailable.

[
  {
    "id": "sku-12345",
    "title": "Молоко Простоквашино 3.2% 930 мл",
    "price": "89.90",
    "in_stock": true,
    "brand": "Простоквашино",
    "promotions": [ { "id": "milk-2plus1", "title": "2+1", "description": "Третий бесплатно" } ]
  }
]

Для kind=network-outlet'а каждый товар содержит stock_by_location[] — остатки/сроки поставки per-магазин. Связка с OutletLocation.id из /shops. Если товара нет, но привезут под заказ — заполнены min_days/max_days (часто разные у разных магазинов: где склад дальше — срок дольше).

{
  "id": "fridge-bosch-kgn",
  "title": "Холодильник Bosch KGN39",
  "price": "89990",
  "in_stock": true,
  "stock_by_location": [
    { "location": "msk-marosey", "in_stock": true,  "stock_qty": "2" },
    { "location": "msk-nikitsk", "in_stock": false, "min_days": 3, "max_days": 5 },
    { "location": "spb-nevsky",  "in_stock": true,  "stock_qty": "5" }
  ]
}

Для kind=marketplace-outlet'а каждый товар содержит delivery[] — варианты получения per-SKU (typepickup / partner_pickup / courier; поля name, delivery_fee, service_fee, min_days, max_days, description).

GET /products/{id}?outlet=<id>

Полная карточка одного товара. Те же поля что в листинге, плюс обычно description, images, categories (хлебные крошки), attributes (характеристики name/value).

2. Четыре уровня акций

Четыре отдельных справочника. Не путайте — иначе акция либо размножится в ответе, либо не дойдёт до LLM.

Лимиты: сеть и регион — 5, точка — 3, товар — 3. Парсер обязан отдавать только актуальные действующие акции, отсортированные по важности.

3. Self-registration

POST /api/v1/retailers/<id>/endpoint
Authorization: Bearer <ваш shp_… ключ из /account/api-key>

{
  "base_url": "https://parser.example.com",
  "parser_token": "опциональный Bearer"
}