Display the cashback a customer will earn on their cart

This guide shows you where the cashback data lives and how to use it to display a "You'll earn $X back on this order" message on a Shopify cart. The actual rendering is up to you — this doc points you at the data and walks through the math.

❗️

This is an estimate. Rivo calculates the authoritative cashback amount server-side when the order is placed. The number you display on the cart is a best-effort preview — see Refining the estimate for the rules that can make it diverge.

Where the data lives

Everything you need is published to the storefront and available on window.Rivo.loy_config:

DataWhere to read it
Is the cashback program on?window.Rivo.loy_config.cashback_program_enabled
The active cashback offers (with earning rate)window.Rivo.loy_config.cashback_offers
Exclusions, order-price & rounding ruleswindow.Rivo.loy_config.order_settings
Which offer the customer opted intolocalStorage["rivo_cashback_profile"] and/or the _rivo_cashback_offer cart attribute

window.Rivo.loy_config is the JavaScript copy of the shop.metafields.rivo.loy metafield, so the same data is available in Liquid if you prefer.

The cashback offer object

Each entry in window.Rivo.loy_config.cashback_offers looks like this:

{
  "id": 123,
  "name": "30% Cashback Launch",
  "identifier": "cashback-launch",
  "description": "Earn 30% back in store credit",
  "status": "active",
  "new_customers_only": true,
  "delay_in_seconds": 0,
  "earning_rule": {
    "trigger": "cashback_order_placed",
    "points_type": "multiplier",
    "balance_amount": 0.30,
    "credits_amount": 0.30,
    "currency_base_amount": 1,
    "multipliers": []
  }
}

The earning rate lives on earning_rule:

keydescription
earning_rule.points_type"multiplier" for a percentage-style offer (e.g. "30% back").
earning_rule.balance_amountCashback earned per currency_base_amount of spend. 0.30 = $0.30 back per unit.
earning_rule.currency_base_amountThe denominator. 1 means "per $1 spent".
offer.new_customers_onlyIf true, only a customer's first order qualifies.

So a "30% cashback" offer is balance_amount: 0.30, currency_base_amount: 1, points_type: "multiplier" — the effective percentage is balance_amount / currency_base_amount * 100.

Order settings (exclusions & rounding)

window.Rivo.loy_config.order_settings carries the rules Rivo applies to earnings. The ones that matter for a cashback estimate:

keydescription
points_program_excluded_product_idsArray of product IDs (numbers). The price of any matching line item is removed from the eligible subtotal.
points_program_excluded_collection_idsArray of collection IDs (numbers). Line items in these collections are excluded — but see the caveat below.
skip_order_earnings_on_product_idsArray of product IDs (strings). If any of these is in the cart, the entire order earns nothing.
order_price_exclude_discountsIf true, earnings are based on the discounted price the customer actually pays.
order_price_include_taxes / order_price_include_shippingWhether tax / shipping are part of the earning base.
credits_order_earnings_rounding_enabledWhether cashback amounts are rounded. Cashback uses the credits_* rounding fields.
credits_order_earnings_rounding_type"round", "floor", or "ceil".
credits_order_earnings_rounding_precisionDecimal places (defaults to 2).

Step 1 — Is the customer opted in?

Cashback only applies once a customer has opted in. Rivo records this in localStorage under rivo_cashback_profile (written by Rivo's storefront script when a customer arrives via a ?rivo-cashback=<identifier> link). The same offer identifier is also stored on the cart as the _rivo_cashback_offer attribute.

const profile = JSON.parse(localStorage.getItem("rivo_cashback_profile") || "{}");

// `cashback_offer` is the identifier they opted into.
// `opt_in_id` is only present once the opt-in has been confirmed and written to the cart.
const optedInIdentifier = profile.cashback_offer;
const isConfirmed = Boolean(profile.opt_in_id);

If profile.cashback_offer is empty, the customer hasn't opted into an offer — don't show a cashback amount.

Step 2 — Find the active offer and its rate

Match the opted-in identifier to an active offer in the config:

function getActiveCashbackOffer() {
  if (!window.Rivo?.loy_config?.cashback_program_enabled) return null;

  const profile = JSON.parse(localStorage.getItem("rivo_cashback_profile") || "{}");
  const identifier = profile.cashback_offer;
  if (!identifier) return null;

  const offers = window.Rivo.loy_config.cashback_offers || [];
  const offer = offers.find(
    (o) => o.identifier === identifier && o.status === "active" && o.earning_rule
  );
  return offer || null;
}

Step 3 — Calculate the cashback on the cart

Read the cart from Shopify's /cart.js, drop excluded line items, apply the rate, and round the way Rivo does.

async function estimateCashback() {
  const offer = getActiveCashbackOffer();
  if (!offer) return null;

  const rule = offer.earning_rule;
  const settings = window.Rivo.loy_config.order_settings || {};
  const excludedProductIds = settings.points_program_excluded_product_ids || [];
  const skipProductIds = settings.skip_order_earnings_on_product_ids || [];

  const cart = await fetch("/cart.js").then((r) => r.json());

  // If a "skip" product is in the cart, the whole order earns nothing.
  // Note: these IDs are strings.
  if (cart.items.some((item) => skipProductIds.includes(String(item.product_id)))) {
    return 0;
  }

  // Sum eligible line items, in dollars. Shopify prices are in cents.
  // `final_line_price` is the price after line-level discounts.
  // Excluded product IDs are numbers.
  const eligibleSubtotal = cart.items
    .filter((item) => !excludedProductIds.includes(item.product_id))
    .reduce((sum, item) => sum + item.final_line_price, 0) / 100;

  let cashback = 0;
  if (rule.points_type === "multiplier") {
    cashback = rule.balance_amount * (eligibleSubtotal / rule.currency_base_amount);
  } else {
    cashback = rule.balance_amount; // flat amount
  }

  return applyCreditsRounding(cashback, settings);
}

function applyCreditsRounding(amount, settings) {
  const precision = settings.credits_order_earnings_rounding_precision ?? 2;
  if (!settings.credits_order_earnings_rounding_enabled) {
    return Math.round(amount * 100) / 100;
  }
  const factor = Math.pow(10, precision);
  switch (settings.credits_order_earnings_rounding_type) {
    case "floor": return Math.floor(amount * factor) / factor;
    case "ceil":  return Math.ceil(amount * factor) / factor;
    default:      return Math.round(amount * factor) / factor;
  }
}

Step 4 — Render it

estimateCashback().then((amount) => {
  if (amount == null || amount <= 0) return;
  const el = document.querySelector("#rivo-cashback-estimate");
  if (el) el.textContent = `You'll earn $${amount.toFixed(2)} back on this order!`;
});
<p id="rivo-cashback-estimate"></p>

Refining the estimate

The snippet above covers the common cases. A few rules are applied by Rivo server-side and can make the final award differ from the cart preview:

  • Collection exclusionspoints_program_excluded_collection_ids is published, but Shopify cart line items don't tell you which collections a product belongs to, so you can't resolve this client-side from /cart.js alone. If you need it, render your own product → collection map (e.g. via Liquid) and skip those line items too.
  • New-customer-only offers — if offer.new_customers_only is true, only the customer's first order qualifies. There's no reliable way to confirm a logged-out shopper's order history client-side, so the server makes the final call.
  • Order-price settingsorder_price_exclude_discounts, order_price_include_taxes, and order_price_include_shipping control exactly which amounts earn. The snippet uses final_line_price (the discounted line total), which matches the most common configuration; adjust if your shop earns on pre-discount totals, taxes, or shipping.

Because of the above, the cart number is a preview. The exact cashback is awarded by Rivo after the order is placed.