Supercommerce API Docs
Admin API

Reviews Module — Admin

HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create…

HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create on behalf of a customer, soft-delete + restore.

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

A review attaches to a product and is owned by either a real Better-Auth user (userId) or an admin-entered author name pair (authorFirstName + authorLastName) — XOR enforced at zod and at the DB CHECK constraint.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching review:* permission.

Endpoint groupPermission
GET /admin/reviews, GET /admin/reviews/:idreview: read
POST /admin/reviewsreview: create
PATCH /admin/reviews/:id, POST /admin/reviews/:id/restorereview: update
POST /admin/reviews/:id/approvereview: approve
POST /admin/reviews/:id/rejectreview: reject
POST /admin/reviews/:id/mark-spam, POST /admin/reviews/:id/unmark-spamreview: mark-spam
DELETE /admin/reviews/:idreview: delete

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

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

Lifecycle

status is one of pending / approved / rejected. Approve / reject endpoints stamp approvedAt / approvedBy / rejectedAt / rejectedBy and emit events. isSpam is an orthogonal flag (a spam review can also be approved/rejected/pending). Soft-delete sets deletedAt; restore clears it.


Domain types

ReviewResponse

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

type ReviewImageResponse = {
  id: string;
  reviewId: string;
  url: string;
  sortOrder: number;
};

type ReviewResponse = {
  id: string;
  productId: string;
  userId: string | null;
  author: { id: string; name: string; email: string } | null; // the customer account that wrote the review; null for admin-entered reviews
  authorFirstName: string | null;
  authorLastName: string | null;
  title: string | null;
  content: string;                 // 1..5000, trimmed
  stars: number;                   // 1..5
  recommended: boolean | null;
  isVerifiedPurchase: boolean;
  isSpam: boolean;
  status: ReviewStatus;
  approvedAt: string | null;
  approvedBy: string | null;       // admin user id
  approver: { id: string; name: string; email: string } | null;
  rejectedAt: string | null;
  rejectedBy: string | null;
  rejecter: { id: string; name: string; email: string } | null;
  createdBy: string | null;        // admin user id when admin-created
  creator: { id: string; name: string; email: string } | null;
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
  images: ReviewImageResponse[];
  vendor: { id: string; name: string; slug: string; logo: string | null } | null;               // owning vendor of the reviewed product (reviewproductvendor); admin-only, null on vendor/store responses
  product: { id: string; title: string; slug: string; image: string | null } | null;             // the reviewed product; admin + vendor responses, null on store
};

vendor is resolved (batched, no N+1) on the admin surface only, so moderators can see and link to the seller a review belongs to — vendor and store responses leave it null. product is resolved the same way on both admin and vendor surfaces (vendor responses omit vendor since the caller already is the owning vendor); store responses leave it null.


Endpoints

Base path: /admin/reviews.

GET /admin/reviews — List reviews

Required permission: review: read. Full moderator filter set including spam and deleted.

Query

NameTypeDefaultNotes
productIdstring?Filter to one product
vendorIdstring?Filter to reviews of products owned by one vendor
userIdstring?Filter to one author
statusReviewStatus?
isSpam"true" | "false"?Coerced to boolean
includeDeleted"true" | "false"?Coerced — when true, soft-deleted rows are returned
pageint1
limitintDEFAULT_REVIEW_PAGE_SIZEBounded by MAX_REVIEW_PAGE_SIZE
orderBy"newest" | "oldest" | "stars-desc" | "stars-asc"?

Response 200 — paginated envelope of ReviewResponse[].


GET /admin/reviews/:id — Get one review

Required permission: review: read. Returns the review with its images.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/reviews — Create a review on behalf of a customer

Required permission: review: create. Either userId (attach to a real customer) or both authorFirstName + authorLastName (anonymous customer the admin is recording for) — XOR.

Body

{
  "productId": "01J9...",
  "userId": "01J9...",                       // OR omit and supply authorFirstName/authorLastName
  "title": "Great product",                  // optional, trimmed, 1..200
  "content": "Loved it",                      // trimmed, 1..5000
  "stars": 5,                                 // 1..5
  "recommended": true,                        // optional
  "isVerifiedPurchase": false,                // optional
  "status": "approved",                       // optional; defaults to service default
  "images": [
    { "url": "https://cdn.example/r/1.jpg", "sortOrder": 0 }
  ]
}
FieldTypeConstraints
productIdstringRequired
userIdstring?XOR with author name pair
authorFirstName / authorLastNamestring?Trimmed, 1..100. Required together when userId absent
titlestring?Trimmed, 1..200
contentstringTrimmed, 1..5000
starsint1..5
recommendedboolean? | null
isVerifiedPurchaseboolean?
statusReviewStatus?
images[]Array<{url, sortOrder}>?url: valid URL; sortOrder >= 0

Response 201ReviewResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (including the XOR rule)

PATCH /admin/reviews/:id — Edit any field

Required permission: review: update. PATCH semantics — only supplied keys are applied. When images is sent, the entire image set is replaced wholesale; omit it to leave images untouched. Retroactively attaching a userId (or swapping in author names) is supported and validated against the same XOR rule.

Body

{
  "title": "Updated title",
  "content": "Edited content...",
  "stars": 4,
  "recommended": true,
  "authorFirstName": null,
  "authorLastName": null,
  "images": [
    { "url": "https://cdn.example/r/1.jpg", "sortOrder": 0 }
  ]
}

Response 200 — updated ReviewResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDUnknown id

POST /admin/reviews/:id/approve — Approve

Required permission: review: approve. Stamps approvedAt / approvedBy = current admin user id. Resets rejectedAt / rejectedBy to null.

Response 200ReviewResponse.


POST /admin/reviews/:id/reject — Reject

Required permission: review: reject. Stamps rejectedAt / rejectedBy.

Response 200ReviewResponse.


POST /admin/reviews/:id/mark-spam — Mark as spam

Required permission: review: mark-spam. Sets isSpam = true. Does not change status.

Response 200ReviewResponse.


POST /admin/reviews/:id/unmark-spam — Clear spam flag

Required permission: review: mark-spam. Sets isSpam = false.

Response 200ReviewResponse.


DELETE /admin/reviews/:id — Soft-delete

Required permission: review: delete. Stamps deletedAt. Hidden from default lists; visible when includeDeleted=true.

Response 200 — deleted ReviewResponse.


POST /admin/reviews/:id/restore — Restore

Required permission: review: update. Clears deletedAt.

Response 200 — restored ReviewResponse.


  • admin-rbac — gates every endpoint via review:*. See admin-rbac.md.
  • catalogproductId references a non-deleted product.
  • customer — admin-create can attach to a customer via the customer picker. See customer.md.
  • authuserId, approvedBy, rejectedBy, createdBy are Better-Auth user ids.

On this page