Supercommerce API Docs
Store API

Reviews Module — Storefront

HTTP surface for product reviews on the storefront — anonymous list and aggregate reads of approved reviews, plus authenticated submission. Status filters are hardcoded…

HTTP surface for product reviews on the storefront — anonymous list and aggregate reads of approved reviews, plus authenticated submission. Status filters are hardcoded server-side so a malicious client can't ask for pending, rejected, or spam rows.

Source: api-modules/reviews/src/controllers/store-reviews.controller.ts.

Admin moderation, vendor visibility, and the verified-purchase signal computation live in sibling docs.


Conventions

Authentication

EndpointAuth
GET /store/products/:productId/reviewsnone (public)
GET /store/products/:productId/reviews/aggregatenone (public)
POST /store/products/:productId/reviewsrequired (customer session)

The submission endpoint takes the author identity from the session — userId is never accepted from the body.

Response envelope

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* on the list endpoint */ }
}

The list endpoint uses the ApiWrappedPaginatedResponse shape with metadata: { total, items, perPage, currentPage, lastPage }.

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Visibility

Storefront list and aggregate endpoints only ever return rows that are all of:

  • status = "approved",
  • deletedAt IS NULL,
  • isSpam = false.

These filters are hardcoded inside ReviewsService — clients cannot widen them via query params.


Domain types

ReviewImageResponse

type ReviewImageResponse = {
  id: string;
  url: string;
  sortOrder: number;                  // 0-based; ascending
};

ReviewResponse

type ReviewStatus = "pending" | "approved" | "rejected";

type ReviewResponse = {
  id: string;
  productId: string;
  userId: string | null;              // null for admin-created reviews
  authorFirstName: string | null;
  authorLastName: string | null;
  title: string | null;
  content: string;
  stars: 1 | 2 | 3 | 4 | 5;
  recommended: boolean | null;
  isVerifiedPurchase: boolean;
  isSpam: boolean;                    // always false on storefront reads
  status: ReviewStatus;               // always "approved" on storefront reads
  approvedAt: string | null;
  approvedBy: string | null;
  rejectedAt: string | null;
  rejectedBy: string | null;
  createdBy: string | null;
  createdAt: string;                  // ISO
  updatedAt: string;
  deletedAt: string | null;           // always null on storefront reads
  images: ReviewImageResponse[];
};

ReviewAggregateResponse

type ReviewAggregateResponse = {
  productId: string;
  avg: number;                        // average over approved reviews (0..5)
  count: number;                      // count of approved reviews
  distribution: {
    "1": number;
    "2": number;
    "3": number;
    "4": number;
    "5": number;
  };
};

Endpoints

Base path: /store/products/:productId/reviews.

GET /store/products/:productId/reviews — List approved reviews

Paginated list of approved, non-deleted, non-spam reviews for a product.

Path params

NameNotes
productIdProduct UUID

Query

NameTypeDefaultNotes
pageint1>= 1
limitint(module default)1..MAX_REVIEW_PAGE_SIZE
orderByenum?newest / oldest / stars-desc / stars-asc

Response 200 — paginated ReviewResponse[].

{
  "data": [
    {
      "id": "01J9...",
      "productId": "01J9...",
      "userId": "01J9...",
      "authorFirstName": "Ada",
      "authorLastName": "L.",
      "title": "Loved it",
      "content": "Works exactly as described.",
      "stars": 5,
      "recommended": true,
      "isVerifiedPurchase": true,
      "isSpam": false,
      "status": "approved",
      "approvedAt": "2026-05-01T08:00:00.000Z",
      "approvedBy": "01J9...",
      "rejectedAt": null,
      "rejectedBy": null,
      "createdBy": null,
      "createdAt": "2026-04-30T10:00:00.000Z",
      "updatedAt": "2026-05-01T08:00:00.000Z",
      "deletedAt": null,
      "images": []
    }
  ],
  "metadata": {
    "total": 47,
    "items": 1,
    "perPage": 20,
    "currentPage": 1,
    "lastPage": 3
  }
}

GET /store/products/:productId/reviews/aggregate — Average + star distribution

Aggregate over the same approved-only set. Useful for the product card / hero rating summary.

Response 200ReviewAggregateResponse.

{
  "data": {
    "productId": "01J9...",
    "avg": 4.62,
    "count": 47,
    "distribution": { "1": 1, "2": 0, "3": 3, "4": 10, "5": 33 }
  }
}

POST /store/products/:productId/reviews — Submit a review

Authenticated. Submits a review for the product. The author is the session user; userId is never read from the body.

The service may set isVerifiedPurchase=true automatically based on the customer's order history for this product (the heuristic lives inside ReviewsService). The newly-created row may not be visible via the public list immediately — submissions land in status: "pending" by default and become visible only after moderation.

Body

{
  "title": "Loved it",                          // optional, trimmed, 1..200 chars
  "content": "Works exactly as described.",     // required, trimmed, 1..5000 chars
  "stars": 5,                                    // integer 1..5
  "recommended": true,                           // optional boolean
  "images": [                                    // optional
    { "url": "https://cdn.example/r/1.jpg", "sortOrder": 0 }
  ]
}
FieldConstraints
titletrimmed, 1..200 chars, nullable, optional
contenttrimmed, 1..5000 chars, required
starsinteger 1..5
recommendedboolean, nullable, optional
images[].urlmust be a valid URL
images[].sortOrderinteger >= 0

Response 201ReviewResponse (the newly-created row, typically with status: "pending").

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
401UNAUTHORIZEDNo customer session

  • catalog — products referenced by productId come from this module.
  • order — used by ReviewsService to compute the isVerifiedPurchase signal.

On this page