Review Groups API

Last updated: August 20, 2026

Combine reviews from multiple products (variants, packs, bundles) into a shared display pool on storefront product pages. Useful for fashion brands with size/color variants, bundles that share underlying products, or accessory lines that share reviews across colorways.

Entitlement gating — all endpoints require the :review_groups entitlement. Without it, calls return 403 entitlement_required, including calls from stores on the legacy Starter plan. Current plans (Unlimited, Studio) include it. On legacy plans (no longer sold), it is available on Growth and above, and on legacy Pro and Enterprise. A store with no active subscription — never subscribed, or cancelled — is in the free state and holds none of the gated entitlements: entitlement-gated endpoints return 403 entitlement_required, and every LLM-backed endpoint returns 402 subscription_required.

Limits — 500 members per group (cap enforced server-side; 422 too_many_members above). A product can be an active member of at most one group at a time (single-display-membership; 409 membership_conflict on overlap, resolve via the /move endpoint).

For the storefront-rendering side (per-product summary, shop-level group_summary_*, and the display_group_id pointer), see the metafields contract.

List Groups

GET /api/v1/review-groups

Response (200)

{
  "data": [
    {
      "id": 42,
      "name": "Summer Tee variants",
      "display_name": "Summer Tee",
      "source": "manual",
      "shopify_collection_gid": null,
      "member_count": 8,
      "inserted_at": "2026-05-08T10:00:00Z",
      "updated_at": "2026-05-08T10:00:00Z"
    }
  ]
}

Get Single Group

GET /api/v1/review-groups/:id

Returns the group plus its full member product ID list and a parallel member_products array with resolved product titles. Returns 404 not_found if the group does not belong to the authenticated store (cross-tenant IDOR safeguard).

member_products[].title is null when the product hasn't been synced to BetterReviews yet (e.g. just-deleted Shopify product or sync lag).

Response (200)

{
  "data": {
    "id": 42,
    "name": "Summer Tee variants",
    "display_name": "Summer Tee",
    "source": "manual",
    "shopify_collection_gid": null,
    "member_count": 3,
    "member_product_ids": ["1001", "1002", "1003"],
    "member_products": [
      {"external_product_id": "1001", "title": "Summer Tee — Navy"},
      {"external_product_id": "1002", "title": "Summer Tee — Sand"},
      {"external_product_id": "1003", "title": null}
    ],
    "inserted_at": "2026-05-08T10:00:00Z",
    "updated_at": "2026-05-08T10:00:00Z"
  }
}

Create Group

POST /api/v1/review-groups

Request

{
  "name": "Summer Tee variants",
  "display_name": "Summer Tee",
  "product_ids": ["1001", "1002", "1003"]
}
FieldTypeDescription
namestring (required)Internal-only label (merchant sees, shoppers don't). Capped at 100 characters.
display_namestring (optional)Customer-facing label rendered in the storefront disclosure subtitle. Validation rejects <, >, {{, }}, ${, and backticks (XSS / template-injection defense). Capped at 100 characters.
product_idsstring[] (optional)Bare numeric Shopify product IDs to add as initial members. The shopify- prefix is stripped server-side. Cap 500.

Responses

  • 201{"data": {...group...}}
  • 409 membership_conflict — one or more product_ids are already displaying from another group. Body includes a conflicting array with external_product_id and the existing group_id. Resolve via the /move endpoint.
  • 422 too_many_members — more than 500 product IDs supplied.
  • 422 validation_error — invalid display_name (XSS chars / over cap) or other field-level validation failure. Body includes errors map.
  • 503 feature_disabled — global kill-switch flipped (REVIEW_GROUPS_ENABLED=false). Reads remain available.

Update Group

PATCH /api/v1/review-groups/:id

{
  "name": "New name",
  "display_name": "New label"
}

Responses

  • 200{"data": {...group...}}
  • 404 not_found — group doesn't belong to authenticated store.
  • 422 validation_error — invalid display_name.
  • 503 feature_disabled — kill-switch flipped.

Delete Group

DELETE /api/v1/review-groups/:id

Soft-deletes the group plus all memberships. Member products revert to per-product reviews on the storefront within ~60 seconds (metafield resync). Soft-deleted rows are hard-deleted by the daily tombstone-sweep worker after 90 days.

Responses

  • 200{"data": {"deleted": true}}
  • 404 not_found — group doesn't belong to authenticated store.
  • 503 feature_disabled — kill-switch flipped.

Add Member

POST /api/v1/review-groups/:id/members

{ "external_product_id": "1004" }

Responses

  • 200{"data": {"added": true}}
  • 404 not_found — group doesn't belong to authenticated store.
  • 409 membership_conflict — product is already an active member of another group. Use the /move endpoint.
  • 422 too_many_members — group is already at the 500-member cap.
  • 503 feature_disabled — kill-switch flipped.

Remove Member

DELETE /api/v1/review-groups/:id/members/:external_product_id

Soft-removes the membership. Tombstone-swept after 90 days.

Responses

  • 200{"data": {"removed": true}}
  • 404 not_found — group or membership not found in this store.
  • 503 feature_disabled — kill-switch flipped.

Move Membership (Conflict Resolution)

POST /api/v1/review-groups/move

Move a product from one group's display to another's. Requires expected_current_group_id for optimistic-concurrency safety; if the product's actual current group has changed since the merchant loaded the page, the request returns 409 stale (refresh and retry).

Request

{
  "external_product_id": "1001",
  "expected_current_group_id": 42,
  "target_group_id": 99
}

Responses

  • 200{"data": {"moved": true}}
  • 409 staleexpected_current_group_id doesn't match server state; refresh and retry.
  • 404 not_found — target group doesn't belong to authenticated store.
  • 422 too_many_members — target group is at the 500-member cap.
  • 422 no_active_membership — product has no active membership to move.
  • 400 same_groupexpected_current_group_id == target_group_id.
  • 400 missing_required_params — request omitted one of external_product_id / expected_current_group_id / target_group_id.
  • 503 feature_disabled — kill-switch flipped.

Storefront metafields written by groups

When a product joins a group:

  • product.metafields.betterreviews.summary stays per-product (unchanged for headless integrations).
  • product.metafields.betterreviews.display_group_id is written as a small pointer payload.
  • shop.metafields.betterreviews.group_summary_<store_id>_<group_id> is written with the aggregate.

JSON-LD AggregateRating schema stays product-only on every PDP — Google's product reviews policy prohibits cross-product aggregation in product schema. See aggregation disclosure for FTC framing and the metafields contract for the full schema.