Skip to main content

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.

Overview

Upcart is a separate app from Aftersell, so Strategies aren’t baked into the Upsells module the way they are in Aftersell’s post-purchase and checkout flows. Instead, you bridge the two apps with a small script that calls the Strategies API directly and feeds the result into Upcart’s existing Upsells module via Upcart’s public API. The script is drop-in - paste it once into Upcart’s custom HTML, replace two values (your Strategy API key and the Strategy ID), and the Upsells module will start surfacing whatever products the Strategy returns.

What You’ll Need

  1. Your Strategy API key. In Aftersell, go to Settings → Product Strategy and copy your Strategy API key.
  2. The Strategy ID. Open the Strategy you want to run in the Aftersell Strategy editor and copy its ID.
  3. The Upsells module enabled in Upcart. The script overrides the list of products shown in the existing upsell block, so the module needs to be turned on for anything to render.

Adding the Script

In Upcart, go to Settings → Custom HTML → Scripts (before load) and paste the script below. Replace STRATEGY_ID and STRATEGY_API_KEY with the values from Aftersell, then save.
<script>
  const STRATEGY_ID = "YOUR_STRATEGY_ID";
  const STRATEGY_API_KEY = "YOUR_STRATEGY_API_KEY";
  const STRATEGY_BACKEND_URL = "https://start.aftersell.app";

  let cartToken = null;
  const fetchCartToken = async () => {
    const res = await fetch("/cart.js");
    const c = await res.json();
    cartToken = c.token;
  };

  const mapCartItemToContext = (cartItem) => ({
    productId: "gid://shopify/Product/" + cartItem.productId.toString(),
    variantId: "gid://shopify/ProductVariant/" + cartItem.variantId.toString(),
    tags: [],
    title: cartItem.title,
    vendor: cartItem.vendor,
    productType: cartItem.productType,
    handle: cartItem.handle,
    quantity: cartItem.quantity,
    price: cartItem.originalPrice / 100,
  });

  // --- StrategyProduct -> Upcart Product conversion -----------------------

  const gidToNumericId = (gid) => Number(String(gid).split("/").pop());
  const priceStringToCents = (price) =>
    price == null ? null : Math.round(parseFloat(price) * 100);

  const strategyMetafieldsToProductMetafields = (metafields = []) => {
    const grouped = {};
    for (const { namespace, key, value } of metafields) {
      grouped[namespace] = grouped[namespace] || {};
      grouped[namespace][key] = value;
    }
    return { product: grouped };
  };

  const deriveProductOptions = (variants = []) => {
    const byName = new Map();
    for (const variant of variants) {
      (variant.selectedOptions ?? []).forEach((opt, idx) => {
        if (!byName.has(opt.name)) {
          byName.set(opt.name, { name: opt.name, position: idx + 1, values: [] });
        }
        const entry = byName.get(opt.name);
        if (!entry.values.includes(opt.value)) entry.values.push(opt.value);
      });
    }
    return [...byName.values()];
  };

  const mapStrategyVariantToProductVariant = (variant) => {
    const selected = variant.selectedOptions ?? [];
    const optionValues = selected.map((o) => o.value);
    return {
      id: gidToNumericId(variant.variantId),
      title: variant.title,
      option1: optionValues[0] ?? null,
      option2: optionValues[1] ?? null,
      option3: optionValues[2] ?? null,
      sku: variant.sku ?? "",
      requires_shipping: true,
      taxable: true,
      featured_image: null,
      available: variant.availableForSale,
      name: variant.title,
      public_title: variant.title,
      options: optionValues,
      price: priceStringToCents(variant.price) ?? 0,
      weight: 0,
      compare_at_price: priceStringToCents(variant.compareAtPrice),
      inventory_management: "",
      barcode: null,
      requires_selling_plan: false,
      selling_plan_allocations: [],
    };
  };

  const mapStrategyProductToProduct = (product) => {
    const variants = (product.variants ?? []).map(mapStrategyVariantToProductVariant);
    const variantPrices = variants.map((v) => v.price);
    const priceMin = variantPrices.length ? Math.min(...variantPrices) : (priceStringToCents(product.price) ?? 0);
    const priceMax = variantPrices.length ? Math.max(...variantPrices) : (priceStringToCents(product.price) ?? 0);

    const variantCompareAtPrices = variants
      .map((v) => v.compare_at_price)
      .filter((p) => p != null);
    const compareAtMin = variantCompareAtPrices.length ? Math.min(...variantCompareAtPrices) : 0;
    const compareAtMax = variantCompareAtPrices.length ? Math.max(...variantCompareAtPrices) : 0;

    const images = (product.images ?? [])
      .slice()
      .sort((a, b) => a.position - b.position)
      .map((img) => img.src);

    return {
      id: gidToNumericId(product.productId),
      title: product.title,
      handle: product.handle,
      description: product.description ?? "",
      published_at: "",
      created_at: "",
      vendor: product.vendor ?? "",
      type: product.productType ?? "",
      tags: [...(product.tags ?? [])],
      price: priceStringToCents(product.price) ?? 0,
      price_min: priceMin,
      price_max: priceMax,
      available: product.availableForSale,
      price_varies: priceMin !== priceMax,
      compare_at_price: priceStringToCents(product.compareAtPrice),
      compare_at_price_min: compareAtMin,
      compare_at_price_max: compareAtMax,
      compare_at_price_varies: compareAtMin !== compareAtMax,
      variants,
      images,
      featured_image: images[0] ?? "",
      options: deriveProductOptions(product.variants),
      url: product.url ?? "",
      media: [],
      requires_selling_plan: false,
      selling_plan_groups: [],
      metafields: strategyMetafieldsToProductMetafields(product.metafields),
    };
  };

  // -----------------------------------------------------------------------

  let replacedUpsells = null;
  let lastFetchedCartSignature = null;

  const cartSignature = (cart) =>
    JSON.stringify(cart.items.map((i) => [i.variantId, i.quantity]));

  const runStrategyEvaluation = async () => {
    if (!STRATEGY_ID || !STRATEGY_API_KEY) return;

    await fetchCartToken();
    if (!cartToken) return;

    const cart = window.upcartGetCart();
    if (!cart) return;

    const signature = cartSignature(cart);
    if (signature === lastFetchedCartSignature) return;
    lastFetchedCartSignature = signature;

    const cartContext = {
      subtotal: cart.total_price,
      itemCount: cart.items.reduce((acc, item) => acc + item.quantity, 0),
      lineCount: cart.items.length,
    };

    const products = cart.items.map(mapCartItemToContext);

    const res = await fetch(STRATEGY_BACKEND_URL + "/api/public/strategy/evaluate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Strategy-Api-Key": STRATEGY_API_KEY,
      },
      body: JSON.stringify({
        shopDomain: window.Shopify.shop,
        strategyId: STRATEGY_ID,
        context: {
          products,
          cartToken,
          cart: cartContext.itemCount > 0 ? cartContext : undefined,
          session: { currencyCode: window.Shopify.currency.active },
        },
      }),
    });
    replacedUpsells = await res.json();

    if (typeof window.upcartRefreshCart === "function") {
      window.upcartRefreshCart();
    }
  };

  window.upcartSubscribeCartUpdated(runStrategyEvaluation);

  const waitForUpcartCart = (timeoutMs = 10000) =>
    new Promise((resolve) => {
      const start = Date.now();
      const check = () => {
        if (window.upcartGetCart()) return resolve(true);
        if (Date.now() - start > timeoutMs) return resolve(false);
        setTimeout(check, 100);
      };
      check();
    });

  waitForUpcartCart().then((ready) => {
    if (ready) runStrategyEvaluation();
  });

  window.upcartModifyListOfUpsells = () => {
    if (!replacedUpsells || !Array.isArray(replacedUpsells.products)) return;
    try {
      return replacedUpsells.products.map(mapStrategyProductToProduct);
    } catch (err) {
      console.error("upcartModifyListOfUpsells mapping failed", err);
      return;
    }
  };
</script>
Your Strategy API key authorizes calls against your shop’s Strategies. The script above places it in client-side code, which is the only practical way to invoke the API from the cart drawer. Treat the key as you would any other public storefront credential and rotate it from Aftersell Settings → Product Strategy if it’s ever exposed in a way you didn’t intend.

What the Strategy Sees

Because this runs from the storefront cart, the context is a slim subset of what’s available on Aftersell’s native surfaces:

Product context

The line items currently in the Upcart cart are sent as the input products. Triggers like product type, vendor, product handle, product title, and any product ID / variant ID triggers all evaluate against these items.

Cart context

  • Subtotal - cart total in cents (Upcart’s total_price).
  • Item count - total quantity across all lines.
  • Line count - number of distinct line items.

Session context

  • Currency code - taken from window.Shopify.currency.active.
Customer triggers and UTM triggers won’t match. The default script doesn’t send customer tags, order count, location, or UTM parameters - so any rule using those triggers will never fire. Use product, cart, and currency triggers, or a Catch all, to make sure something always returns.

What Happens When the Strategy Returns

The products returned by the Strategy fully replace the list Upcart would otherwise show in the Upsells module. The merchant-defined upsell list is overridden for the duration of that cart - it’s not merged in. Each Strategy product is mapped to Upcart’s expected product shape (variants, images, options, metafields, etc.) so it renders inside the upsell block exactly like any other product.

When No Product Is Returned

If the Strategy returns no products, the Upsells module renders empty - no upsells are shown. To avoid this, configure a Catch all in the Strategy so there is always a fallback product to return. See the Building Strategies page for how to set up a Catch all.

Re-Evaluation on Cart Changes

Unlike checkout upsells, the Upcart implementation re-evaluates the Strategy every time the cart changes - items added, removed, or quantity-updated. The script subscribes to Upcart’s cartUpdated event, sends the new cart to the Strategies API, and refreshes the cart drawer with the new upsell list. A cart-signature check skips redundant calls if the line items and quantities haven’t actually changed, so back-to-back cart events that don’t materially change the cart won’t re-hit the API.

Tips for Upcart Strategies

  • Design around the cart. Cart-shape and product triggers are the strongest signals you have here. Customer history and UTM-based targeting aren’t sent by the default script.
  • Use Catch all as a safety net. Without one, the Upsells module will show nothing whenever no rule matches.
  • Cache-friendly by default. The cart-signature guard prevents re-firing the API if the cart hasn’t materially changed - good for shoppers who toggle the cart open and closed without editing it.