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:
| Data | Where 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 rules | window.Rivo.loy_config.order_settings |
| Which offer the customer opted into | localStorage["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:
| key | description |
|---|---|
earning_rule.points_type | "multiplier" for a percentage-style offer (e.g. "30% back"). |
earning_rule.balance_amount | Cashback earned per currency_base_amount of spend. 0.30 = $0.30 back per unit. |
earning_rule.currency_base_amount | The denominator. 1 means "per $1 spent". |
offer.new_customers_only | If 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:
| key | description |
|---|---|
points_program_excluded_product_ids | Array of product IDs (numbers). The price of any matching line item is removed from the eligible subtotal. |
points_program_excluded_collection_ids | Array of collection IDs (numbers). Line items in these collections are excluded — but see the caveat below. |
skip_order_earnings_on_product_ids | Array of product IDs (strings). If any of these is in the cart, the entire order earns nothing. |
order_price_exclude_discounts | If true, earnings are based on the discounted price the customer actually pays. |
order_price_include_taxes / order_price_include_shipping | Whether tax / shipping are part of the earning base. |
credits_order_earnings_rounding_enabled | Whether cashback amounts are rounded. Cashback uses the credits_* rounding fields. |
credits_order_earnings_rounding_type | "round", "floor", or "ceil". |
credits_order_earnings_rounding_precision | Decimal 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 exclusions —
points_program_excluded_collection_idsis 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.jsalone. 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_onlyistrue, 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 settings —
order_price_exclude_discounts,order_price_include_taxes, andorder_price_include_shippingcontrol exactly which amounts earn. The snippet usesfinal_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.