Review Groups API
Last updated: May 8, 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.
Plan gating — all endpoints require the :review_groups entitlement. Calls from the Reviews tier return 403 entitlement_required. Available on Pro and Enterprise plans.
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"]
} | Field | Type | Description |
|---|---|---|
name | string (required) | Internal-only label (merchant sees, shoppers don't). Capped at 100 characters. |
display_name | string (optional) | Customer-facing label rendered in the storefront disclosure subtitle. Validation rejects <, >, {{, }}, ${, and backticks (XSS / template-injection defense). Capped at 100 characters. |
product_ids | string[] (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 moreproduct_idsare already displaying from another group. Body includes aconflictingarray withexternal_product_idand the existinggroup_id. Resolve via the/moveendpoint.422 too_many_members— more than 500 product IDs supplied.422 validation_error— invaliddisplay_name(XSS chars / over cap) or other field-level validation failure. Body includeserrorsmap.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— invaliddisplay_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/moveendpoint.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 stale—expected_current_group_iddoesn'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_group—expected_current_group_id == target_group_id.400 missing_required_params— request omitted one ofexternal_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.summarystays per-product (unchanged for headless integrations).product.metafields.betterreviews.display_group_idis 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.