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;
  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
  rejectedAt: string | null;
  rejectedBy: string | null;
  createdBy: string | null;        // admin user id when admin-created
  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
  images: ReviewImageResponse[];
};

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