<!-- Canonical: https://docs.shopstory.live/docs/guide/custom-commerce-integrations -->

# Интеграция с интернет-магазином

ShopStory связывает эфиры и записи с товарами вашего магазина. Покупатель открывает карточку в плеере, выбирает вариант товара и выполняет действие покупки. Ваш сайт или приложение использует свои API и сессию, а плеер отображает данные и результат.

## Что можно подключить

| Возможность | Как она работает |
| --- | --- |
| Товары через фид | Каталог синхронизируется из согласованной выгрузки |
| Товарные данные через API | Обработчик магазина передаёт данные для карточки и условия покупки |
| Фид вместе с API | Фид описывает товары и варианты, API обновляет условия для покупателя |
| Цвет, размер и другие опции | Карточка объединяет реальные SKU; покупатель выбирает конкретный вариант |
| Актуальная цена и наличие | Магазин возвращает условия выбранного SKU в текущей сессии |
| Покупка внутри плеера | Магазин добавляет выбранный SKU в свою корзину и подтверждает результат |
| Полная карточка, корзина и выбор получения | Открываются интерфейсы магазина с возвратом к видео |
| Эфиры и записи | Используют один товарный контракт; условия покупки проверяются при просмотре |

Подключение обработчиков

Код магазина передаёт плееру товарные данные, условия покупки и результат действия. При подключении фиксируются версия SDK, способ регистрации обработчиков и целевые платформы. На этой странице описан контракт данных; примеры показывают код адаптера магазина.

## 1. Выберите источник товарных данных

### Товарный фид

Передайте выгрузку каталога по HTTPS. ShopStory связывает стабильные ID товаров с эфирами и записями. Названия, изображения, ссылки и переданные цены обновляются при синхронизации. Для простого каталога с едиными ценами и наличием этого достаточно: [Требования к фиду](<https://docs.shopstory.live/docs/guide/feed-requirements>).

Цена и наличие в выгрузке — снимок на момент её обновления. Окончательную проверку при покупке выполняет магазин.

### Товарный API

В расширенном контракте магазин может предоставлять данные карточки через свой обработчик: описание, изображения, URL, структуру вариантов и актуальные условия. Обработчик сопоставляет товарные ID ShopStory с каталогом магазина и возвращает нормализованные данные.

Такой способ подходит для персональных цен, нескольких рынков или каталога, который уже доступен через API. Источником структуры вариантов может быть API: передача мастер-фида для этого режима не обязательна. API должен возвращать полный список реальных SKU и опций для запрошенного товара, включая временно отсутствующие варианты.

Здесь товарный API — API вашего магазина. Это не новый endpoint ShopStory Public API и не публичный доступ к внутреннему каталогу магазина.

### Фид и API вместе

Фид передаёт стабильное описание и структуру вариантов. API возвращает цену, наличие и доступное действие для конкретного покупателя. Это позволяет обновлять условия без новой выгрузки всего каталога.

Выберите один источник структуры товара: фид или API. Ответы с условиями покупателя обновляют его карточку и не меняют общий каталог. Правила отсутствующих товаров и неполного обновления фиксируются до подключения; ошибка источника не должна незаметно удалять существующие варианты.

Источники данных карточки · расширенный контракт

```mermaid
---
config:
  theme: base
  fontFamily: Euclid Circular B, sans-serif
  themeVariables:
    primaryColor: '#f8f8fb'
    primaryTextColor: '#242329'
    primaryBorderColor: '#d8d7e0'
    lineColor: '#5c3dff'
---
flowchart TB
    accTitle: Откуда карточка получает описание и условия покупки
    accDescr: Описание товара и реальные SKU поступают из одного выбранного источника: фида или товарного API магазина. При подключении актуальных условий обработчик сайта или приложения запрашивает API магазина в сессии покупателя. Плеер объединяет описание и условия по ID товара и SKU. Товарный API может передавать и описание, и условия.
    source["Источник описания: фид ИЛИ товарный API"]
    product["Описание товара<br/>ID, название, изображения, URL, реальные SKU и опции"]
    api["API магазина<br/>Сессия и условия покупателя"]
    adapter["Обработчик сайта или приложения<br/>Цена, наличие, действие по каждому SKU"]
    card["Карточка в плеере ShopStory<br/>Выбор SKU и отображение условий"]
    source --> product
    api --> adapter
    product --> card
    adapter --> card
```

Описание и условия сопоставляются по ID товара и SKU. В режиме фида с едиными условиями отдельный API для карточки не нужен. Персональные ответы обновляют карточку текущего покупателя.

## 2. Опишите товары и варианты

| Данные | Назначение |
| --- | --- |
| ID товара | Связывает карточку с товаром магазина |
| Название, изображения и URL | Содержимое карточки и переход на полную страницу |
| ID каждого SKU | Однозначно определяет продаваемый вариант |
| Связь SKU с товаром | Объединяет варианты в одной карточке |
| Опции SKU | Цвет, размер и другие значения для выбора |

Передавайте только существующие сочетания опций. Например, «синий, размер M» и «синий, размер L» — два отдельных SKU со своими ценой и наличием. Недоступность одного размера не означает недоступность всех синих вариантов.

Все ID передаются строками с сохранением ведущих нулей и регистра. В расширенном контракте `productId` соответствует согласованному `feedProductId`, а `variantId` — идентификатору SKU; для товара без вариантов `variantId=null`. Выбранные опции должны однозначно указывать на один SKU. Магазин проверяет именно этот SKU при добавлении.

Список вариантов поступает из выбранного товарного источника. Плеер не создаёт сочетания из отдельных списков цветов и размеров. Временно отсутствующий SKU остаётся в структуре; его состояние определяется актуальными условиями.

Пример нормализованных товарных данных для API-режима:

product-data.json

```json
{
  "productId": "0001001",
  "name": "Футболка",
  "images": ["https://example.ru/images/0001001.webp"],
  "url": "https://example.ru/products/0001001",
  "variants": [
    {
      "variantId": "0001001-blue-M",
      "options": { "Цвет": "Синий", "Размер": "M" }
    },
    {
      "variantId": "0001001-blue-L",
      "options": { "Цвет": "Синий", "Размер": "L" }
    }
  ]
}
```

Это данные обработчика магазина, а не запрос к ShopStory Public API. Для товара без вариантов `variants` — пустой массив. В режиме фида эти же связи извлекаются из согласованного маппинга выгрузки.

## 3. Передайте актуальные условия покупки

Обработчик `getProductConditions(items)` получает пакет пар `productId` + `variantId`. Магазин запрашивает свои API в сессии покупателя и возвращает условия по каждой паре. Порядок ответа может отличаться от запроса: сопоставление выполняется по обоим ID.

| Поле | Значение для карточки |
| --- | --- |
| `productId`, `variantId` | Товар и SKU из запроса |
| `price` | Сумма и валюта; `null`, если цена ещё не определена |
| `priceStatus` | `provisional` — предварительная, `confirmed` — подтверждённая, `unknown` — не определена |
| `available` | Наличие конкретного SKU для текущего покупателя |
| `showProduct` | Показывать ли общую карточку; одинаково для её SKU |
| `fulfilmentLabel` | Подпись выбранного способа получения |
| `inCart` | Есть ли выбранный SKU в текущей корзине |
| `action` | Разрешённое следующее действие |
| `conditionsRef` | Непрозрачная ссылка на условия, проверяемая магазином при добавлении |

Магазин определяет действие: `addDirectly` — добавить SKU, `chooseFulfilment` — выбрать получение, `openMerchantFlow` — открыть интерфейс магазина, `disabled` — покупка недоступна. Цена, наличие и действие передаются отдельно: наличие само по себе не разрешает покупку.

Предварительная цена получает явную подпись. При `price=null` карточка объясняет, когда цена станет известна. Цена одного SKU не подменяет цену другого.

Все перечисленные поля обязательны в ответе; для допускающих отсутствие значения полей передавайте `null`. Поле `price` содержит `amount` — десятичную сумму строкой, и `currency` — код валюты, например `{"amount":"1299.00","currency":"RUB"}`. При `price=null` статус равен `unknown`. `available`, `showProduct` и `inCart` — boolean-значения подтверждённого ответа; неизвестный результат передаётся пропуском пары ID. `fulfilmentLabel` — строка или `null`, если способ получения ещё не выбран. `conditionsRef` — строка для разрешённого прямого добавления; когда добавление ещё недоступно, значение может быть `null`.

`showProduct` должен совпадать для всех SKU одной карточки. При противоречивом ответе магазин повторно проверяет условия этого товара; покупка недоступна до согласованного ответа. Для товара без вариантов в запрос и ответ передаётся `variantId=null`.

Пропущенный ответ означает неизвестное состояние SKU. Описание сохраняется, покупка этого варианта недоступна до успешной проверки. Ошибка API не превращается в «Нет в наличии». Код магазина привязывает чтение к текущей ревизии сессии, магазина, получения и выбранного SKU. При изменении этого контекста ревизия меняется, условия обновляются, а запоздавший ответ прежнего чтения отбрасывается. Сессионные данные для такого сравнения остаются в коде магазина.

[Обработчик наличия](<https://docs.shopstory.live/docs/guide/product-availability>) возвращает только boolean-карту и не меняет цену или варианты. Для актуальной цены, опций и разрешённого действия используйте `getProductConditions`; этот контракт подключается отдельно от boolean-проверки наличия.

## 4. Подключите корзину

Обработчик `startCommerceAction(request, events)` получает `productId`, `variantId`, `action`, `conditionsRef` и `operationId`. Магазин выполняет разрешённое действие своим API-клиентом.

При добавлении commerce API проверяет выбранный SKU и актуальные условия при записи. `conditionsRef` связывает запрос с условиями для этого SKU и контекста; сервер магазина проверяет срок действия и отклоняет изменённые или устаревшие условия. Подтверждение `added` содержит те же ID товара, варианта и операции. Только подтверждённый результат переводит карточку в «В корзине». HTTP `200` и наличие SKU в фиде этого не доказывают.

Если требуется форма магазина, отдельно передаются её открытие, закрытие и изменение условий. Закрытие формы и выбор способа получения не считаются добавлением. После получения новых условий покупатель выполняет действие покупки.

Подтверждённое добавление · расширенный контракт

```mermaid
---
config:
  theme: base
  fontFamily: Euclid Circular B, sans-serif
  themeVariables:
    actorBkg: '#f8f8fb'
    actorBorder: '#d8d7e0'
    actorTextColor: '#242329'
    actorLineColor: '#d8d7e0'
    signalColor: '#5c3dff'
    signalTextColor: '#4d4b55'
    noteBkgColor: '#f8f8fb'
    noteBorderColor: '#e7e6ec'
    noteTextColor: '#4d4b55'
  sequence:
    mirrorActors: false
    wrap: true
    actorMargin: 24
    width: 150
    messageMargin: 28
---
sequenceDiagram
    accTitle: Как плеер получает подтверждение добавления SKU
    accDescr: Плеер запрашивает условия выбранного SKU через код магазина. Покупатель нажимает добавить. Обработчик передаёт SKU, ссылку на условия и идентификатор операции серверу магазина. Сервер проверяет права и условия, изменяет корзину и возвращает подтверждение. Только совпадающее подтверждение позволяет показать В корзине. Открытие и закрытие формы не подтверждают добавление.
    participant player as Плеер ShopStory
    participant adapter as Код сайта / приложения
    participant api as API магазина
    player->>adapter: Условия выбранного SKU
    adapter->>api: Чтение в сессии покупателя
    api-->>adapter: Цена, наличие, действие, conditionsRef
    adapter-->>player: Условия для productId + variantId
    Note over player,adapter: Покупатель нажимает «Добавить»
    player->>adapter: SKU + operationId + conditionsRef
    adapter->>api: Проверка условий и добавление
    api-->>adapter: Подтверждение с теми же ID
    adapter-->>player: added с совпадающими ID
    Note over player: Можно показать «В корзине»
    Note over player,api: Открытие и закрытие формы — отдельные сигналы
```

Показан успешный путь прямого добавления. Корзину изменяет магазин. При отказе или потере ответа подтверждение не отправляется; правила проверки неизвестного результата описаны ниже.

### Сбой связи и безопасный повтор

В расширенном контракте плеер создаёт уникальный `operationId` при первом действии добавления и передаёт его обработчику. Код магазина сохраняет полученный ID до окончательного ответа. Сервер связывает ID с исходной корзиной, сессией и неизменным запросом. Повтор с тем же ID и другим SKU или условиями отклоняется.

Потерянный ответ означает неизвестный исход операции. Автоматического повтора по умолчанию нет. Он допустим только при подтверждённой защите API магазина от дублей: неизменный запрос с тем же `operationId` возвращает исходный результат без второй записи.

Магазин связывает операцию с исходной корзиной и сессией и проверяет права при каждом обращении. `operationId` и `conditionsRef` не заменяют авторизацию.

До окончательного ответа код магазина блокирует новое добавление того же SKU в исходную корзину. Открытие корзины, смена варианта и возврат к видео не снимают блокировку. Наличие товара в корзине не доказывает исход конкретной операции. API магазина должен позволять получить окончательный результат без нового добавления; способ проверки, интервал и предел ожидания, срок хранения результата и восстановление после потери сессии фиксируются до запуска.

Покупатель видит «Проверяем добавление» и может открыть корзину магазина. По истечении согласованного времени ожидания показывается «Не удалось проверить добавление». Просмотр видео и переход к корзине остаются доступны; тайм-аут не разрешает повторную запись. Блокировка снимается после подтверждённого добавления или окончательного отказа, который гарантирует отсутствие записи. Для новой корзины проверяется её собственное состояние.

## 5. Подготовьте обработчики магазина

Для текущего Web SDK используйте опубликованные [callback корзины](<https://docs.shopstory.live/docs/guide/add-to-cart#web-sdk-callback>) и [обработчик наличия](<https://docs.shopstory.live/docs/guide/product-availability#%D0%BA%D0%BE%D0%BD%D1%82%D1%80%D0%B0%D0%BA%D1%82-%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%87%D0%B8%D0%BA%D0%B0>).

Для расширенного контракта подключите чтение условий и выполнение действия. Ниже показана логика адаптера; `store` — существующий API-клиент вашего магазина. Регистрация обработчиков выполняется по инструкции для подключаемой версии SDK.

При подключении проверьте `getProductConditions` и `startCommerceAction` в списке обработчиков этой инструкции. Используйте указанный в ней способ регистрации; имена параметров и вызовы инициализации по примеру адаптера не определяются.

commerce-adapter.js

```javascript
function isProductPair(value) {
  return value !== null && typeof value === 'object' &&
    typeof value.productId === 'string' && value.productId.length > 0 &&
    (value.variantId === null ||
      (typeof value.variantId === 'string' && value.variantId.length > 0));
}

const commerceAdapter = {
  async getProductConditions(items) {
    if (!Array.isArray(items) || !items.every(isProductPair)) {
      throw new Error('Некорректный список товаров');
    }
    const conditions = await store.readProductConditions(items);
    if (!Array.isArray(conditions)) throw new Error('Некорректный ответ магазина');
    const requested = new Set(items.map(item => JSON.stringify([item.productId, item.variantId])));
    const visibility = new Map();
    return conditions.map(({ productId, variantId, price, priceStatus,
      available, showProduct, fulfilmentLabel, inCart, action, conditionsRef }) => {
      const key = JSON.stringify([productId, variantId]);
      const validPrice = price !== null && typeof price === 'object' &&
        typeof price.amount === 'string' && /^\d+(?:\.\d+)?$/.test(price.amount) &&
        typeof price.currency === 'string' && /^[A-Z]{3}$/.test(price.currency);
      if (typeof productId !== 'string' || !productId ||
          (variantId !== null && (typeof variantId !== 'string' || !variantId)) ||
          !requested.has(key) ||
          !['provisional', 'confirmed', 'unknown'].includes(priceStatus) ||
          (price === null ? priceStatus !== 'unknown' : !validPrice || priceStatus === 'unknown') ||
          [available, showProduct, inCart].some(value => typeof value !== 'boolean') ||
          (fulfilmentLabel !== null && typeof fulfilmentLabel !== 'string') ||
          (conditionsRef !== null && typeof conditionsRef !== 'string') ||
          !['addDirectly', 'chooseFulfilment', 'openMerchantFlow', 'disabled'].includes(action) ||
          (action === 'addDirectly' && !conditionsRef)) {
        throw new Error('Некорректные условия товара');
      }
      if (visibility.has(productId) && visibility.get(productId) !== showProduct) {
        throw new Error('Противоречивые условия карточки');
      }
      requested.delete(key);
      visibility.set(productId, showProduct);
      return {
        productId, variantId,
        price: price === null ? null : { amount: price.amount, currency: price.currency },
        priceStatus, available, showProduct, fulfilmentLabel, inCart, action, conditionsRef,
      };
    });
  },

  async startCommerceAction(request, events) {
    // Для других action код магазина подключает формы и навигацию по разделам 4 и 6.
    if (request?.action !== 'addDirectly') {
      throw new Error('Этот пример обрабатывает прямое добавление');
    }
    if (!isProductPair(request) ||
        typeof request.operationId !== 'string' || !request.operationId ||
        typeof request.conditionsRef !== 'string' || !request.conditionsRef ||
        !events || typeof events.added !== 'function') {
      throw new Error('Некорректное действие покупки');
    }

    // Код магазина удерживает блокировку SKU и проверяет исход при сбое связи.
    // Исключение из этого примера само по себе не разрешает повторное добавление.
    const result = await store.addSelectedSku(request);
    if (!result || typeof result !== 'object' || result.added !== true ||
        result.productId !== request.productId ||
        result.variantId !== request.variantId ||
        result.operationId !== request.operationId) {
      throw new Error('Добавление не подтверждено');
    }
    events.added({
      productId: result.productId,
      variantId: result.variantId,
      operationId: result.operationId,
    });
  },
};
```

Метод чтения проверяет обязательные поля, соответствие запрошенным ID, отсутствие дублей и согласованность `showProduct`. Некорректный ответ отклоняется; код магазина повторно проверяет условия, сохраняя покупку недоступной. Метод возвращает только нормализованные поля раздела 3; дополнительные поля API магазина отбрасываются, в том числе внутри `price`. Метод добавления передаёт неизменные ID и условия серверу магазина и возвращает подтверждённый результат. В событие для плеера включаются только идентификаторы подтверждённой операции; полный ответ API магазина не передаётся. Пример показывает прямое добавление и проверку идентичности ответа. Его вызывают после проверки `action=addDirectly` и установки блокировки в коде магазина. Формы, отказы и неизвестный результат обрабатываются по правилам раздела 4.

Для API как источника описания товара дополнительно подготовьте чтение полного товара и его SKU по ID из раздела 2. Для режима «фид + API» повторно передавать описание при каждом обновлении условий не требуется.

## 6. Настройте переходы и возврат

Полная карточка, корзина и формы открываются через навигацию магазина. Сигналы `screenOpened`, `screenClosed` и `conditionsChanged` отделены от подтверждения покупки. После возврата плеер сохраняет выбранный товар и SKU и обновляет условия: покупатель мог изменить корзину вне плеера.

Запись сохраняет позицию просмотра; эфир возвращается к текущему моменту. При полном перекрытии плеера или уходе приложения в фон видео и звук приостанавливаются. Возврат, звук и сохранение выбора проверяются на целевых платформах.

На web обработчик работает в коде сайта. В приложении его выполняет код приложения с существующими API-клиентом и авторизацией; экраны магазина открываются штатной навигацией. Формат подключения и целевые платформы фиксируются для используемой версии [Mobile SDK](<https://docs.shopstory.live/docs/guide/mobile-sdk>).

## Безопасность и ответственность

| ShopStory | Интернет-магазин |
| --- | --- |
| Плеер, карточки и выбор SKU | Каталог, SKU и торговые правила |
| Запрос и отображение условий | Цена и наличие для текущей сессии |
| Передача действия и показ результата | Проверка условий, изменение корзины и защита от дублей |
| Состояние просмотра и обработка возврата | Авторизация, формы, навигация и оплата |

Результат подтверждает система, которая владеет корзиной и остатками. Cookies, токены, адреса покупателя и другие персональные данные в ответ для плеера не включаются. Серверные ключи остаются на сервере магазина.

Для browser-интеграции используйте API, доступное с вашей страницы, либо серверный посредник магазина с его проверками доступа. Прямой доступ серверов ShopStory к торговому API для обработчиков не требуется. Commerce-обработчик не является стандартным endpoint Public API.

Клиентские URL, учётные данные, внутренние схемы API и договорные ограничения входят в закрытое техническое приложение. Публичный контракт описывает только данные карточки, действия и сигналы.

## Проверка перед запуском

- Товарные ID совпадают между источником данных, плеером и корзиной. Выбранный цвет и размер приводят к добавлению нужного SKU.
- Цена, наличие и действие соответствуют текущей сессии. Пропущенный ответ отличается от подтверждённой недоступности.
- Выбор получения, отмена формы и отказ API не показывают добавление.
- Поздний ответ прежнего контекста не меняет карточку. После возврата условия обновляются.
- Потеря ответа и допустимый повтор не увеличивают количество повторно; неизвестный результат сохраняет блокировку.
- Эфир, запись, переходы, звук и фон проверены с реальными API и устройствами.

Повторяйте затронутые проверки после изменений API магазина, обработчиков, SDK или навигации.
