Reviews Module — Vendor surface
Vendor-facing HTTP surface for moderating reviews on the vendor's own products — listing, editing content, approving/rejecting, marking/unmarking spam, and soft-deleting. List is…
Vendor-facing HTTP surface for moderating reviews on the vendor's own products — listing, editing content, approving/rejecting, marking/unmarking spam, and soft-deleting. List is always allowed; every write action is gated by a corresponding admin.reviews.allow_vendor_* platform setting. Stars and author identity are intentionally not editable by vendors (rating-integrity / honest-attribution policy).
Source:
api-modules/reviews/src/controllers/vendor-reviews.controller.ts.
Conventions
Authentication
All endpoints require a Better-Auth bearer session with an active vendor.
Authorization: Bearer <session-token>The active vendor is resolved via resolveActiveVendorId(session). Vendor scope is always read from the session — never accepted from the client.
Tenant scoping
Every read and write scopes to reviews whose product.vendorId matches the active vendor. Cross-vendor review ids return 404 Not Found (no leak of foreign ids). Soft-deleted reviews are hidden from the vendor list entirely (no includeDeleted toggle — that's an admin concern).
Spam reviews are also hidden from the vendor list unless the platform has set admin.reviews.allow_vendor_show_spam. The rationale: an admin's spam classification shouldn't re-surface in the vendor's UI by default — abusive content the platform decided to suppress stays suppressed for the vendor too.
Platform-gated writes
Every mutating endpoint is gated by a setting in the admin.reviews.* namespace (consult settings.md). When the relevant flag is off the endpoint returns 403 Forbidden with the message "Vendor <action> disabled by platform configuration". The flags are enforced inside ReviewsService — the controller stays declarative.
| Endpoint | Setting |
|---|---|
PATCH /vendor/reviews/:id | admin.reviews.allow_vendor_edit |
POST /vendor/reviews/:id/approve | admin.reviews.allow_vendor_approve |
POST /vendor/reviews/:id/reject | admin.reviews.allow_vendor_reject |
POST /vendor/reviews/:id/mark-spam, /unmark-spam | admin.reviews.allow_vendor_mark_spam |
DELETE /vendor/reviews/:id | admin.reviews.allow_vendor_delete |
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (vendor action disabled by platform) |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR |
Domain types
ReviewStatus
type ReviewStatus = "pending" | "approved" | "rejected";ReviewResponse
type ReviewImageResponse = {
id: 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;
stars: number; // 1..5 — not editable by vendors
recommended: boolean | null;
isVerifiedPurchase: boolean;
isSpam: boolean;
status: ReviewStatus;
approvedAt: string | null; // ISO
approvedBy: string | null;
rejectedAt: string | null;
rejectedBy: string | null;
createdBy: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
images: ReviewImageResponse[];
};Reviews
Base path: /vendor/reviews. :id is review.id.
GET /vendor/reviews — List reviews on my products
Returns reviews whose product.vendorId matches the active vendor. Soft-deleted rows are excluded; spam is excluded unless admin.reviews.allow_vendor_show_spam is on.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
productId | string? | — | Filter to one product |
status | "pending" | "approved" | "rejected"? | — | Filter by moderation status |
page | int | 1 | >= 1 |
limit | int | platform default | >= 1, capped by MAX_REVIEW_PAGE_SIZE |
orderBy | "newest" | "oldest" | "stars-desc" | "stars-asc"? | newest | Sort order |
Response 200 — paginated envelope of ReviewResponse.
{
"data": [ /* ReviewResponse[] */ ],
"metadata": {
"total": 124,
"items": 20,
"perPage": 20,
"currentPage": 1,
"lastPage": 7
}
}PATCH /vendor/reviews/:id — Edit content fields
Editable fields are intentionally narrow: title, content, recommended. Stars and author identity are not editable by vendors.
Body
{
"title": "Updated title", // nullable; 1..200 chars trimmed
"content": "Updated content body", // optional; 1..5000 chars trimmed
"recommended": true // nullable
}| Field | Type | Constraints |
|---|---|---|
title | string | null? | Trimmed; 1..200 chars when set |
content | string? | Trimmed; 1..5000 chars |
recommended | boolean | null? |
Response 200 — updated ReviewResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_edit is off |
| 400 | VALIDATION_ERROR | Body fails zod |
POST /vendor/reviews/:id/approve — Approve
Transitions status → "approved" and stamps approvedAt / approvedBy.
Body — empty.
Response 200 — updated ReviewResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_approve is off |
POST /vendor/reviews/:id/reject — Reject
Transitions status → "rejected" and stamps rejectedAt / rejectedBy.
Body — empty.
Response 200 — updated ReviewResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_reject is off |
POST /vendor/reviews/:id/mark-spam — Flag as spam
Sets isSpam = true. The review remains in the underlying table but is hidden from the storefront and (by default) from the vendor list.
Body — empty.
Response 200 — updated ReviewResponse with isSpam: true.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_mark_spam is off |
POST /vendor/reviews/:id/unmark-spam — Clear spam flag
Sets isSpam = false.
Body — empty.
Response 200 — updated ReviewResponse with isSpam: false.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_mark_spam is off |
DELETE /vendor/reviews/:id — Soft delete
Sets deletedAt. Soft-deleted reviews disappear from the vendor list and the storefront. Admins retain visibility (and may restore via the admin surface).
Response 200 — updated ReviewResponse (now carrying deletedAt).
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Review not on a product owned by active vendor |
| 403 | FORBIDDEN | admin.reviews.allow_vendor_delete is off |
Related modules
settings—admin.reviews.allow_vendor_*toggles live here. Seesettings.md.catalog— review-to-product ownership goes throughproduct.vendorId.storage— review images are stored in object storage; only the public URL surfaces on the API.
Product Attribute Module — Vendor surface
Vendor-facing read endpoints for platform-defined product attributes and attribute groups that the vendor product form binds to. Attributes themselves (definitions, codes, types,…
Settings Module — Vendor surface
Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax/payouts, store for…