Reviews
This page is the API reference for developers reading or writing reviews.
The Reviews app holds the product reviews a shop collects. A review is one customer's stars, title and text about one product. A review written through a review link is a verified purchase: the app checks that the link's order is completed and holds the product. A review submitted through a link waits for moderation, unless the app setting auto_publish_min_rating (default 0, off) lets ratings at or above it go live at once. A review recorded by hand or imported through POST /v1/reviews/reviews is stored as pending unless the caller sends state: published.
The app also keeps the figures of each product (count, average and star spread of its published reviews, per market and for all markets together) and answers them for the product page, the mail product blocks and the structured data and feeds. The app sends no mail itself: a review link is handed out through a journey.
Objects
| Object | What it holds |
|---|---|
| Review | The product, the order and contact it came from, the author's name, rating (1 to 5), title, body, verified_purchase, state (pending, published or rejected), the merchant reply (stored as merchant_reply), and the six trash fields. |
| Review request | One link per completed order and contact: the products it names, its token, expires_at, state (issued, used or expired), journey_ref, market and locale. The token is never part of a list answer. |
| Aggregate | The figures of one product in one market: count, average, the distribution of stars from 1 to 5, and verified_count. Market * holds the figures for all markets together. |
A review is one per purchase. POST /v1/reviews/reviews refuses a second review of the same product from the same order and contact with 409 (already_reviewed), when both order_id and contact_id are sent. Through a link, a duplicate entry comes back in rejected with already_reviewed, or the call is 400 with nothing_accepted when every entry is refused. A review in the trash counts nowhere and leaves the product page and the figures until it is restored.
Base path: https://api.revenexx.com/v1/reviews.
Scopes
A key calls these routes with the capability key of each route. The App's manifest names one per operation, in the form reviews.<group>.<operation>, and the API keys page explains that a key holds capability keys or a namespace such as reviews.*. The scope names are listed in the tables below. A call that needs a scope the key does not hold is refused with 403.
The market header
The app's routes do not use X-Revenexx-Market: its code never looks at the header. A review's market comes from the body market of POST /v1/reviews/reviews. A review without one has no market, and it shows only in the all-markets figures (market *), not in any per-market figure. GET /v1/reviews/products/{product_id} with ?market=DE returns only the reviews assigned to DE; without market it returns every published review. Review requests take their market from the body market, or else from the order's metadata.market.
Reviews
| Operation | Scope | What it does |
|---|---|---|
GET /v1/reviews/reviews | reviews.reviews.list | Lists the reviews of the tenant in every state. The trash is left out unless trashed asks for it. |
POST /v1/reviews/reviews | reviews.reviews.create | Records a review by hand or from an import. |
GET /v1/reviews/reviews/{id} | reviews.reviews.get | Reads one review. |
DELETE /v1/reviews/reviews/{id} | reviews.reviews.delete | Moves a review into the trash. A second DELETE on the same review deletes it for good. |
POST /v1/reviews/reviews/{id}/restore | reviews.reviews.restore | Takes a review back out of the trash. |
POST /v1/reviews/reviews/trash/empty | reviews.reviews.empty-trash | Deletes every review in the trash for good. |
POST /v1/reviews/reviews/trash/purge | reviews.reviews.purge | Deletes the reviews that have been in the trash longer than the tenant's trash retention. It is the nightly purge, callable by hand. |
GET /v1/reviews/reviews/{id}/stars | reviews.aggregates.review_stars | Reads the stars of the review's product as bar rows, five down to one, across all markets. |
GET /v1/reviews/reviewsfilters:state(for the moderation queue usestate=pending),product_id,contact_id,rating,verified_purchase=true,trashed(exclude,onlyorinclude), and further plain columns.limittakes 1 to 200 (default 50), withoffsetandorder.POST /v1/reviews/reviewsbody: requiredproduct_idandrating. Optional:title,body,sku,product_name,order_id,contact_id,author_first_name,author_last_name,locale,market,source(adminorimport; omitted ornullmeansadmin, an empty string issource_invalid),state(pendingby default, orpublished),published_at,external_id,external_refs,source_synced_atandsource_data.titleis at most 200 characters andbodyat most 5000. The purchase is verified only whenorder_idandcontact_idname a completed order of that contact that holds the product.POST /v1/reviews/reviewserrors:400withproduct_required,rating_invalid,too_long,source_invalidorstate_invalid(details.fieldnames the field).409withalready_reviewed, orin_trashwhen the review of that purchase is in the trash.DELETEerrors:404when no review has the id. A secondDELETEon a review that is already in the trash deletes it for good.POST .../restoreerror:409withnot_in_trash.POST .../publish,.../rejectand.../replyon a trashed review:409within_trash. Restore it first.- Trash empty and purge: a call from a storefront contact is
403withpurge_not_allowed. A review that something still blocks stays in the trash and is named with its reason. A purge is skipped when the trash retention cannot be read, and then nothing is deleted. Each call handles at most 1000 trashed rows; a larger trash needs another call. GET .../stars: the rows are five stars down to one, each with its count and share, thenfooterrows: the average and the review count always, and the verified-purchases row only when at least one review is verified. The labels follow theAccept-Languageheader; German is the default unless English is asked for. No published review gives no rows.
Moderation
| Operation | Scope | What it does |
|---|---|---|
POST /v1/reviews/reviews/{id}/publish | reviews.moderation.publish | Shows the review in the shop and recomputes the product figures. Publishing a published review answers it unchanged. |
POST /v1/reviews/reviews/{id}/reject | reviews.moderation.reject | Rejects a review, or takes a published one down. |
POST /v1/reviews/reviews/{id}/reply | reviews.moderation.reply | Answers a review publicly, or removes the answer. |
rejectbody:reason, required. Without it the answer is400withreason_required.replybody: a body fieldreply, a string of up to 5000 characters. An empty ornullvalue removes the answer. The answer is stored inmerchant_reply. A longer text is400withtoo_long. A rejected review cannot be answered:409withreview_rejected.- Customer text: the app never edits the title or body of a review.
- Errors on all three:
404when no review has the id, and409within_trashon a trashed review.
Review requests
| Operation | Scope | What it does |
|---|---|---|
POST /v1/reviews/requests | reviews.requests.issue | Issues the review link for one completed order and its contact. |
GET /v1/reviews/requests | reviews.requests.list | Lists the issued links, without their tokens. |
GET /v1/reviews/requests/{token} | reviews.requests.form | Reads the products of a link and which of them are reviewed. A storefront renders its form from this. |
POST /v1/reviews/submit/{token} | reviews.requests.submit | Writes the reviews of a link, one per product. |
POST /v1/reviews/requestsbody: requiredorder_idandcontact_id, both UUIDs. Optional:journey_ref,market,localeandproduct_ids. The same order and contact get the same link only while that link isissuedand unexpired. Any other existing link, including ausedone, is replaced by a new one. The link names the order's products that the contact has not reviewed yet.POST /v1/reviews/requestserrors:400withorder_and_contact_required.404withorder_not_found.409withorder_not_completed,contact_not_on_orderornothing_to_review.502withorders_unavailablewhen the orders app does not answer.GET /v1/reviews/requestsfilters:id,order_id,order_number,contact_id,organization_id,state,journey_ref,market,locale,reviewed_countandissued_count. A token cannot be filtered on.GET /v1/reviews/requests/{token}errors:404withunknown_token.410withrequest_expiredwhen the link has expired.POST /v1/reviews/submit/{token}body:reviews, an array with one entry per product. Each entry hasproduct_idandrating(required) andtitleandbody(optional). The app writes the accepted entries and names the rest inrejected. Reviews wait for moderation unless the app settingauto_publish_min_ratingis set and the rating is at or above it.POST /v1/reviews/submit/{token}errors:400withreviews_required, ornothing_acceptedwith each entry's reason:product_not_in_request,already_reviewed,rating_invalidortoo_long.404withunknown_token.410withrequest_expiredwhen the link has expired.
Products and figures
| Operation | Scope | What it does |
|---|---|---|
GET /v1/reviews/products/{product_id} | reviews.products.get | Reads the published reviews and the figures of one product. |
GET /v1/reviews/aggregates | reviews.aggregates.list | Reads the figures of many products. |
POST /v1/reviews/aggregates/rebuild | reviews.aggregates.rebuild | Recomputes the figures from the published reviews. |
GET /v1/reviews/products/{product_id}:sortisnewest,oldest,rating_descorrating_asc. Alsomarket,limit(default 10, at most 100) andoffset.400when the id is not a product id.GET /v1/reviews/aggregates: withproduct_ids(comma-separated, at most 200) it answers one entry per product, zero for a product nobody reviewed. Without it, the stored figures are paged. Filters:market(default*, the all-markets figures),product_id,limit(default 50, at most 200),offsetandorder(a column, optionally.ascor.desc, defaultcount.desc).400withproduct_ids_invalidortoo_many_products.POST /v1/reviews/aggregates/rebuildbody:product_ids, an optional array of UUIDs. Without it the rebuild covers every product with reviews or figures, up to 1000 per call.400withproduct_ids_invalid.
Privacy and jobs
| Operation | Scope | What it does |
|---|---|---|
POST /v1/reviews/contacts/{contact_id}/anonymise | reviews.privacy.anonymise | Removes a person from their reviews. |
POST /v1/reviews/maintenance/run | reviews.jobs.maintain | Runs the hourly job by hand: it expires review links past their date. |
- Anonymise: the stars and the text stay, so the product figures do not move. The name, contact and company are cleared, and the person's review links are removed. The review text itself is not changed.
- Automatic on deletion: not yet (as of 2026-10). The app is set to run this when the customers or marketing-consents apps report a deletion, but the platform does not deliver those events yet. Until it does, a caller must send
POST /v1/reviews/contacts/{contact_id}/anonymise.
Vocabularies
| Operation | Scope | What it does |
|---|---|---|
GET /v1/reviews/vocabularies | reviews.vocabularies.list | Lists the closed vocabularies the app enforces. |
GET /v1/reviews/vocabularies/{name} | reviews.vocabularies.get | Reads one vocabulary with its values and labels. 404 when there is no such vocabulary. |