> ## 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.

# Intégration personnalisée

> Appelez directement l'API Strategies pour alimenter des expériences de boutique personnalisées - carrousels PDP, upsells de panier personnalisés, et partout où les surfaces natives d'Aftersell n'arrivent pas.

<div id="overview">
  ## Aperçu
</div>

Lorsque ni les surfaces natives d'Aftersell (post-achat, checkout, Upcart) ni une intégration packagée ne conviennent, vous pouvez appeler vous-même l'API Strategies depuis votre thème Shopify et afficher les produits renvoyés comme bon vous semble.

Le schéma est le même dans tous les cas : construisez une charge utile de contexte depuis Liquid (afin que les attributs Shopify comme le produit actuel, le contenu du panier et les champs client soient remplis au moment du rendu), envoyez-la en `POST` à `/api/public/strategy/evaluate`, et affichez la réponse.

Cette page couvre deux schémas d'implémentation :

* **Contexte PDP** — déposez une section sur les pages produit qui appelle l'API avec le **produit actuellement consulté** et affiche un carrousel des recommandations renvoyées.
* **Contexte panier** — affichez un bloc d'upsell dans un panier personnalisé qui appelle l'API avec **toutes les lignes d'articles actuelles du panier** et affiche les produits renvoyés.

C'est la forme du **contexte produit** qui diffère entre les deux : un seul produit sur la PDP, un tableau de toutes les lignes d'articles dans le panier.

***

<div id="what-youll-need">
  ## Ce dont vous aurez besoin
</div>

1. **Votre clé API Strategy.** Dans Aftersell, accédez à **Settings → Product Strategy** et, dans la carte **Security Token**, copiez votre jeton (c'est votre clé API Strategy).
2. **Le Strategy ID.** Ouvrez la Strategy que vous souhaitez exécuter dans l'éditeur de Strategy d'Aftersell et copiez son ID.
3. **L'accès au code du thème.** Vous ajouterez une section Liquid (PDP) ou un bloc (panier personnalisé) à votre thème Shopify — Online Store → Themes → ... → Edit code.

<Warning>
  Votre clé API Strategy se trouve dans le code de thème côté client, ce qui la rend visible pour quiconque consulte le code source de la page. Traitez-la comme un identifiant public de boutique et régénérez-la depuis Aftersell **Settings → Product Strategy** si elle est un jour exposée d'une manière non souhaitée.
</Warning>

***

<div id="pdp-context-section-snippet">
  ## Contexte PDP : extrait de section
</div>

Ce schéma ajoute une section Shopify à votre page produit. Au rendu de la page, Liquid intègre les attributs du produit actuel, du panier et du client dans la charge utile, puis JavaScript envoie une requête à l'API Strategies et affiche les produits renvoyés dans un carrousel Splide.

<div id="installing">
  ### Installation
</div>

1. Dans votre admin Shopify, accédez à **Online Store → Themes**, cliquez sur **...** sur votre thème et sélectionnez **Edit code**.
2. Dans le dossier **Sections**, créez un nouveau fichier nommé `aftersell-upsell-carousel.liquid`.
3. Collez l'extrait ci-dessous dans le nouveau fichier et remplacez `YOUR_STRATEGY_API_KEY` par la clé API provenant d'Aftersell.
4. Enregistrez.
5. Ouvrez votre template produit (généralement `templates/product.json` ou `sections/main-product.liquid`) et ajoutez la section **Aftersell Carousel** là où vous souhaitez que le carrousel apparaisse. Depuis l'éditeur de thème, vous pouvez aussi la faire glisser directement sur la page produit.
6. Dans les paramètres de la section, collez votre **Strategy ID**.

<div id="what-the-section-sends">
  ### Ce que la section envoie
</div>

Pour chaque affichage de PDP, la charge utile inclut :

* **products** — un tableau à un seul élément contenant le produit actuellement consulté (productId, variantId, quantity, price, handle, title, vendor, productType, tags, collections, sellingPlan).
* **cart** — sous-total, nombre d'articles, nombre de lignes du panier actuel de l'acheteur (omis si le panier est vide).
* **cartToken** — pour que l'API puisse rattacher cette évaluation à la même session.
* **customer** — tags, pays, province, locale, nombre de commandes, total dépensé et indicateur d'acceptation du marketing, mais **uniquement si l'acheteur est connecté**.
* **session** — code de devise provenant de `shop.currency`.

La section n'envoie pas les paramètres UTM par défaut. Si vous souhaitez un ciblage basé sur les UTM sur la PDP, capturez-les côté client et ajoutez-les à l'objet `session` avant le fetch.

<div id="the-snippet">
  ### L'extrait
</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="Un carrousel de produits piloté par une Strategy affiché sur une page produit Shopify" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

<div id="customizing">
  ### Personnalisation
</div>

Le schéma de la section expose quatre paramètres modifiables par le marchand : **Strategy ID**, **Heading**, **CTA Button Label** et **Max Products to Show**. Ajoutez ou retirez des paramètres dans le bloc `{% schema %}` pour exposer davantage de réglages dans l'éditeur de thème.

Le CSS est isolé sous des noms de classes `.aftersell-*` et inclut un carrousel Splide à 4 éléments qui passe à 2 éléments à 768 px et à 1 élément à 480 px. Modifiez-le librement pour l'adapter à votre thème — rien de tout cela n'est requis pour que l'appel API fonctionne.

***

<div id="cart-context-custom-cart-upsell-block">
  ## Contexte panier : bloc d'upsell de panier personnalisé
</div>

Ce schéma est structurellement identique à celui de la PDP, avec une différence clé : le **tableau de contexte produit** est construit à partir des lignes d'articles du panier plutôt qu'à partir du produit actuellement consulté. La Strategy reçoit alors chaque article ajouté par l'acheteur et renvoie des recommandations basées sur le panier dans son ensemble.

L'implémentation se trouve là où réside le code de votre panier personnalisé — une section Liquid qui affiche le tiroir de panier, un bloc personnalisé dans une boutique headless, ou un template de thème comme `cart.liquid`. La forme de l'appel API et le traitement de la réponse sont identiques à l'exemple PDP — seul le tableau `products` diffère.

La structure ressemble à ceci :

```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
  };
});
```

Le reste de la charge utile (cart, customer, session, cartToken) et l'appel `fetch` à `/api/public/strategy/evaluate` sont inchangés par rapport au schéma PDP ci-dessus — seul le tableau `products` passe de `[productContext]` au tableau dérivé du panier.

***

<div id="what-happens-when-the-strategy-returns">
  ## Ce qui se passe quand la Strategy renvoie un résultat
</div>

La forme de la réponse est la même quel que soit le contexte envoyé :

```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": {} }
}
```

L'`evaluationId` est un identifiant unique pour cette évaluation. Si vous le capturez et l'attachez aux produits que vous affichez, vous pouvez attribuer la commande résultante à la recommandation exacte qui l'a produite — voir [Attribution](#attribution) ci-dessous.

La manière dont vous affichez le tableau `products` dépend entièrement du code de votre thème. L'extrait PDP ci-dessus les affiche sous forme de carrousel de cartes avec sélecteurs de variantes et boutons d'ajout au panier ; un bloc de panier personnalisé pourrait les afficher en liste verticale dans le tiroir.

Pour le schéma complet de requête et de réponse, consultez la [référence de l'API Evaluate Strategy](/fr/aftersell/strategies_api_reference_evaluate_strategy).

***

<div id="when-no-product-is-returned">
  ## Quand aucun produit n'est renvoyé
</div>

Si la Strategy ne renvoie aucun produit (`products: []`), c'est à votre code de décider comment gérer la situation. L'extrait PDP ci-dessus masque entièrement le carrousel. Un bloc de panier personnalisé pourrait se rabattre sur la liste d'upsells par défaut du panier, ou simplement ne rien afficher.

Pour éviter une réponse vide, configurez un **Catch all** dans la Strategy afin qu'il y ait toujours un produit de repli à renvoyer. Consultez la page [Construire des Strategies](/fr/aftersell/strategies_building_in_app) pour savoir comment configurer un Catch all.

***

<div id="tips-for-custom-integrations">
  ## Conseils pour les intégrations personnalisées
</div>

* **Construisez le contexte en Liquid.** Liquid s'exécute au moment du rendu et a accès à l'ensemble du graphe d'objets Shopify — produit, panier, client, boutique, requête. Utilisez-le pour remplir la charge utile côté serveur plutôt que de recourir à des appels côté client.
* **Gardez la clé API hors des dépôts publics.** Elle finira dans le code de votre thème, qui est envoyé au navigateur — c'est acceptable. Mais ne collez pas le même thème dans un dépôt public et ne partagez pas le bundle en externe.
* **Utilisez un Catch all.** Les expériences de boutique semblent cassées quand un emplacement disparaît. Un Catch all avec un petit ensemble de valeurs par défaut sûres garde l'interface cohérente.
* **Mettez en cache là où c'est pertinent.** L'API Strategies effectue une mise en cache légère côté serveur (`meta.servedFromCache`), mais pour les PDP à fort trafic, vous pouvez aussi vouloir temporiser (debounce) ou mémoïser les appels côté client (par ex. ne pas rappeler l'API lorsque le même produit est affiché deux fois dans une session).

***

<div id="attribution">
  ## Attribution
</div>

Lorsqu'un acheteur clique sur le bouton d'ajout au panier dans l'extrait, l'appel `/cart/add.js` attache des **propriétés de ligne d'article** à l'article du panier :

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

Ces propriétés accompagnent la ligne d'article jusqu'à la commande Shopify, où elles apparaissent sur l'enregistrement de la ligne. Vous pouvez les utiliser en aval pour attribuer les revenus, filtrer les commandes ou alimenter des outils d'analyse qui lisent les propriétés de lignes d'articles.

Les clés et les valeurs sont des conventions, pas des exigences — l'appel API fonctionne de la même manière quel que soit leur contenu. Modifiez-les pour les adapter à votre propre modèle d'attribution. Par exemple :

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

<Tip>
  Les clés de propriété qui commencent par un underscore (`_`) sont masquées dans l'interface du panier et du checkout mais restent attachées à la commande. Utilisez le préfixe underscore pour les métadonnées d'attribution que vous ne voulez pas montrer aux acheteurs.
</Tip>

Appliquez le même schéma dans l'implémentation en contexte panier — tout appel d'ajout au panier effectué depuis un bloc d'upsell personnalisé peut transporter les propriétés dont vous avez besoin.

<div id="attributing-back-to-the-evaluation">
  ### Attribuer à l'évaluation d'origine
</div>

Pour relier une commande à l'**évaluation exacte** qui a recommandé le produit — plutôt qu'à un simple « provient d'une Strategy » — capturez l'`evaluationId` de la réponse et attachez-le à la ligne d'article sous la propriété `__as_offer_id`. AfterSell lit cette clé, donc les commandes qui la portent sont attribuées à l'évaluation spécifique dans les rapports.

Dans le gestionnaire `evaluate()`, conservez l'identifiant de la réponse :

```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));
})
```

Puis incluez-le dans les propriétés d'ajout au panier :

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

Conservez le double underscore sur `__as_offer_id` — c'est la clé qu'AfterSell recherche, et le préfixe underscore la garde masquée pour les acheteurs. Si `evaluationId` est absent (par exemple, aucun produit n'a été renvoyé), omettez la propriété plutôt que d'envoyer une valeur vide.
