Протокол парсера
Парсер торговой сети — это 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), чтобы он сгенерировал парсер.
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 (type ∈ pickup / 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 (type ∈ pickup / 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.
- Сеть — на всю сеть.
GET /promotionsбез параметров. Кэш per-retailer. - Регион — только в одном городе/регионе (для
kind=network).GET /promotions?lat&lon. Live. Не пересекаются с сетевыми. - Точка — в одной физической точке.
OutletLocation.promotionsв/shops. - Товар — на конкретный SKU.
product.promotionsв/products. Не кэшируется.
Лимиты: сеть и регион — 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"
}