Storefront widgets

The widgets in @cascade-commerce/widgets render ratings, reviews, wishlists and the loyalty program on a merchant's storefront. Drop one script tag on the page and mark up the elements you want filled in:

<script
  src="https://cdn.example.com/cascade/embed.js"
  data-cascade-base-url="https://api.cascade.example.com"
  data-cascade-publishable-key="pk_live_..."
></script>

<!-- Star rating for a product -->
<div data-cascade-rating="classic-tee"></div>

<!-- Full reviews list for a product -->
<div data-cascade-reviews="classic-tee"></div>

<!-- A row of the best reviews from across your store -->
<div data-cascade-reviews-carousel></div>

<!-- A wall of shopper photos and videos -->
<div data-cascade-media-grid></div>

The value of each attribute is the product slug. On Shopify that is the product handle:

<div data-cascade-reviews="{{ product.handle }}"></div>

The review and rating widgets read public data, so the publishable key is all they need. The wishlist widgets belong to one shopper, so they do their real work once the storefront says who that shopper is: see Identifying the customer and Wishlists. For a guest they keep the shopper's saves in the browser until they sign in. The loyalty widget works both ways too: it shows the program to anyone, and a shopper's own balance once the storefront signs them in.

Every endpoint these widgets call is documented in the Widget API reference.

If your storefront is a React app, the same widgets are React components you can import and take apart: see React widgets for the package, the parts each widget exposes and how to lay them out yourself.

Appearance

The widgets are built to disappear into your storefront. Out of the box they take their font and their text color from whatever they sit in, their background is transparent, and the one filled control uses your page's own text-on-page pair. On most stores that is already close enough that there is nothing to do.

When it is not, Settings, Widget appearance is where you change it. It is a form beside a live preview of the real widgets against sample data, so you can see a change before anyone else does.

Per site

Appearance is set per site, so a second storefront can look like itself. Pick the site at the top of the page. If you want two sites to match, use Copy from another site: it copies the colors and the custom CSS across in one go. It is a copy rather than a link, so the two drift apart the moment you edit either of them again.

The Stylesheet URL shown under the site picker is the stylesheet your widgets load. The embed script requests it for you, so you only need it if you would rather link it yourself; see How the theme reaches your storefront.

The properties you can set

Everything the widgets draw resolves through this set of CSS custom properties. The editor sets them for you, and you can also set any of them yourself from your own stylesheet.

PropertyWhat it colorsDefault
--cascade-fontThe font stack.inherit
--cascade-textBody text.inherit
--cascade-muted-textDates, counts, help lines.65% of the text color
--cascade-surfaceThe ground behind cards and panels.transparent
--cascade-surface-mutedRating bar tracks, thumbnails, the summary panel.A faint gray
--cascade-borderEvery hairline between rows, cards and controls.A light gray
--cascade-accentFilled buttons, the selected tab, progress fills.CanvasText
--cascade-accent-textText and glyphs on top of the accent.Canvas
--cascade-starA filled star, and the rating breakdown bars.#facc15
--cascade-dangerError messages, and a negative review theme.#b42318
--cascade-radiusCorner radius on cards, panels and controls.8px

A few notes on filling them in. The font is a CSS font stack, and left as inherit the widgets use whatever font the page around them uses; the same goes for the text color. A transparent surface lets your own page color show through, which is usually what you want. The radius is a CSS length such as 8px, and cards, panels and controls all use it.

Two more are not in the editor, because they are derived rather than chosen. Set them from custom CSS if you need to:

  • --cascade-control-radius rounds buttons and inputs on their own, so you can have pill buttons without rounding the cards to match. It follows --cascade-radius by default.
  • --cascade-floating-surface is the ground under the two things that float over your page, the loyalty panel and the wishlist dialog. It has to be opaque, so it does not follow --cascade-surface.

Check your accent against your text on accent. They are the one pairing in the set that carries text on a filled ground, and a light accent with light text on it is unreadable.

How the theme reaches your storefront

The embed script requests a stylesheet before it renders anything, so your storefront never flashes the default colors and then swaps:

GET /widgets/theme.css?apiPublishableKey=pk_live_...&site=main-store

It uses the site from data-cascade-site on your script tag. With no site named it serves your oldest site's appearance, which is the right answer if you only have one.

That URL is cached for five minutes. If you would rather it were cached indefinitely, the appearance page shows a versioned URL: put the version on your script tag as data-cascade-theme-version and the file becomes immutable until you next change something. You can also link the versioned URL from your own <head> instead, which is worth doing if you load the embed script late.

Custom CSS

Anything the properties above cannot express goes in the custom CSS box. The class names below are a supported API: we will not rename them without saying so. Anything else in the markup is internal and can change, so do not select on it.

WidgetClasses
Shared.cascade (on every widget's outermost element), .cascade-button, .cascade-button--primary, .cascade-button--link, .cascade-input, .cascade-muted, .cascade-heading, .cascade-notice
Rating.cascade-rating, .cascade-rating-stars, .cascade-rating-count
Reviews.cascade-reviews, .cascade-reviews-summary, .cascade-reviews-overall, .cascade-reviews-average, .cascade-reviews-breakdown, .cascade-reviews-breakdown-row, .cascade-reviews-breakdown-bar, .cascade-reviews-breakdown-fill, .cascade-reviews-toolbar, .cascade-reviews-list, .cascade-reviews-item, .cascade-reviews-item-header, .cascade-reviews-item-name, .cascade-reviews-item-title, .cascade-reviews-item-body, .cascade-reviews-item-media, .cascade-reviews-item-response, .cascade-reviews-empty, .cascade-reviews-ai-summary, .cascade-reviews-write, .cascade-reviews-write-toggle, .cascade-reviews-form, .cascade-reviews-form-field, .cascade-reviews-form-label, .cascade-reviews-form-stars, .cascade-reviews-form-star, .cascade-reviews-form-media, .cascade-reviews-form-thumb, .cascade-reviews-form-actions, .cascade-reviews-form-status, .cascade-reviews-question-summary, .cascade-reviews-question-summary-item, .cascade-reviews-question-summary-header, .cascade-reviews-question-summary-value, .cascade-reviews-question-track, .cascade-reviews-question-marker, .cascade-reviews-question-ends, .cascade-reviews-question-options, .cascade-reviews-question-option, .cascade-reviews-question-option-bar, .cascade-reviews-question-option-fill, .cascade-reviews-item-answers, .cascade-reviews-item-answer, .cascade-reviews-item-answer-label, .cascade-reviews-item-answer-value, .cascade-reviews-question, .cascade-reviews-question-scale, .cascade-reviews-question-step, .cascade-reviews-question-choices, .cascade-reviews-question-choice
Reviews carousel.cascade-reviews-carousel, .cascade-reviews-carousel-header, .cascade-reviews-carousel-nav, .cascade-reviews-carousel-arrow, .cascade-reviews-carousel-track, .cascade-reviews-carousel-card, .cascade-reviews-carousel-card-title, .cascade-reviews-carousel-card-body, .cascade-reviews-carousel-card-footer, .cascade-reviews-carousel-name, .cascade-reviews-carousel-verified, .cascade-reviews-carousel-product, .cascade-reviews-carousel-thumb, .cascade-reviews-carousel-empty
Media grid.cascade-media-grid, .cascade-media-grid-tiles, .cascade-media-grid-tile, .cascade-media-grid-tile-button, .cascade-media-grid-image, .cascade-media-grid-play, .cascade-media-grid-load-more, .cascade-media-grid-empty, .cascade-media-grid-caption, .cascade-media-grid-caption-title, .cascade-media-grid-caption-meta, .cascade-media-grid-verified, .cascade-media-grid-product-link
Loyalty.cascade-loyalty, .cascade-loyalty-header, .cascade-loyalty-balance, .cascade-loyalty-section, .cascade-loyalty-list, .cascade-loyalty-row, .cascade-loyalty-row-name, .cascade-loyalty-tiers, .cascade-loyalty-tier, .cascade-loyalty-progress, .cascade-loyalty-stamps, .cascade-loyalty-code, .cascade-loyalty-launcher, .cascade-loyalty-launcher-button, .cascade-loyalty-launcher-panel, .cascade-loyalty-points
Wishlists.cascade-wishlists, .cascade-wishlists-header, .cascade-wishlists-row, .cascade-wishlists-items, .cascade-wishlists-item, .cascade-wishlists-modal, .cascade-add-to-wishlist, .cascade-add-to-wishlist-popover

A modifier is always a -- suffix on the block it belongs to (.cascade-button--primary, .cascade-loyalty-tier--current), so a state is easy to spot in a selector.

Your CSS is checked before it is saved, and what is stored is the checked version. The rules:

  • Everything is scoped to the widget root. A rule for body or :root is kept but can never match, because it is rewritten to sit under .cascade. Nothing you write here can restyle the rest of your page, which is the point.
  • Only @media, @supports and @container survive. @import, @font-face and the rest are removed. Load a webfont from your own theme and point --cascade-font at it.
  • url() may only carry a data: URI. The stylesheet is served from Cascade's domain, so it does not fetch from anywhere else. A declaration with any other url() is dropped.
  • Nested rules are dropped. Write each selector out in full.
  • 20,000 characters is the limit. Past that the save is refused rather than truncated.

Anything removed is reported back to you, so you are never left wondering why a rule did nothing.

Writing CSS from a description

Describe what you want in the box above the editor and Write it for me turns it into CSS. It knows the properties and the class names above, and it rewrites your custom CSS in full rather than appending to it.

Nothing is saved until you say so. You get the CSS, a sentence on what it changes, and the preview showing it, and then you choose Accept or Discard. It can only do what you could do by hand: the result goes through the same checks as anything you type, so it cannot reach outside your widgets either.

Ask for a look, not for a layout. Colors, spacing, type, borders and corners are what this is for. It cannot reorder sections, hide the rating breakdown, change the star shape or remove the verified-buyer badge, because those are not styling.

It is limited to 20 requests per organization, refilling at one a minute.

Putting an earlier version back

Every save of the custom CSS is kept, including the ones a description wrote, and the last 50 are listed under Earlier versions with who saved them and what they were asked for. Restore puts one back. A restore is itself saved as a version, so undoing an undo costs nothing.

The colors are not versioned. They are one value each and the editor shows what they are, so Reset beside a property is the whole story there.

Merchant replies

Open a review in the dashboard and use the reply composer to answer it. The reply shows up under the review body in the reviews widget, headed "Response from" the name you replied as.

A reply is published separately from the review it answers, so you can draft one now and put it live later. Only published replies reach the widget: a draft is never sent to the storefront.

Review summaries

Once a product has enough published reviews, the reviews widget shows a short summary of what reviewers say above the list, headed "What reviewers say" and marked as generated. Under it are up to five themes, each with roughly how many reviews mention it and whether reviewers raise it approvingly, as a complaint, or both.

The summary is generated from the published reviews only, in whichever language most of them are written in, and it says so: shoppers are told it is not written by you or by a reviewer. It is kept out of the structured data on the page, because it is not a review anyone wrote.

It refreshes on its own as reviews come in, at most once a day per product, and it is never generated from fewer than five published reviews. On the product page in the dashboard you can regenerate it, hide it from your storefront, or go back to the previous version after a regeneration. To turn the whole feature off, use "Summarize what reviewers say" on the Reviews settings page.

Writing a review

The reviews widget carries a "Write a review" button above the list. It opens a form in place, so nothing floats over the page and the shopper keeps their place on it. The form asks for a rating, a title, the review, a name and an email address, and takes up to five photos or videos. If you have set up review questions, it asks those too, between the review and the name.

What happens next is your moderation setting, not the widget's: a review submitted here arrives unpublished and waits in Reviews, unless auto-publishing is on and the rating clears your threshold. The shopper is told which happened, so nobody is left expecting their review to appear straight away. A review that is waiting does not show up in the list or in the structured data on the page.

Two more things about these reviews:

  • They are never verified buyers. That badge is for reviews collected from a request sent after a real order.
  • The email address is not published. It identifies the reviewer for moderation and keeps one person to one review per product. Reviewers appear as a first name and a last initial, the same as every other review.

When the storefront has signed the shopper in (see Identifying the customer), their address is filled in, locked, and used instead of anything typed into the form.

To collect reviews only from the requests you send, turn the form off:

<div data-cascade-reviews="classic-tee" data-cascade-review-form="false"></div>

On Shopify it is the "Let shoppers write a review here" checkbox on the Product reviews block.

Review questions

Beside the stars you can ask reviewers a few short questions of your own: how something fits, how the quality feels, whether they would buy it again. You set them up on the Reviews settings page, and each one is either a scale (a row of numbered steps with a word at each end, and optionally one in the middle) or a multiple choice (a set of options, one or several of which can be picked).

The reviews widget uses them in three places.

  • In the write form. Every question scoped to the product is asked under the review box, in the order you set. A scale is a row of steps with its end labels underneath; a multiple choice is a set of radio buttons, or checkboxes when several answers are allowed. A question you marked as required carries a "Required" note and the form will not send until it is answered.
  • Above the list. Under the star breakdown, each question you chose to show gets a line of its own: a scale as a track with a marker at the average and a reading like "True to size, 3.0 of 5 on average", a multiple choice as one bar per option with the share that picked it. Only published reviews count, and only questions and options you chose to show appear. A question nobody has answered yet is left out rather than drawn empty. Because a shopper can pick several options on some questions, those shares can add up to more than 100%.
  • On each review card. Under the review body, that reviewer's own answers read as short lines: "How is the fit? True to size", "Would you buy it again? Yes". A step you did not name reads as its position, like "4 of 5".

Archiving a question keeps the answers it already has, so the block above the list can still name it. Hiding a question, or one of its options, takes it off the storefront everywhere while the answers stay in your reports.

The same questions are asked on the emailed review form, so the two collect the same thing. See Collecting reviews.

To keep the answers on the review cards but drop the block above the list:

<div data-cascade-reviews="classic-tee" data-cascade-question-summary="false"></div>

Sorting and filtering

The reviews widget carries a sort control above the list. Shoppers can order reviews by newest (the default), highest rating, lowest rating, or most helpful. Ties in the helpful order fall back to newest, so the list is stable from one page to the next.

The bars in the rating breakdown are buttons. Clicking one narrows the list to that star rating, and clicking it again (or "Show all") clears it. The counts in the breakdown always describe the whole product, never the filtered subset, so the shape of the ratings stays readable while a filter is on.

Helpful votes

Each review carries "Helpful" and "Not helpful" buttons with a running count. Voting takes no sign-in: the widget keeps a random token in the browser's local storage and sends that with the vote, so a visitor can change their mind and the earlier vote is replaced rather than counted twice. The buttons show which way this visitor voted, and that survives a reload as long as local storage is available.

Votes are anonymous and are not tied to a customer record. Only published reviews accept them, and a review that is unpublished later keeps its votes without showing them anywhere.

The carousel is a sliding row of review cards, for the places a full review list is too much: a home page, a landing page, the space under a product's description. Each card carries the stars, the review title and an excerpt of the body, the reviewer's name and the verified-buyer badge, and a thumbnail if the review came with a photo.

<!-- Reviews from across your store -->
<div data-cascade-reviews-carousel></div>

<!-- Only this product's reviews -->
<div data-cascade-reviews-carousel="classic-tee"></div>

Store-wide is the default, and in that mode each card names the product the review is about and links to it when Cascade knows the product's URL. Put a product slug in the attribute to narrow it to one product, and the product line goes away because every card is the same product.

AttributeWhat it does
data-cascade-min-ratingLowest rating to show. Defaults to 4 store-wide, and to no floor when the carousel is one product's.
data-cascade-max-reviewsHow many reviews to load. Defaults to 10, and the maximum is 50.
data-cascade-headingThe heading above the cards. Defaults to "What shoppers say".

The cards are ordered by rating, highest first. They slide with the arrow buttons on either side of the heading, by dragging on a touch screen, or with the arrow keys once the row has focus. Nothing advances on its own, because a card that moves while someone is reading it is worse than one they have to ask for.

The carousel does not emit structured data. Your product pages already do that through the reviews or rating widget, and a second copy of the same reviews on a home page is not something to publish twice.

On Shopify it is the Reviews carousel block, with the same three settings plus an "Only this product" checkbox for product templates.

Media grid

The media grid is a wall of the photos and videos shoppers attached to their reviews, one tile per file. It is the shoppable UGC surface: a home page section, a landing page, or a band under a product's description. The tiles carry no words, and clicking one opens the photo full size with the review behind it: the stars, the title, an excerpt, the reviewer's name and their verified-buyer badge.

<!-- Photos and videos from across your store -->
<div data-cascade-media-grid></div>

<!-- Only this product's photos and videos -->
<div data-cascade-media-grid="classic-tee"></div>

Store-wide is the default, and in that mode the lightbox carries a "View product" link to the product the review is about, when Cascade knows the product's URL. Put a product slug in the attribute to narrow it to one product, and the link goes away because every photo is the same product.

AttributeWhat it does
data-cascade-min-ratingLowest rating to show. There is no floor by default, so every published review counts.
data-cascade-max-itemsHow many tiles a page holds. Defaults to 12, and the maximum is 48.
data-cascade-headingThe heading above the grid. Defaults to "From our customers".

Tiles are ordered newest review first, and a review's own photos stay together, so a page can come back a little over the number you asked for. When there is more, a "Load more photos" button appends the next page. Video tiles show a frame from the start of the clip under a play glyph, and play with controls in the lightbox. Inside the lightbox, the arrow keys walk the whole grid and Escape closes it.

The grid does not emit structured data. Your product pages already publish these reviews through the reviews or rating widget.

On Shopify it is the Review photos and videos block, with the same three settings plus an "Only this product" checkbox for product templates.

Structured data

Both review widgets can emit schema.org JSON-LD, which is what produces the star snippets in Google search results. The markup is a Product node carrying an AggregateRating, and, from the reviews widget, the individual Review records as well:

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Classic T-Shirt",
  "url": "https://shop.example.com/products/classic-tee",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.67,
    "reviewCount": 3,
    "bestRating": 5,
    "worstRating": 1
  },
  "review": [
    {
      "@type": "Review",
      "name": "Great shirt",
      "reviewBody": "Fits perfectly and the material is soft.",
      "datePublished": "2026-01-02",
      "author": { "@type": "Person", "name": "Alice J." },
      "reviewRating": {
        "@type": "Rating",
        "ratingValue": 5,
        "bestRating": 5,
        "worstRating": 1
      }
    }
  ]
}

When the product has published questions and answers, the reviews widget emits a second node beside it: a QAPage whose mainEntity is one Question per published question, with your store's answer as the acceptedAnswer and any shopper answers as suggestedAnswer. A question with no published answer is left out, because search engines reject a question that has none.

The generated instant answer is deliberately absent from both nodes. It is private to the shopper who asked, and nobody wrote it.

Defaults

WidgetEmits markup by defaultWhy
data-cascade-reviewsYesIt belongs on a product page, once per page.
data-cascade-ratingNoIt usually repeats across a collection or grid.

Override either with data-cascade-structured-data:

<!-- Product page with only the compact rating widget: turn markup on -->
<div data-cascade-rating="classic-tee" data-cascade-structured-data></div>

<!-- Reviews widget somewhere that is not a product page: turn markup off -->
<div data-cascade-reviews="classic-tee" data-cascade-structured-data="false"></div>

In React, the same controls are the structuredData and productUrl props on <ProductRatingWidget> and <ProductReviewsWidget>. Both keep emitting their markup when you compose the widget from parts.

Where to put the widget

Structured data describes the page it sits on, so where the widget goes decides which page Google attaches the stars to.

  • Put the marked-up widget on the product's own page. A review snippet is only eligible when the page's main content is the thing being reviewed. Markup on a home page or a blog post will be ignored, or flagged in Search Console.
  • One marked-up widget per page. If a product page has both widgets, leave the rating widget's markup off (the default) and let the reviews widget emit it. Two Product nodes with different rating values on one page is worse than one.
  • Leave it off in collection and search grids. Those pages list many products and none of them is the page's main content.
  • Check what your theme already emits. Most Shopify themes emit their own Product node. That is fine, and Google reads both, but if the theme already publishes an aggregateRating from another review app, turn Cascade's off rather than shipping two contradictory ratings.

The widget takes the page's <link rel="canonical"> for the product URL, and falls back to the current path without its query string or fragment. Set data-cascade-product-url when neither is right, for example when the widget sits in a modal or a quick-view drawer served from a different path.

What gets emitted

  • Nothing at all until the product has at least one published review. An AggregateRating with a count of zero is invalid, so no script tag is written.
  • Only published reviews, which is what the widget itself shows.
  • The reviewer's first name and last initial, the same name shown on the page. A review from an unnamed customer is attributed to "Anonymous".
  • Reviews loaded through "Load more" are added to the markup as they appear, so what Google sees keeps matching what a visitor sees.

Verifying it

Run the product page through Google's Rich Results Test. Use the URL tab, not the code tab: the widget writes its JSON-LD after it fetches its data, so the markup only exists once the page's JavaScript has run, and the URL test renders the page the way Googlebot does. A healthy product page reports valid "Product snippets" and "Review snippets" items.

Identifying the customer

Anything that belongs to one shopper (their wishlists, their loyalty balance) needs the storefront to say who is signed in. Cascade will not take the browser's word for it, so the storefront proves the email address with a signature made on your server.

Add three more attributes to the same script tag:

<script
  src="https://cdn.example.com/cascade/embed.js"
  data-cascade-base-url="https://api.cascade.example.com"
  data-cascade-publishable-key="pk_live_..."
  data-cascade-customer-email="[email protected]"
  data-cascade-timestamp="1735689600"
  data-cascade-signature="48eb876e883be4490d05d06ea8b9fe870b481c1098cc7ef88553998690a1dd31"
></script>
AttributeValue
data-cascade-customer-emailThe signed-in customer's email address.
data-cascade-timestampUnix time in seconds when you made the signature.
data-cascade-signatureHex HMAC-SHA256 of the email and timestamp, keyed with your secret.

All three go together. A page that carries some of them but not all is refused by the API, which is a wiring mistake worth surfacing rather than quietly serving the guest view. For a signed-out shopper, leave all three off: the review and rating widgets carry on working, and the wishlist widgets switch to browser-kept guest lists that are claimed on sign-in. See Before signing in.

The signing recipe

  1. Take the customer's email address exactly as you send it in data-cascade-customer-email, including its case.
  2. Take the current Unix time in seconds, as a string.
  3. Concatenate the two, in that order, with nothing between them.
  4. HMAC the result with SHA-256, keyed with your widget signing secret, and encode the digest as lowercase hexadecimal.

A signature is good for 24 hours, so a cached page keeps working for the rest of the day. Sign on every page render anyway: it costs nothing and the shopper never meets an expiry.

Your widget signing secret lives in Cascade under Settings > API keys, in the widget signing secret card below your keys. There is one per organization, shared by every site and every widget. Reveal it, copy it, and set it on your server as CASCADE_SIGNING_SECRET or whatever your app calls it.

The publishable key is safe in page source. The signing secret is not: anyone holding it can sign as any of your customers, so keep it on your server and out of theme files, JavaScript bundles and version control.

Rotating the secret

The same card has a Rotate secret button, for when the value has leaked or has been sitting in one place too long. Rotation takes effect immediately and there is no grace period: every storefront still signing with the old value starts failing, and authenticated widgets stop loading for your shoppers until the new secret is deployed everywhere you sign. Have the replacement ready to ship before you rotate.

Rotations are recorded in the audit trail, so a widget that suddenly stops authenticating can be traced back to one.

Signing it on your server

Node
import { createHmac } from "node:crypto"

const signingSecret = process.env.CASCADE_SIGNING_SECRET

export function cascadeCustomerAuth(email) {
  const timestamp = String(Math.floor(Date.now() / 1000))
  const signature = createHmac("sha256", signingSecret).update(`${email}${timestamp}`).digest("hex")

  return { email, timestamp, signature }
}

All three produce the same signature. With the secret wsk_demo_signing_secret, the email [email protected] and the timestamp 1735689600, every one of them returns 48eb876e883be4490d05d06ea8b9fe870b481c1098cc7ef88553998690a1dd31. Use that as a test vector when you wire yours up.

On Shopify

There is nothing to do. Turn on the Cascade embed in your theme and the app handles this for you: it asks Cascade for the signature of whoever is signed in, on your store's own domain, and hands it to the widgets. Your signing secret stays on Cascade's servers.

Do not sign this in Liquid. Liquid has an hmac_sha256 filter, but the secret would have to live in a theme setting or a shop metafield, and both are readable by anyone who can view your storefront or edit your theme.

See Widgets in your theme.

When a signature is rejected

Every widget call that carries a customer answers 401 with one of these messages:

MessageWhat happened
Unknown publishable keyThe publishable key is wrong, or belongs to another organization.
Invalid timestampThe timestamp isn't a number. It has to be Unix seconds, not an ISO date.
Signature expiredThe timestamp is more than 24 hours from now. Check your server clock.
Invalid signatureThe digest doesn't match. Usually the email and timestamp were joined with a separator, the digest is base64 rather than hex, or the secret is stale.
Missing emailThe email was blank.
Missing signatureThe signature was blank.

Wishlists

Three embeds cover the wishlist feature: a save button for a product page, a full list manager for a customer account page, and the same manager in a modal.

<!-- Save this product, on a product page -->
<div data-cascade-add-to-wishlist="classic-tee"></div>

<!-- The customer's wishlists, inline -->
<div data-cascade-wishlists></div>

<!-- The same, behind a button -->
<div data-cascade-wishlists-modal></div>

All three need the customer attributes from Identifying the customer. See Wishlists for what each one does, how sharing and purchase tracking work, and the Shopify markup.

Loyalty

Two embeds show the loyalty program, and they render the same panel. Use whichever suits the page:

<!-- The program, rendered in place. Goes on a rewards or account page. -->
<div data-cascade-loyalty></div>

<!-- The same panel behind a floating button in the corner of every page. -->
<div data-cascade-loyalty-launcher></div>
AttributeWhat it renders
data-cascade-loyaltyThe panel, inline. Takes no value.
data-cascade-loyalty-launcherA floating pill that opens the panel. Its value is the button label, if you want a different one.

The launcher sits in the bottom right by default. Move it with data-cascade-loyalty-position="bottom-left".

<div data-cascade-loyalty-launcher="Rewards" data-cascade-loyalty-position="bottom-left"></div>

Signed in and signed out

The loyalty widget renders for everybody. Without a customer it shows the catalog: the tiers, the ways to earn, the offers, the stamp cards and what points can be spent on. That is the marketing surface, and it is what convinces somebody to join.

Add the customer attributes from Identifying the customer and the same panel gains the member's own state: their balance, their tier and how far they are from the next one, their stamp cards, and the rewards they are holding with the codes to use them. Joining, claiming a reward and canceling one all need a signed-in shopper.

Unlike the wishlist widgets, a page missing the customer attributes is not an error here. It renders as a signed-out visitor. Send all three attributes or none: a page carrying some but not all of them is treated as signed out.

Script tag attributes

Three optional attributes on the script tag apply to both loyalty embeds:

AttributeWhat it does
data-cascade-siteNames which of your sites this storefront is, when the site's slug is not obvious from the request.
data-cascade-currencyThe ISO currency code that money in the panel is formatted in. Defaults to USD.
data-cascade-loyalty-visitsSet to false to stop the widget reporting visits.
<script
  src="https://cdn.example.com/cascade/embed.js"
  data-cascade-base-url="https://api.cascade.example.com"
  data-cascade-publishable-key="pk_live_..."
  data-cascade-site="ridgeline-uk"
  data-cascade-currency="GBP"
  data-cascade-customer-email="[email protected]"
  data-cascade-timestamp="1735689600"
  data-cascade-signature="48eb876e..."
></script>

Which sections appear

The panel shows five sections, always in this order: tiers, ways to earn, offers, stamp cards and rewards. Which of them appear is decided in the dashboard rather than in your markup:

  • Ways to earn, offers and stamp cards follow the Sections switches on the program settings page.
  • Tiers appears whenever the program has any.
  • Rewards appears once the program's points are named. A program with no points has no balance and nothing to spend, so the section would be empty.

A section that is switched off is not merely hidden. Its rules are never sent to the browser at all, so switching one off is a real change rather than a cosmetic one.

The order ships with every response, so the panel never hardcodes it and a future rearrangement needs no change to your theme.

Visits

The widget reports a visit when it loads for a signed-in shopper, which is what makes the "Visits the store" trigger work. Visits are deduplicated per browser session, and the allowance you set on the rule (so many times per day, week or month) is enforced by Cascade against your organization's own time zone rather than by the browser.

Set data-cascade-loyalty-visits="false" on the script tag if you would rather report visits yourself, or not at all. It applies to both embeds.

Points on the product page

A one-line badge for a product page: what buying this product is worth in points. It reads your program's purchase earning rules, so the number is the same one the shopper's balance will move by after checkout.

<div data-cascade-loyalty-points data-cascade-price="49.00"></div>
AttributeWhat it does
data-cascade-loyalty-pointsThe badge. Takes no value.
data-cascade-priceWhat the shopper would spend, in whole currency units rather than cents. Required.
data-cascade-currencyThe currency the price is in. Falls back to the script tag's currency.

The price has to come from your storefront, because only your storefront knows what the product costs and which variant is selected. The attribute is read once, when the badge mounts, so a theme that swaps the price on a variant change should re-render the badge (or use the React component, whose price prop it follows).

A signed-in member reads "Earn 250 points with this purchase." and gets their tier multiplier counted; everybody else reads "Join to earn 250 points with this purchase." If the program names its points something else, the badge uses that name.

The badge renders nothing rather than an error when there is nothing honest to say: no program, no live purchase rule, a paused program, a program with no points, or a price too small to earn anything. Rules restricted to particular products are left out too, because a price on its own cannot say whether this product qualifies.

When the page also has the inline loyalty embed on it, the badge is a link to it. The floating launcher keeps its open state to itself, so on a page with only a launcher the badge is a plain sentence.

On Shopify it is the Points on product page block.

A member's share link is your site's base URL with ?ref= and their code on it. The widget reads ref from the landing URL, remembers it, and sends it with the join, which is what turns the link into an attributed referral.

There is nothing to wire up for this beyond having the widget on the landing page. Note that attribution needs the friend to actually join, since a referral has to name a customer.

Cascade's loyalty emails link to your storefront, not to a page we host. Which page is up to you: the Sites card on the loyalty settings page takes a loyalty page URL per site, and leaving it empty points the links at the site's own base URL, where the launcher usually sits.

Every one of those links carries the fragment #cascade-rewards, and both embeds act on it:

  • The launcher opens its panel on arrival, so the shopper sees the program rather than a pill in the corner.
  • The inline embed scrolls itself into view.

There is nothing to configure for this. Have one of the embeds on whichever page you point the links at and it works.

Because the link lands on your storefront, a shopper who is signed in gets their own balance, tier and rewards: your theme signs the customer trio exactly as it does for any other page. A shopper who is not signed in gets the guest catalog and whatever sign-in your store already offers, which is a state they can get themselves out of.

What the widgets call

Every endpoint behind these embeds is documented in the Widget API reference. For the program itself, its rules, tiers and stamp cards, see Setting up loyalty.