Display the cashback a customer will earn on their cart

Show estimated cashback in the cart

You can display an estimated cashback amount in your Shopify cart by combining the customer's Rivo cashback opt-in with the active offer and order settings published to your storefront.

❗️

The cart amount is an estimate. Rivo determines the final cashback after the order is placed and all eligibility rules can be checked.

Before you begin

Customers must first opt in through a Rivo cashback link containing:

?rivo-cashback=<offer-identifier>

Rivo then saves their cashback profile in:

localStorage["rivo_cashback_profile"]

The exact key is rivo_cashback_profile, rather than rivo_cashback.

Rivo also attaches the following attributes to the customer's Shopify cart:

_rivo_cashback_offer
_rivo_cashback_opt_in_id

Use both the saved profile and cart attributes to avoid displaying cashback for an expired or incomplete opt-in.

Cashback data available on the storefront

The cashback program configuration is available through window.Rivo.loy_config.

InformationLocation
Whether cashback is enabledcashback_program_enabled
Active cashback offerscashback_offers
Excluded products and collectionsorder_settings
Discount, tax, and shipping behaviororder_settings
Cashback rounding rulesorder_settings

The selected offer's earning_rule contains the earning amount:

SettingPurpose
points_typemultiplier for percentage-based cashback or fixed for a flat amount
balance_amountThe cashback amount earned
currency_base_amountThe amount of spend represented by one earning increment

For example, these settings represent 30% cashback:

{
  "points_type": "multiplier",
  "balance_amount": 0.3,
  "currency_base_amount": 1
}

Add a cashback message to the cart

Add a placeholder to your cart template:

<p id="rivo-cashback-estimate" hidden></p>

Then add the following JavaScript after Rivo's storefront script has loaded:

function getRivoCashbackProfile() {
  try {
    return JSON.parse(
      localStorage.getItem("rivo_cashback_profile") || "{}"
    );
  } catch (error) {
    return {};
  }
}

function applyCashbackRounding(amount, settings) {
  // Percentage earnings are first calculated to two decimal places.
  let roundedAmount = Math.round(amount * 100) / 100;

  if (!settings.credits_order_earnings_rounding_enabled) {
    return roundedAmount;
  }

  const precision =
    settings.credits_order_earnings_rounding_precision ?? 2;
  const factor = 10 ** precision;

  switch (settings.credits_order_earnings_rounding_type) {
    case "floor":
      return Math.floor(roundedAmount * factor) / factor;
    case "ceil":
      return Math.ceil(roundedAmount * factor) / factor;
    default:
      return Math.round(roundedAmount * factor) / factor;
  }
}

function formatCashback(amount, cart) {
  const currency =
    cart.currency ||
    window.Shopify?.currency?.active;

  if (!currency) {
    return amount.toFixed(2);
  }

  return new Intl.NumberFormat(
    document.documentElement.lang || "en",
    {
      style: "currency",
      currency
    }
  ).format(amount);
}

async function estimateRivoCashback() {
  const config = window.Rivo?.loy_config;

  if (!config?.cashback_program_enabled) {
    return null;
  }

  const profile = getRivoCashbackProfile();

  if (!profile.cashback_offer || !profile.opt_in_id) {
    return null;
  }

  const cartResponse = await fetch("/cart.js");

  if (!cartResponse.ok) {
    return null;
  }

  const cart = await cartResponse.json();
  const attributes = cart.attributes || {};

  // Confirm that the saved opt-in is attached to the current cart.
  if (
    attributes._rivo_cashback_offer !== profile.cashback_offer ||
    String(attributes._rivo_cashback_opt_in_id) !==
      String(profile.opt_in_id)
  ) {
    return null;
  }

  const offer = (config.cashback_offers || []).find(
    (candidate) =>
      candidate.identifier === profile.cashback_offer &&
      candidate.status === "active" &&
      candidate.earning_rule?.status === "active"
  );

  if (!offer) {
    return null;
  }

  const rule = offer.earning_rule;
  const settings = config.order_settings || {};

  const excludedProductIds = new Set(
    (settings.points_program_excluded_product_ids || []).map(String)
  );

  const skipOrderProductIds = new Set(
    (settings.skip_order_earnings_on_product_ids || []).map(String)
  );

  // These products prevent the entire order from earning cashback.
  const shouldSkipOrder = cart.items.some((item) =>
    skipOrderProductIds.has(String(item.product_id))
  );

  if (shouldSkipOrder) {
    return { amount: 0, cart };
  }

  if (rule.points_type === "fixed") {
    const amount = applyCashbackRounding(
      Number(rule.balance_amount || 0),
      settings
    );

    return { amount, cart };
  }

  if (
    rule.points_type !== "multiplier" ||
    Number(rule.currency_base_amount) <= 0
  ) {
    return null;
  }

  const eligibleSubtotalInCents = cart.items
    .filter(
      (item) =>
        !excludedProductIds.has(String(item.product_id))
    )
    .reduce((total, item) => {
      /*
       * Despite its name, `order_price_exclude_discounts`
       * means that total savings are added back when true.
       */
      const linePrice =
        settings.order_price_exclude_discounts
          ? item.original_line_price
          : item.final_line_price;

      return total + linePrice;
    }, 0);

  const eligibleSubtotal = eligibleSubtotalInCents / 100;

  const amount = applyCashbackRounding(
    Number(rule.balance_amount) *
      (eligibleSubtotal / Number(rule.currency_base_amount)),
    settings
  );

  return { amount, cart };
}

async function updateRivoCashbackMessage() {
  const element = document.querySelector(
    "#rivo-cashback-estimate"
  );

  if (!element) return;

  const estimate = await estimateRivoCashback();

  if (!estimate || estimate.amount <= 0) {
    element.hidden = true;
    element.textContent = "";
    return;
  }

  const formattedAmount = formatCashback(
    estimate.amount,
    estimate.cart
  );

  element.textContent =
    `You'll earn an estimated ${formattedAmount} back on this order!`;
  element.hidden = false;
}

updateRivoCashbackMessage();

If your theme updates the cart without reloading the page, call updateRivoCashbackMessage() again from your theme's cart-updated callback.

How exclusions are handled

The example supports two product exclusion settings:

  • points_program_excluded_product_ids removes matching products from the eligible subtotal.

  • skip_order_earnings_on_product_ids makes the entire cart ineligible when it contains a matching product.

Product IDs are converted to strings before comparison so the calculation works whether the published IDs are numbers or strings.

Limitations of a cart estimate

Some eligibility rules cannot be confirmed reliably from /cart.js:

  • Excluded collections: The cart does not include every collection to which a product belongs.

  • Order tags: These are not available before the order is created.

  • Taxes and shipping: Their final amounts may not be known on the cart page.

  • Customer eligibility: New-customer requirements, previous cashback earnings, and offer audience eligibility are confirmed later.

  • Campaign multipliers: Collection-based or other conditional multipliers may require additional product information.

For these reasons, describe the displayed amount as "estimated" or "potential" cashback. The amount awarded by Rivo after checkout remains authoritative.


Did this page help you?