> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Пользовательская интеграция

> Вызывайте Strategies API напрямую для создания пользовательских решений на витрине — карусели на страницах товаров, кастомные апселлы в корзине и любые другие места, недоступные нативным поверхностям Aftersell.

<div id="overview">
  ## Обзор
</div>

Когда ни нативные поверхности Aftersell (post-purchase, оформление заказа, Upcart), ни готовая интеграция не подходят, вы можете самостоятельно вызывать Strategies API из вашей темы Shopify и отображать возвращенные товары так, как вам нужно.

Схема одинакова во всех случаях: сформируйте полезную нагрузку контекста в Liquid (чтобы атрибуты Shopify, такие как текущий товар, содержимое корзины и поля покупателя, заполнялись во время рендеринга), отправьте ее методом `POST` на `/api/public/strategy/evaluate` и отобразите ответ.

На этой странице рассмотрены две схемы реализации:

* **Контекст PDP** — добавьте секцию на страницы товаров, которая вызывает API с **просматриваемым в данный момент товаром** и отображает карусель возвращенных рекомендаций.
* **Контекст корзины** — отобразите блок апселла внутри кастомной корзины, который вызывает API со **всеми текущими позициями корзины** и отображает возвращенные товары.

Различие между ними — в форме **контекста товаров**: один товар на PDP, массив всех позиций в корзине.

***

<div id="what-youll-need">
  ## Что вам понадобится
</div>

1. **Ваш API-ключ Strategy.** В Aftersell перейдите в **Settings → Product Strategy** и в карточке **Security Token** скопируйте ваш токен (это и есть ваш API-ключ Strategy).
2. **Идентификатор Strategy.** Откройте нужную Strategy в редакторе Strategy Aftersell и скопируйте ее идентификатор.
3. **Доступ к коду темы.** Вы будете добавлять секцию Liquid (PDP) или блок (кастомная корзина) в вашу тему Shopify — Online Store → Themes → ... → Edit code.

<Warning>
  Ваш API-ключ Strategy находится в клиентском коде темы, что делает его видимым для любого, кто просматривает исходный код страницы. Обращайтесь с ним как с публичными учетными данными витрины и обновите его в Aftersell в разделе **Settings → Product Strategy**, если он когда-либо окажется раскрыт нежелательным образом.
</Warning>

***

<div id="pdp-context-section-snippet">
  ## Контекст PDP: сниппет секции
</div>

Эта схема добавляет секцию Shopify на страницу товара. При рендеринге страницы Liquid встраивает атрибуты текущего товара, корзины и покупателя в полезную нагрузку, затем JavaScript отправляет запрос в Strategies API и отображает возвращенные товары в карусели Splide.

<div id="installing">
  ### Установка
</div>

1. В админ-панели Shopify перейдите в **Online Store → Themes**, нажмите **...** на вашей теме и выберите **Edit code**.
2. В папке **Sections** создайте новый файл с именем `aftersell-upsell-carousel.liquid`.
3. Вставьте приведенный ниже сниппет в новый файл и замените `YOUR_STRATEGY_API_KEY` на API-ключ из Aftersell.
4. Сохраните.
5. Откройте шаблон товара (обычно `templates/product.json` или `sections/main-product.liquid`) и добавьте секцию **Aftersell Carousel** там, где должна появиться карусель. В редакторе темы вы также можете перетащить ее прямо на страницу товара.
6. В настройках секции вставьте ваш **идентификатор Strategy**.

<div id="what-the-section-sends">
  ### Что отправляет секция
</div>

Для каждого просмотра PDP полезная нагрузка включает:

* **products** — массив из одного элемента, содержащий просматриваемый в данный момент товар (productId, variantId, quantity, price, handle, title, vendor, productType, tags, collections, sellingPlan).
* **cart** — промежуточная сумма, количество товаров и количество позиций текущей корзины покупателя (опускается, если корзина пуста).
* **cartToken** — чтобы API мог связать это вычисление с той же сессией.
* **customer** — теги, страна, регион, локаль, количество заказов, общая сумма покупок и флаг согласия на маркетинг, но **только если покупатель авторизован**.
* **session** — код валюты из `shop.currency`.

Секция по умолчанию не отправляет UTM-параметры. Если вы хотите использовать таргетинг на основе UTM на PDP, зафиксируйте их на стороне клиента и добавьте в объект `session` перед вызовом fetch.

<div id="the-snippet">
  ### Сниппет
</div>

```liquid theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{% comment %}
  Aftersell Carousel (Splide)
  Type: Section — save to sections/aftersell-upsell-carousel.liquid
{% endcomment %}

{% if product %}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide-core.min.css">
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js" defer></script>

<div id="aftersell-upsell-{{ section.id }}" class="aftersell-upsell-carousel" style="display:none" {{ section.shopify_attributes }}>
  <h2 class="aftersell-heading">{{ section.settings.heading | default: 'You might also like' }}</h2>
  <div class="aftersell-carousel-wrapper">
    <button class="aftersell-arrow aftersell-arrow--prev" aria-label="Previous" disabled>&#8592;</button>
    <div class="aftersell-track-container">
      <div class="splide" id="aftersell-splide-{{ section.id }}">
        <div class="splide__track">
          <ul class="splide__list">
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
          </ul>
        </div>
      </div>
    </div>
    <button class="aftersell-arrow aftersell-arrow--next" aria-label="Next" disabled>&#8594;</button>
  </div>
</div>

<style>
.aftersell-upsell-carousel { font-family: Modernist, sans-serif; max-width: 1300px; margin: 0 auto; padding: 24px 0 0; box-sizing: border-box; }
.aftersell-heading { font-family: Modernist, sans-serif; font-size: 30px; font-weight: 700; line-height: 45px; color: #0C0A09; margin: 0; }
.aftersell-carousel-wrapper { display: flex; align-items: center; gap: 8px; }
.aftersell-track-container { overflow: hidden; flex: 1; min-width: 0; }
.aftersell-card { display: flex; flex-direction: column; box-sizing: border-box; height: 100%; }
.aftersell-card-link { text-decoration: none; color: inherit; display: block; flex: 1; }
.aftersell-card-image { aspect-ratio: 1; overflow: hidden; background: #f5f5f5; border-radius: 8px 8px 0 0; position: relative; }
.aftersell-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; }
.aftersell-card-image:hover img { transform: scale(1.04); }
.aftersell-card-badge { position: absolute; top: 10px; left: 8px; background: #c60006; color: #fff; font-size: 11px; font-weight: 500; padding: 4px 8px; border-radius: 16px; z-index: 1; }
.aftersell-card-body { padding: 6px 0 0; display: flex; flex-direction: column; }
.aftersell-card-vendor { font-size: 13px; font-weight: 550; text-transform: uppercase; color: #1d4481; margin: 0; }
.aftersell-card-title { font-size: 13px; font-weight: 700; color: #0C0A09; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aftersell-card-price { font-size: 13px; color: #0C0A09; margin: 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.aftersell-card-price--compare { text-decoration: line-through; color: #595959; }
.aftersell-card-price--sale { font-weight: 700; color: #c60006; }
.aftersell-card-actions { padding: 6px 0 0; display: flex; flex-direction: column; margin-top: auto; }
.aftersell-variant-select { width: 100%; font-size: 13px; padding: 0.35rem 0.5rem; border: 1px solid #DBDBDB; border-radius: 4px; background: #fff; color: #0C0A09; cursor: pointer; }
.aftersell-cta { padding: 16px 12px; background: #c50007; color: #FFFFFF; border: none; border-radius: 5px; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; transition: background 0.2s, opacity 0.2s; }
.aftersell-cta:hover:not(:disabled) { opacity: 0.85; }
.aftersell-cta:disabled { opacity: 0.5; cursor: default; }
.aftersell-cta--added { background: #1d4481; }
.aftersell-cta--unavailable { background: #999; cursor: default; }
.aftersell-arrow { width: 30px; height: 90px; background: rgba(255,255,255,0.6); border: 1px solid #DBDBDB; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; }
.aftersell-arrow:disabled { opacity: 0.3; cursor: default; }
.aftersell-skeleton { border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: aftersell-shimmer 1.4s infinite; aspect-ratio: 0.75; width: 100%; }
@keyframes aftersell-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

#aftersell-splide-{{ section.id }} .splide__arrows,
#aftersell-splide-{{ section.id }} .splide__pagination { display: none !important; }

@media (max-width: 768px) {
  .aftersell-upsell-carousel { padding: 16px 16px 0; }
  .aftersell-arrow { display: none; }
  .aftersell-carousel-wrapper { gap: 0; }
}
</style>

<script>
(function() {
  var BACKEND_URL  = 'https://start.aftersell.app';
  var API_KEY      = 'YOUR_STRATEGY_API_KEY';
  var STRATEGY_ID  = {{ section.settings.strategy_id | json }};
  var SHOP_DOMAIN  = {{ shop.permanent_domain | json }};
  var CTA_LABEL    = '{{ section.settings.cta_label | default: "Add to cart" }}';
  var MAX_PRODUCTS = {{ section.settings.max_products | default: 8 }};
  var CURRENCY     = {{ shop.currency | default: "USD" | json }};
  var SECTION_ID   = {{ section.id | json }};
  if (!SHOP_DOMAIN || !STRATEGY_ID) return;

  var fmt;
  try {
    fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: CURRENCY });
  } catch(e) {
    fmt = { format: function(n) { return '$' + parseFloat(n).toFixed(2); } };
  }
  function money(str) { return fmt.format(parseFloat(str) || 0); }

  function gidToNumeric(gid) {
    return gid ? String(gid).split('/').pop() : null;
  }

  function priceHtml(price, compareAt) {
    var hasSale = compareAt && parseFloat(compareAt) > parseFloat(price);
    return hasSale
      ? '<span class="aftersell-card-price--compare">' + money(compareAt) + '</span>'
        + '<span class="aftersell-card-price--sale">' + money(price) + '</span>'
      : '<span class="aftersell-card-price--current">' + money(price) + '</span>';
  }

  var productContext = {
    productId:   'gid://shopify/Product/{{ product.id }}',
    variantId:   'gid://shopify/ProductVariant/{{ product.selected_or_first_available_variant.id }}',
    quantity:    1,
    price:       {{ product.price | divided_by: 100.0 }},
    handle:      {{ product.handle | json }},
    title:       {{ product.title | json }},
    vendor:      {{ product.vendor | json }},
    productType: {{ product.type | json }},
    tags:        {{ product.tags | json }},
    collections: [{% for col in product.collections %}'gid://shopify/Collection/{{ col.id }}'{% unless forloop.last %},{% endunless %}{% endfor %}],
    sellingPlan: {% if product.selected_selling_plan %}'subscription'{% else %}'one-time'{% endif %}
  };

  var cartContext = {
    subtotal:  {{ cart.total_price | divided_by: 100.0 }},
    itemCount: {{ cart.item_count }},
    lineCount: {{ cart.items.size }}
  };

  {% if customer %}
  var customerContext = {
    customerId:       'gid://shopify/Customer/{{ customer.id }}',
    tags:             {{ customer.tags | json }},
    {% if customer.default_address.country_code %}countryCode: {{ customer.default_address.country_code | json }},{% endif %}
    {% if customer.default_address.province_code %}provinceCode: {{ customer.default_address.province_code | json }},{% endif %}
    locale:           {{ request.locale.iso_code | json }},
    orderCount:       {{ customer.orders_count }},
    totalSpent:       {{ customer.total_spent | times: 1.0 }},
    acceptsMarketing: {{ customer.accepts_marketing }}
  };
  {% endif %}

  var blockEl  = document.getElementById('aftersell-upsell-' + SECTION_ID);
  var splideEl = document.getElementById('aftersell-splide-' + SECTION_ID);
  var list     = splideEl.querySelector('.splide__list');
  var prevBtn  = blockEl.querySelector('.aftersell-arrow--prev');
  var nextBtn  = blockEl.querySelector('.aftersell-arrow--next');
  var splideInstance = null;

  function getCartToken() {
    var t = {{ cart.token | json }};
    if (t) return Promise.resolve(t);
    return fetch('/cart.js').then(function(r){ return r.json(); }).then(function(c){ return c.token || null; }).catch(function(){ return null; });
  }

  function evaluate() {
    getCartToken().then(function(cartToken) {
      if (!cartToken) return;
      return fetch(BACKEND_URL + '/api/public/strategy/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Strategy-Api-Key': API_KEY },
        body: JSON.stringify({
          shopDomain:  SHOP_DOMAIN,
          strategyId:  STRATEGY_ID,
          context: {
            products:  [productContext],
            cartToken: cartToken,
            cart:      cartContext.itemCount > 0 ? cartContext : undefined,
            {% if customer %}customer: customerContext,{% endif %}
            session:   { currencyCode: CURRENCY }
          }
        })
      });
    }).then(function(res) { if (!res) return; return res.json(); })
    .then(function(data) {
      if (!data || !data.success || !data.products || !data.products.length) {
        clearSkeletons(); return;
      }
      renderCards(data.products.slice(0, MAX_PRODUCTS));
    }).catch(function(err) {
      console.error('[AfterSell carousel] Error:', err);
      clearSkeletons();
    });
  }

  function renderCards(products) {
    if (!products || !products.length) {
      blockEl.style.display = 'none'; return;
    }

    blockEl.style.display = '';

    list.innerHTML = products.map(function(p, i) {
      var img      = p.images && p.images[0] ? p.images[0].src : '';
      var alt      = (p.images && p.images[0] && p.images[0].altText) || p.title;
      var variants = p.variants || [];
      var availableVariants = variants.filter(function(v) {
        return v.availableForSale !== false;
      });
      var isDefaultVariant = variants.length === 1 && variants[0].title === 'Default Title';
      var showSelect       = !isDefaultVariant && availableVariants.length >= 1;
      var firstVariant     = availableVariants.length ? availableVariants[0] : null;
      var firstVariantId   = firstVariant ? gidToNumeric(firstVariant.variantId) : null;
      var anyAvailable     = availableVariants.length > 0;

      var onSale = p.compareAtPrice && parseFloat(p.compareAtPrice) > parseFloat(p.price);
      var badge  = '';
      if (onSale) {
        var pct = Math.round((1 - parseFloat(p.price) / parseFloat(p.compareAtPrice)) * 100);
        badge = '<span class="aftersell-card-badge">Save ' + pct + '%</span>';
      }
      var vendor = p.vendor ? '<p class="aftersell-card-vendor">' + p.vendor + '</p>' : '';

      var selectHtml = '';
      if (showSelect) {
        var options = availableVariants.map(function(v) {
          return '<option value="' + gidToNumeric(v.variantId) + '"'
            + ' data-price="'   + (v.price         || p.price) + '"'
            + ' data-compare="' + (v.compareAtPrice || '')     + '"'
            + '>' + v.title + '</option>';
        }).join('');
        selectHtml = '<select class="aftersell-variant-select" aria-label="Select variant">' + options + '</select>';
      }

      var atcLabel = anyAvailable ? CTA_LABEL : 'Sold Out';
      var atcClass = anyAvailable ? 'aftersell-cta' : 'aftersell-cta aftersell-cta--unavailable';
      var atcBtn   = '<button class="' + atcClass + '"'
        + (firstVariantId ? ' data-variant-id="' + firstVariantId + '"' : '')
        + (anyAvailable ? '' : ' disabled')
        + '>' + atcLabel + '</button>';

      var productUrl = p.url + (p.url.indexOf('?') > -1 ? '&' : '?') + 'ref=aftersell';

      return '<li class="splide__slide">'
        + '<div class="aftersell-card" data-idx="' + i + '">'
        + '<a class="aftersell-card-link" href="' + productUrl + '">'
        + '<div class="aftersell-card-image">' + badge
        + (img ? '<img src="' + img + '" alt="' + alt + '" loading="lazy">' : '')
        + '</div>'
        + '<div class="aftersell-card-body">'
        + vendor
        + '<p class="aftersell-card-title">' + p.title + '</p>'
        + '<p class="aftersell-card-price" data-price-el>' + priceHtml(p.price, p.compareAtPrice) + '</p>'
        + '</div></a>'
        + '<div class="aftersell-card-actions">'
        + selectHtml
        + atcBtn
        + '</div></div></li>';
    }).join('');

    attachCardEvents();
    initSplide();
  }

  function attachCardEvents() {
    list.addEventListener('change', function(e) {
      if (!e.target.classList.contains('aftersell-variant-select')) return;
      var select  = e.target;
      var card    = select.closest('.aftersell-card');
      var opt     = select.options[select.selectedIndex];
      var priceEl = card.querySelector('[data-price-el]');
      var btn     = card.querySelector('.aftersell-cta');
      priceEl.innerHTML     = priceHtml(opt.getAttribute('data-price'), opt.getAttribute('data-compare'));
      btn.dataset.variantId = opt.value;
      btn.disabled          = false;
      btn.textContent       = CTA_LABEL;
      btn.className         = 'aftersell-cta';
    });

    list.addEventListener('click', function(e) {
      var btn = e.target.closest('.aftersell-cta');
      if (!btn || btn.disabled) return;
      var variantId = btn.dataset.variantId;
      if (!variantId) return;

      e.preventDefault();
      btn.disabled    = true;
      btn.textContent = 'Adding…';

      fetch('/cart/add.js', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: parseInt(variantId, 10),
          quantity: 1,
          properties: {
            "_source": "Aftersell",
            "_attribution": "CTA"
          }
        })
      })
      .then(function(r) {
        if (!r.ok) throw new Error('Cart add failed: ' + r.status);
        return r.json();
      })
      .then(function() {
        btn.textContent = 'Added!';
        btn.classList.add('aftersell-cta--added');
        if (typeof window.theme !== 'undefined' && window.theme.cart && window.theme.cart.open) {
          window.theme.cart.open();
        }
        setTimeout(function() {
          btn.textContent = CTA_LABEL;
          btn.classList.remove('aftersell-cta--added');
          btn.disabled = false;
        }, 2500);
      })
      .catch(function(err) {
        console.error('[AfterSell carousel] Add to cart error:', err);
        btn.textContent = 'Try Again';
        btn.disabled    = false;
      });
    });
  }

  function initSplide() {
    function mount() {
      if (typeof window.Splide === 'undefined') {
        setTimeout(mount, 50);
        return;
      }

      splideInstance = new Splide('#aftersell-splide-' + SECTION_ID, {
        type:       'slide',
        perPage:    4,
        perMove:    4,
        gap:        '10px',
        pagination: false,
        arrows:     false,
        speed:      350,
        drag:       false,
        breakpoints: {
          768: { perPage: 2, perMove: 2, drag: 'free', snap: true },
          480: { perPage: 1, perMove: 1, drag: 'free', snap: true, padding: { right: '25%' } }
        }
      }).mount();

      function updateArrows() {
        var idx = splideInstance.index;
        var end = splideInstance.length - splideInstance.options.perPage;
        prevBtn.disabled = idx <= 0;
        nextBtn.disabled = idx >= end;
      }

      prevBtn.addEventListener('click', function() { splideInstance.go('<'); });
      nextBtn.addEventListener('click', function() { splideInstance.go('>'); });
      splideInstance.on('moved', updateArrows);
      updateArrows();
    }
    mount();
  }

  function clearSkeletons() { list.innerHTML = ''; }

  evaluate();
})();
</script>

{% endif %}

{% schema %}
{
  "name": "Aftersell Carousel",
  "settings": [
    { "type": "text",  "id": "strategy_id",  "label": "AfterSell Strategy ID", "default": "ADD_ID_HERE" },
    { "type": "text",  "id": "heading",      "label": "Heading",               "default": "You might also like" },
    { "type": "text",  "id": "cta_label",    "label": "CTA Button Label",      "default": "Add to cart" },
    { "type": "range", "id": "max_products", "label": "Max Products to Show",  "default": 8, "min": 1, "max": 20, "step": 1 }
  ],
  "presets": [{ "name": "Aftersell Carousel" }]
}
{% endschema %}
```

<Frame>
  <img src="https://mintcdn.com/aftersell/SnVX3h-PpMMxQMDU/images/aftersell/strategy-carousel-example.png?fit=max&auto=format&n=SnVX3h-PpMMxQMDU&q=85&s=8fb7ac08c2ae0c430b5bc4f810c59e40" alt="Карусель товаров на основе Strategy, отображаемая на странице товара Shopify" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

<div id="customizing">
  ### Настройка
</div>

Схема секции предоставляет четыре настройки, редактируемые продавцом: **Strategy ID**, **Heading**, **CTA Button Label** и **Max Products to Show**. Добавляйте или удаляйте настройки в блоке `{% schema %}`, чтобы предоставить редактору темы больше параметров.

CSS ограничен именами классов `.aftersell-*` и включает карусель на Splide с 4 карточками в ряд, которая переходит на 2 карточки при 768px и на 1 при 480px. Свободно редактируйте его под свою тему — ничего из этого не требуется для работы вызова API.

***

<div id="cart-context-custom-cart-upsell-block">
  ## Контекст корзины: блок апселла в кастомной корзине
</div>

Эта схема структурно совпадает со схемой для PDP, с одним ключевым отличием: **массив контекста товаров** формируется из позиций корзины, а не из просматриваемого в данный момент товара. Strategy тогда получает каждый товар, добавленный покупателем, и возвращает рекомендации на основе корзины в целом.

Реализация находится там же, где живет код вашей кастомной корзины, — в секции Liquid, отображающей cart drawer, в кастомном блоке headless-витрины или в шаблоне темы, таком как `cart.liquid`. Форма вызова API и обработка ответа идентичны примеру для PDP — отличается только массив `products`.

Структура выглядит так:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
var products = {{ cart.items | json }}.map(function(item) {
  return {
    productId: 'gid://shopify/Product/' + item.product_id,
    variantId: 'gid://shopify/ProductVariant/' + item.variant_id,
    quantity:  item.quantity,
    price:     item.price / 100
    // ...other fields as needed
  };
});
```

Остальная часть полезной нагрузки (cart, customer, session, cartToken) и вызов `fetch` к `/api/public/strategy/evaluate` не отличаются от схемы для PDP выше — только массив `products` меняется с `[productContext]` на массив, полученный из корзины.

***

<div id="what-happens-when-the-strategy-returns">
  ## Что происходит, когда Strategy возвращает результат
</div>

Форма ответа одинакова независимо от того, какой контекст вы отправили:

```json theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{
  "success": true,
  "products": [ /* enriched recommended products */ ],
  "evaluationId": "f3a1c2e0-...",
  "resolution": { "strategyId": "...", "matchedRuleIds": [...], "fallbackUsed": false },
  "meta": { "servedFromCache": false, "processingTimeMs": 12, "data": {} }
}
```

`evaluationId` — уникальный идентификатор этого вычисления. Если вы зафиксируете его и прикрепите к отображаемым товарам, вы сможете атрибутировать итоговый заказ к точной рекомендации, которая его породила, — см. [Атрибуцию](#attribution) ниже.

То, как вы отображаете массив `products`, полностью зависит от кода вашей темы. Приведенный выше сниппет для PDP отображает их как карусель карточек с выбором вариантов и кнопками добавления в корзину; кастомный блок корзины может отобразить их вертикальным списком внутри drawer.

Полную схему запроса и ответа см. в [справочнике Evaluate Strategy API](/ru/aftersell/strategies_api_reference_evaluate_strategy).

***

<div id="when-no-product-is-returned">
  ## Когда товар не возвращается
</div>

Если Strategy не вернула товары (`products: []`), обработка этого случая остается на усмотрение вашего кода. Приведенный выше сниппет для PDP полностью скрывает карусель. Кастомный блок корзины может вернуться к стандартному списку апселлов корзины или просто ничего не отображать.

Чтобы избежать пустого ответа, настройте **Catch all** в Strategy, чтобы всегда был резервный товар для возврата. О настройке Catch all см. страницу [Создание Strategies](/ru/aftersell/strategies_building_in_app).

***

<div id="tips-for-custom-integrations">
  ## Советы по пользовательским интеграциям
</div>

* **Формируйте контекст в Liquid.** Liquid выполняется во время рендеринга и имеет доступ к полному графу объектов Shopify — товар, корзина, покупатель, магазин, запрос. Используйте его для заполнения полезной нагрузки на стороне сервера вместо клиентских вызовов.
* **Не публикуйте API-ключ в открытых репозиториях.** Он окажется в коде вашей темы, который доставляется в браузер, — это нормально. Но не вставляйте эту же тему в публичный репозиторий и не делитесь сборкой с посторонними.
* **Используйте Catch all.** Витрина выглядит сломанной, когда слот исчезает. Catch all с небольшим набором безопасных значений по умолчанию сохраняет согласованность интерфейса.
* **Кешируйте там, где это уместно.** Strategies API выполняет легкое кеширование на стороне сервера (`meta.servedFromCache`), но для страниц товаров с высоким трафиком имеет смысл также применять debounce или мемоизацию вызовов на клиенте (например, не вызывать API повторно, когда один и тот же товар отображается дважды за сессию).

***

<div id="attribution">
  ## Атрибуция
</div>

Когда покупатель нажимает кнопку добавления в корзину в сниппете, вызов `/cart/add.js` прикрепляет **свойства позиции (line item properties)** к товару в корзине:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "Aftersell",
  "_attribution": "CTA"
}
```

Эти свойства сопровождают позицию вплоть до заказа Shopify, где они появляются в записи позиции. Вы можете использовать их дальше по цепочке для атрибуции выручки, фильтрации заказов или передачи в аналитические инструменты, которые читают свойства позиций.

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

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "PDP Carousel",
  "_strategy_id": "{{ section.settings.strategy_id }}",
  "_campaign": "summer-2026"
}
```

<Tip>
  Ключи свойств, начинающиеся с подчеркивания (`_`), скрыты из интерфейса корзины и оформления заказа, но все равно прикрепляются к заказу. Используйте префикс с подчеркиванием для метаданных атрибуции, которые вы не хотите показывать покупателям.
</Tip>

Применяйте ту же схему в реализации с контекстом корзины — любой вызов добавления в корзину из кастомного блока апселла может передавать любые нужные вам свойства.

<div id="attributing-back-to-the-evaluation">
  ### Атрибуция к конкретному вычислению
</div>

Чтобы связать заказ с **конкретным вычислением**, которое порекомендовало товар, — а не просто с фактом «пришло из Strategy», — зафиксируйте `evaluationId` из ответа и прикрепите его к позиции в свойстве `__as_offer_id`. AfterSell читает этот ключ, поэтому заказы, помеченные им, атрибутируются к конкретному вычислению в отчетности.

В обработчике `evaluate()` сохраните идентификатор из ответа:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.then(function(data) {
  if (!data || !data.success || !data.products || !data.products.length) {
    clearSkeletons(); return;
  }
  evaluationId = data.evaluationId; // declare `var evaluationId;` alongside your other state
  renderCards(data.products.slice(0, MAX_PRODUCTS));
})
```

Затем включите его в свойства добавления в корзину:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: Object.assign(
  { "_source": "Aftersell", "_attribution": "CTA" },
  evaluationId ? { "__as_offer_id": evaluationId } : {}
)
```

Сохраняйте двойное подчеркивание в `__as_offer_id` — это ключ, который ищет AfterSell, а префикс с подчеркиванием скрывает его от покупателей. Если `evaluationId` отсутствует (например, товары не были возвращены), пропустите это свойство, а не отправляйте пустое значение.
