Shopify Metafields Contract

Last updated: May 8, 2026

BetterReviews syncs review data to Shopify metafields under the betterreviews namespace. The storefront widget reads them in Liquid; headless storefronts (Hydrogen, custom React, etc.) can read them directly via the Storefront API. This page documents what's stable, what may change, and what to escape before rendering.

Per-product metafields (product.metafields.betterreviews.*)

summarystable contract

This product's own review aggregates. Always per-product, never group-aggregated, even when the product is in a Review Group. Your headless renderer can rely on this metafield reflecting the product the page is about, full stop.

{
  "average_rating": 4.7,
  "total_count": 23,
  "rating_counts": {"1": 0, "2": 1, "3": 2, "4": 5, "5": 15},
  "with_media_count": 8,
  "total_media_count": 12,
  "verified_count": 18,
  "qa_count_visible": 7,
  "updated_at": "2026-05-08T10:00:00Z"
}

qa_count_visible is the number of approved questions that have at least one approved + published answer (the same predicate that controls visibility in the storefront Q&A list). The first-party Liquid widget guards on != blank when rendering the Questions tab badge — products synced before the field was rolled out have nil, and you should treat nil and 0 as different states (nil = "not yet computed," 0 = "no qualifying Q&A").

top_reviewsstable shape, contents may change

Pre-rendered top reviews for SSR. Each row carries display_product_id — the bare numeric Shopify product ID this review was submitted for. When a product is in a Review Group and you render group-aggregated content, compare display_product_id to the current PDP's product id and add cross-product attribution ("Reviewed: Red Shirt") for non-matching rows. FTC compliance lives at this seam — see aggregation-disclosure.

display_group_idmay evolve

Pointer to a shop-level group aggregate. Present only when the product is an active member of a Review Group AND the merchant is on Pro or Enterprise. Absent otherwise.

{
  "group_id": 42,
  "store_id": 89712787790,
  "cache_version": 17
}

cache_version is a cache-buster you can ignore — its only purpose is to differentiate snapshots when invalidating CDN caches.

intelligence, media_gallery, widget_token, submission_token

See your in-app guide. widget_token and submission_token are HMAC-signed JWTs scoped to (store_id, product_id); treat them as opaque.

Shop-level metafields (shop.metafields.betterreviews.*)

group_summary_<store_id>_<group_id>may evolve

The shop-level aggregate for a Review Group. Read this when a product carries a display_group_id pointer and you want to show group-aggregated stars + reviews on the PDP.

{
  "group_id": 42,
  "store_id": 89712787790,
  "display_name": "Summer Tee",
  "member_count": 8,
  "variant_count": 8,
  "summary": { /* same shape as per-product summary, group-aggregated */ },
  "top_reviews": { "reviews": [ /* up to 10 rows, each with display_product_id */ ] },
  "media_gallery": [ /* up to 12 photos across the group */ ],
  "intelligence": { /* group-aggregated value drivers + quotes */ },
  "cache_version": 17
}

Stability guarantees

  • Stable contract: we will not change the semantics or required keys of product.metafields.betterreviews.summary without a versioned migration path. Headless integrations against this metafield will keep working when Review Groups ships and onwards.
  • May evolve: we may add fields to group_summary_*, display_group_id, top_reviews, media_gallery, and intelligence. New fields are additive — your renderer should ignore unknown keys, not throw.
  • Removed metafields: we will publish a deprecation notice in this doc at least 90 days before removing any metafield key, including a deletion-write to clear the value from Shopify.

Headless integrations: what to escape

The merchant-set display_name on group_summary_* is server-side validated to reject <, >, {{, }}, ${, and backticks, and capped at 100 characters. Always still HTML-escape it on render — defence-in-depth, and the same string flows through your view templates.

FTC disclosure when rendering grouped reviews

When you render group-aggregated counts on a product page, your storefront — not BetterReviews — is the entity making a representation to the consumer. The FTC's review and endorsement guidance requires you to surface that the displayed count covers more than just the current product. The shop-level group_summary payload includes member_count and an optional merchant-set display_name so you can render the disclosure "Based on 47 reviews across 8 products" or "Based on 47 reviews across the Summer Tee collection". See aggregation disclosure for full guidance.

JSON-LD aggregateRating

When emitting schema.org/Product markup on a PDP, aggregateRating must reflect the product's own reviews only, never group-aggregated counts. Google's product reviews policy explicitly prohibits cross-product aggregation in product schema. BetterReviews's first-party Liquid widget renders product-only schema even when the visible widget shows group data. If you render schema yourself, use summary, not group_summary.