Skip to content

Reviews API Reference

Base paths: /api/v1/reviews, /api/v1/tours/:tourId/stops/:stopId/reviewsAuthentication: All endpoints require app-user authentication. Protected routes accept Authorization: Bearer <jwt> or a valid Better Auth session cookie. Feature epic: KUB-181 Tours and Itineraries See also: Tours API

Status

Implemented for the current V1 route surface. Aggregates, Google badge enrichment, and richer moderation metadata remain planned follow-up work.


Overview

The Reviews API provides a polymorphic in-app review system for tour stops backed by canonical reviewable_type values of place and event. Each user may submit one in-app review per entity.

External-source reviews remain a separate enrichment concern and do not overwrite in-app reviews.


Review Status Lifecycle

text
published → flagged
	↓          ↓
 hidden   published | hidden | rejected

In-app reviews are published immediately. A moderation flag moves the review into flagged, where an admin can republish it, hide it, or reject it. Other statuses still exist in the schema for moderation and future ingestion workflows, but they are not the primary public in-app path today.


Source Values

ValueDescription
in_appSubmitted by a Kubuli user
googleImported from Google Places via background enrichment
tripadvisorImported from TripAdvisor (future)
yelpImported from Yelp (future)
manualReserved for future/manual backfill workflows

Endpoints

Create a Review

http
POST /api/v1/tours/:tourId/stops/:stopId/reviews

Auth: Required (tour owner only) Description: Submits an in-app review for the place or event associated with the tour stop.

Request body:

json
{
	"rating": 4,
	"title": "Fresh and affordable",
	"body": "Great produce and friendly vendors. Highly recommended for early mornings.",
	"trip_type": "solo",
	"visit_date": "2025-07-01",
	"is_anonymous": false,
	"aspects": [
		{ "aspect": "value", "rating": 5 },
		{ "aspect": "ambiance", "rating": 4 },
		{ "aspect": "service", "rating": 4 }
	]
}

Both camelCase and snake_case request fields are accepted for tripType / trip_type, visitDate / visit_date, and isAnonymous / is_anonymous.

FieldTypeRequiredNotes
ratingintegerYesMust be an integer between 1 and 5
titlestringNoOptional trimmed string, max 100 characters
bodystringNoOptional trimmed string
trip_typestringNoOptional free-form trip label, max 50 characters
visit_datedateNoISO 8601 date string
is_anonymousbooleanNoOptional boolean
aspectsarrayNoValid entries are kept; invalid ones are ignored

Response 201:

json
{
	"data": {
		"reviewId": "clx..."
	}
}

Error responses:

StatusCodeDescription
400invalid_bodyRequest body must be valid JSON
404not_foundTour or stop was not found for the current user
409review_existsUser has already reviewed this entity with source=in_app
422validation_errorRating is invalid or the payload is otherwise unusable

List Reviews for a Stop

http
GET /api/v1/tours/:tourId/stops/:stopId/reviews

Auth: Required (tour owner only) Description: Returns published, non-deleted reviews for the entity associated with the stop.

Query parameters:

ParameterTypeDefaultNotes
limitinteger20Max 100
cursorstringOpaque next cursor

Response 200:

json
{
	"data": [
		{
			"id": "clx...",
			"rating": 4,
			"title": "Fresh and affordable",
			"body": "Great produce and friendly vendors.",
			"trip_type": "solo",
			"visit_date": "2025-07-01",
			"aspect_ratings": [],
			"media": []
		}
	],
	"meta": {
		"cursor": null
	}
}

The current handler filters by published status, excludes soft-deleted rows, and uses the stop's resolved entity linkage. Review aggregates and Google badge enrichment are planned follow-up work and are not yet present in the live list response.


Vote on a Review

http
POST /api/v1/reviews/:reviewId/votes

Auth: Required Description: Records a helpful or not_helpful vote for a published review.

Request body:

json
{
	"vote_type": "helpful"
}

The canonical public path /api/v1/reviews/:reviewId/votes accepts both voteType and vote_type.

The legacy compatibility alias /api/v1/tours/reviews/:reviewId/votes is also still accepted and now accepts both voteType and vote_type.

FieldTypeRequiredNotes
voteType or vote_typestringYeshelpful or not_helpful

Response 200:

json
{
	"data": {
		"reviewId": "clx...",
		"voteType": "helpful"
	}
}

Error responses:

StatusCodeDescription
404not_foundReview does not exist or is not published
400invalid_bodyRequest body must be valid JSON
422validation_errorVote payload is invalid
429rate_limitedToo many write actions for the current user

Flag a Review for Moderation

http
POST /api/v1/reviews/:reviewId/flag

Auth: Required Description: Flags a published review for admin moderation.

The canonical public path is /api/v1/reviews/:reviewId/flag. The legacy compatibility alias /api/v1/tours/reviews/:reviewId/flag is also still accepted.

Flag submissions share the same per-user write rate limit as votes.

Request body:

json
{
	"reason": "spam",
	"notes": "Looks fake."
}
FieldTypeRequiredNotes
reasonstringYesAny non-empty string
notesstringNoOptional free text

Response 201:

json
{
	"data": {
		"flagId": "clx...",
		"reviewId": "clx..."
	}
}

Error responses:

StatusCodeDescription
404not_foundReview does not exist or is not visible
400invalid_bodyRequest body must be valid JSON
422validation_errorReason is blank
429rate_limitedToo many write actions for the current user

Review Aggregates (Planned)

Denormalized review aggregates and richer per-stop review summaries are planned follow-up work. They are not yet included in the live review list or tour preview responses.


External Enrichment (Google Badge, Planned)

External review enrichment remains planned. When added, Google-sourced reviews will remain separate from in-app reviews and will surface as a badge or summary rather than overwriting user-submitted content.


Admin Moderation

Review moderation (publish, hide, reject) is available via the admin CMS (apps/admin). The current CMS does not create manual reviews, and there is no public response-writing endpoint in the current V1 API.


Common Error Responses

json
{
	"error": {
		"code": "not_found",
		"message": "Not found",
		"status": 404
	}
}
StatusMeaning
401Unauthorized — missing or invalid bearer token or session cookie
404Not found
409Conflict — duplicate in-app review
422Unprocessable entity — validation failure
429Rate limit exceeded

Built with VitePress