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

# Custom integratie

> Roep de Strategies API rechtstreeks aan om custom storefront-ervaringen aan te sturen - PDP-carrousels, custom cart-upsells en overal waar de eigen surfaces van Aftersell niet reiken.

<div id="overview">
  ## Overzicht
</div>

Wanneer noch de eigen surfaces van Aftersell (post-purchase, checkout, Upcart), noch een kant-en-klare integratie past, kun je de Strategies API zelf aanroepen vanuit je Shopify-thema en de geretourneerde producten weergeven zoals jij dat wilt.

Het patroon is in elk geval hetzelfde: bouw een contextpayload vanuit Liquid (zodat Shopify-attributen zoals het huidige product, de winkelwageninhoud en klantvelden bij het renderen worden ingevuld), `POST` deze naar `/api/public/strategy/evaluate` en render het antwoord.

Deze pagina behandelt twee implementatiepatronen:

* **PDP-context** - plaats een sectie op productpagina's die de API aanroept met het **momenteel bekeken product** en een carrousel van geretourneerde aanbevelingen weergeeft.
* **Cart-context** - render een upsellblok binnen een custom winkelwagen die de API aanroept met **alle huidige regelitems in de winkelwagen** en de geretourneerde producten weergeeft.

De vorm van de **productcontext** is wat tussen de twee verschilt: één product op de PDP, een array van alle regelitems in de winkelwagen.

***

<div id="what-youll-need">
  ## Wat je nodig hebt
</div>

1. **Je Strategy API-sleutel.** Ga in Aftersell naar **Settings → Product Strategy** en kopieer in de kaart **Security Token** je token (dit is je Strategy API-sleutel).
2. **De Strategy-ID.** Open de Strategy die je wilt uitvoeren in de Aftersell Strategy-editor en kopieer de ID.
3. **Toegang tot themacode.** Je voegt een Liquid-sectie (PDP) of blok (custom winkelwagen) toe aan je Shopify-thema - Online Store → Themes → ... → Edit code.

<Warning>
  Je Strategy API-sleutel staat in client-side themacode, waardoor deze zichtbaar is voor iedereen die de paginabron bekijkt. Behandel deze als een publieke storefront-credential en roteer de sleutel via Aftersell **Settings → Product Strategy** als deze ooit op een onbedoelde manier wordt blootgesteld.
</Warning>

***

<div id="pdp-context-section-snippet">
  ## PDP-context: sectiesnippet
</div>

Dit patroon voegt een Shopify-sectie toe aan je productpagina. Wanneer de pagina rendert, sluit Liquid het huidige product, de winkelwagen en de klantattributen in de payload in, waarna JavaScript naar de Strategies API post en de geretourneerde producten in een Splide-carrousel weergeeft.

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

1. Ga in je Shopify-beheer naar **Online Store → Themes**, klik op **...** bij je thema en selecteer **Edit code**.
2. Maak in de map **Sections** een nieuw bestand aan met de naam `aftersell-upsell-carousel.liquid`.
3. Plak de onderstaande snippet in het nieuwe bestand en vervang `YOUR_STRATEGY_API_KEY` door de API-sleutel uit Aftersell.
4. Sla op.
5. Open je producttemplate (meestal `templates/product.json` of `sections/main-product.liquid`) en voeg de sectie **Aftersell Carousel** toe waar je de carrousel wilt laten verschijnen. Vanuit de thema-editor kun je deze ook rechtstreeks op de productpagina slepen.
6. Plak in de instellingen van de sectie je **Strategy-ID**.

<div id="what-the-section-sends">
  ### Wat de sectie verstuurt
</div>

Voor elke PDP-weergave bevat de payload:

* **products** - een array met één element dat het momenteel bekeken product bevat (productId, variantId, quantity, price, handle, title, vendor, productType, tags, collections, sellingPlan).
* **cart** - subtotaal, aantal items, aantal regels van de huidige winkelwagen van de shopper (weggelaten als de winkelwagen leeg is).
* **cartToken** - zodat de API deze evaluatie aan dezelfde sessie kan koppelen.
* **customer** - tags, land, provincie, taalinstelling, aantal bestellingen, totaal besteed en de accepts-marketing-vlag, maar **alleen als de shopper is ingelogd**.
* **session** - valutacode uit `shop.currency`.

De sectie stuurt standaard geen UTM-parameters mee. Als je op UTM gebaseerde targeting op de PDP wilt, leg deze dan client-side vast en voeg ze toe aan het `session`-object vóór de fetch.

<div id="the-snippet">
  ### De 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="Een door een Strategy aangedreven productcarrousel weergegeven op een Shopify-productpagina" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

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

Het sectieschema biedt vier door de merchant bewerkbare instellingen: **Strategy ID**, **Heading**, **CTA Button Label** en **Max Products to Show**. Voeg instellingen toe of verwijder ze in het `{% schema %}`-blok om meer knoppen aan de thema-editor beschikbaar te stellen.

De CSS is gescoped onder `.aftersell-*`-klassenamen en bevat een door Splide aangedreven carrousel met 4 items, die terugvalt naar 2 items bij 768px en 1 item bij 480px. Pas deze vrij aan om bij je thema te passen - niets ervan is vereist om de API-aanroep te laten werken.

***

<div id="cart-context-custom-cart-upsell-block">
  ## Cart-context: custom cart-upsellblok
</div>

Dit patroon is structureel hetzelfde als het PDP-patroon, met één belangrijk verschil: de **productcontext-array** wordt opgebouwd uit de regelitems van de winkelwagen in plaats van het momenteel bekeken product. De Strategy ontvangt dan elk item dat de shopper heeft toegevoegd en retourneert aanbevelingen op basis van de winkelwagen als geheel.

De implementatie leeft waar je custom winkelwagencode leeft - een Liquid-sectie die de cart drawer rendert, een custom blok in een headless storefront, of een thematemplate zoals `cart.liquid`. De vorm van de API-aanroep en de responseverwerking zijn identiek aan het PDP-voorbeeld - alleen de `products`-array verschilt.

De structuur ziet er als volgt uit:

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

De rest van de payload (cart, customer, session, cartToken) en de `fetch`-aanroep naar `/api/public/strategy/evaluate` zijn ongewijzigd ten opzichte van het PDP-patroon hierboven - alleen de `products`-array wisselt van `[productContext]` naar de uit de winkelwagen afgeleide array.

***

<div id="what-happens-when-the-strategy-returns">
  ## Wat er gebeurt wanneer de Strategy een resultaat geeft
</div>

De vorm van de response is hetzelfde, ongeacht welke context je hebt verstuurd:

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

De `evaluationId` is een unieke id voor deze evaluatie. Als je deze vastlegt en koppelt aan de producten die je rendert, kun je de resulterende bestelling terugkoppelen aan de exacte aanbeveling die deze heeft opgeleverd - zie [Attributie](#attribution) hieronder.

Hoe je de `products`-array rendert, is volledig aan je themacode. De PDP-snippet hierboven rendert ze als een carrousel van kaarten met variantkiezers en toevoegen-aan-winkelwagen-knoppen; een custom cart-blok zou ze als een verticale lijst binnen de drawer kunnen renderen.

Zie voor het volledige request- en responseschema de [Evaluate Strategy API-referentie](/nl/aftersell/strategies_api_reference_evaluate_strategy).

***

<div id="when-no-product-is-returned">
  ## Wanneer er geen product wordt geretourneerd
</div>

Als de Strategy geen producten retourneert (`products: []`), is het aan je code hoe je hiermee omgaat. De PDP-snippet hierboven verbergt de carrousel volledig. Een custom cart-blok zou kunnen terugvallen op de standaard upselllijst van de winkelwagen, of simpelweg niets renderen.

Om een lege response te voorkomen, configureer je een **Catch all** in de Strategy, zodat er altijd een terugvalproduct is om te retourneren. Zie de pagina [Strategies bouwen](/nl/aftersell/strategies_building_in_app) voor het instellen van een Catch all.

***

<div id="tips-for-custom-integrations">
  ## Tips voor custom integraties
</div>

* **Bouw de context in Liquid.** Liquid draait op het moment van renderen en heeft toegang tot de volledige Shopify-objectgraaf - product, winkelwagen, klant, winkel, request. Gebruik het om de payload server-side te vullen in plaats van naar client-side aanroepen te grijpen.
* **Houd de API-sleutel uit publieke repositories.** De sleutel komt in je themacode terecht, die naar de browser wordt verzonden - dat is prima. Maar plak hetzelfde thema niet in een publieke repository en deel de bundel niet extern.
* **Gebruik een Catch all.** Storefront-ervaringen ogen kapot wanneer een slot verdwijnt. Een Catch all met een kleine set veilige standaardproducten houdt de UI consistent.
* **Cache waar het zinvol is.** De Strategies API doet lichte caching server-side (`meta.servedFromCache`), maar voor PDP's met veel verkeer wil je aanroepen mogelijk ook debounce of memoizeren op de client (bijv. niet opnieuw aanroepen wanneer hetzelfde product twee keer in een sessie wordt gerenderd).

***

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

Wanneer een shopper op de toevoegen-aan-winkelwagen-knop in de snippet klikt, voegt de `/cart/add.js`-aanroep **line item properties** toe aan het winkelwagenitem:

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

Deze properties reizen met het regelitem mee tot aan de Shopify-bestelling, waar ze op het regelitemrecord verschijnen. Je kunt ze verderop gebruiken om omzet toe te schrijven, bestellingen te filteren of analysetools te voeden die line item properties lezen.

De sleutels en waarden zijn conventies, geen vereisten - de API-aanroep werkt hetzelfde ongeacht wat je hier invult. Pas ze aan aan je eigen attributiemodel. Bijvoorbeeld:

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

<Tip>
  Property-sleutels die met een underscore (`_`) beginnen, zijn verborgen in de winkelwagen- en checkout-UI, maar worden nog steeds aan de bestelling gekoppeld. Gebruik de underscore-prefix voor metadata die alleen voor attributie is en die je niet aan shoppers wilt tonen.
</Tip>

Pas hetzelfde patroon toe in de cart-context-implementatie - elke toevoegen-aan-winkelwagen-aanroep die je vanuit een custom upsellblok doet, kan alle properties bevatten die je nodig hebt.

<div id="attributing-back-to-the-evaluation">
  ### Terugkoppelen naar de evaluatie
</div>

Om een bestelling terug te koppelen aan de **exacte evaluatie** die het product heeft aanbevolen - in plaats van alleen "kwam van een Strategy" - leg je de `evaluationId` uit de response vast en koppel je deze aan het regelitem onder de property `__as_offer_id`. AfterSell leest deze sleutel, zodat bestellingen die ermee zijn getagd in de rapportage aan de specifieke evaluatie worden toegeschreven.

Houd in de `evaluate()`-handler de id uit de response vast:

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

Neem deze vervolgens op in de toevoegen-aan-winkelwagen-properties:

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

Behoud de dubbele underscore in `__as_offer_id` - het is de sleutel waar AfterSell naar zoekt, en de underscore-prefix houdt deze verborgen voor shoppers. Als `evaluationId` ontbreekt (bijvoorbeeld omdat er geen producten zijn geretourneerd), sla de property dan over in plaats van een lege waarde te versturen.
