52 "when": "Not used by the import: no reviewed writer sends it, so naming it as a write operation is refused (MISSING_WRITER). Retry reviews whose bulk result was failed by calling createStoresReviews again with only those records (a chunk of one is valid); the tail of a batch is just a smaller chunk. A retry of a review that did land answers already-exists (one review per author per product)."
57 "when": "Second pass after create, to force the imported moderation state (APPROVED / REJECTED). moderation.moderationStatus is read-only on create."
65 "when": "Follow-up on a created review, only when the source carries a merchant reply (reply is read-only on create). Sent by setStoresReviewReply with the review record's own grant, never as a task write operation. Docs-verified only: a live probe is owed before verification becomes verified-live."
66 }
67 ],
68 "fallbacks": [
69 {
70 "classification": "cms",
71 "when": "Use for review facts the native object cannot carry — the verified-purchase badge, ratings of 0, over-length bodies, and the source review ID/author email when no contact is created. Native reviews stay the primary record; the CMS shadow is evidence, not display."
72 }
73 ],
74 "reliability": {
75 "status": "partially-reliable",
76 "flags": []
77 },
78 "pitfalls": [
79 {
80 "code": "contact-required-per-review",
81 "severity": "blocker",
82 "summary": "Every review needs a contact: Bulk Create requires reviews.author.contactId, and the Create Review introduction states the single-create method requires a contact ID too. Importing reviews therefore CREATES OR RESOLVES A CONTACT PER DISTINCT REVIEWER — a CRM side effect well beyond the catalog, and reviewers are frequently not customers. Resolve each reviewer by email through Query Contacts first, create only on miss, and disclose the contact count at the execution gate."
87 "summary": "moderation.moderationStatus is read-only on create; a created review starts as SUBMITTED and is then moved by the site's moderation rules to APPROVED / IN_MODERATION / REJECTED. Source approval state (WooCommerce approved vs hold vs spam vs trash) survives only via a second pass — Bulk Update Moderation Status (POST /reviews/v1/bulk/reviews/moderate), whose status enum accepts APPROVED or REJECTED only (not IN_MODERATION), is filter-based, and returns a jobId (asynchronous — completion is not observable in the create response). VERIFIED LIVE 2026-10-27: on a new site a created review came back APPROVED at once. Bulk moderate returns a jobId; the reviewed writer moderateStoresReview moderates one review per call and reads it back until the status matches."
88 },
89 {
90 "code": "verified-badge-not-writable",
91 "severity": "warning",
92 "summary": "review.verified (the verified-owner/verified-purchase badge) is read-only on create and has no documented import path. WooCommerce's per-review `verified` flag CANNOT CARRY — this is a reportable faithfulness loss on every imported review, not a rounding error, because the badge is the trust signal shoppers read."
93 },
94 {
95 "code": "rating-1-to-5-only",
96 "severity": "info",
97 "summary": "VERIFIED LIVE 2026-10-27: bulk create ACCEPTED content.rating 0 and stored it as 0 (the docs list Min 1; single create may still require 1-5). The reviewed writer uses bulk create and accepts integers 0-5, so a WooCommerce rating-0 review imports natively."
98 },
99 {
100 "code": "one-review-per-author-per-entity",
101 "severity": "warning",
102 "summary": "\"A review author is limited to 1 review per entity. Duplicate reviews receive an error response.\" WooCommerce permits many reviews by the same author (same email) on the same product, so a collision policy is required BEFORE the write: keep newest / keep highest-rated / merge bodies / spill the losers to the CMS shadow. Detect collisions during mapping by grouping source reviews on (reviewer_email, product_id) — an anonymized verification environment had 0 collisions across 120 reviews, which makes this easy to miss on a first run. VERIFIED LIVE 2026-10-27: a second review by the same contact on the same product fails that bulk item with ALREADY_EXISTS (uniqueConstraintViolation on namespace, contextId, author.contactId) while the rest of the batch succeeds. The mapping keeps the newest native and sends the rest to the CMS shadow."
103 },
104 {
105 "code": "body-plain-text-3000",
106 "severity": "warning",
107 "summary": "content.body is plain text with maxLength 3000 (content.title also maxLength 3000, author.authorName maxLength 100, content.media maxItems 10, entityId maxLength 36). WooCommerce review bodies are HTML — strip tags before the write, and ledger every review whose stripped body exceeds 3000 characters with the chosen policy (truncate or divert to the CMS shadow)."
108 },
109 {
110 "code": "stores-namespace-only",
111 "severity": "warning",
112 "summary": "namespace supports \"stores\" only — \"Currently, only Wix Stores is fully integrated with the Wix Reviews app.\" entityId must be the Wix Stores PRODUCT ID (maxLength 36), so the product crosswalk must be complete and the products must already exist before any review write. Reviews of non-product entities (WordPress post/page comments) have no home here; they belong to blog/comment."
113 },
114 {
115 "code": "required-field-asymmetry",
116 "severity": "warning",
117 "summary": "The two create surfaces do not require the same fields. Create Review requires review.namespace, review.entityId, review.content, review.content.rating (author.contactId is not in its required list despite the prose). Bulk Create requires reviews.namespace, reviews.entityId, reviews.content, reviews.author, reviews.author.contactId — but NOT reviews.content.rating. Send the full set on both paths rather than relying on either list."
118 },
119 {
120 "code": "docs-endpoint-path-inconsistency",
121 "severity": "info",
122 "summary": "VERIFIED LIVE 2026-10-27: GET /reviews/v1/reviews/{id} and GET /reviews/api/v1/reviews/{id} both answer. The writers use /reviews/v1/..."
123 },
124 {
125 "code": "bulk-batch-100",
126 "severity": "warning",
127 "summary": "Bulk Create takes minItems 1 / maxItems 100 reviews per call and returns per-item results with originalIndex and a success flag (entities only when returnEntity is true). Chunk at 100, always request returnEntity so the crosswalk can record server-assigned review IDs, and treat partial failure as normal — the call succeeds while individual items fail."
128 },
129 {
130 "code": "reviews-app-required",
131 "severity": "blocker",
132 "summary": "VERIFIED LIVE 2026-10-27: bulk create answers 428 APP_NOT_INSTALLED (appId a95a5fce-e370-4402-9ce4-96956acc747d) until Wix Reviews is installed - the install-wix-reviews setup step."
133 },
134 {
135 "code": "rejected-reviews-hidden-from-reads",
136 "severity": "warning",
137 "summary": "VERIFIED LIVE 2026-10-27: a REJECTED review answers 404 on GET and is missing from Query unless returnPrivateReviews=true is passed on GET. buildGetReviewRequest always passes it; findStoresReview (Query) only sees visible reviews."
138 },
139 {
140 "code": "merchant-reply-is-a-follow-up",
141 "severity": "warning",
142 "summary": "Docs (Set Reply, read 2026-10-29): reply is read-only on create; PATCH /reviews/v1/reviews/{reviewId}/reply with { message } sets the one reply a review can have and overwrites any existing reply. The request allows 10000 characters but the Review object caps reply.message at 1000, so setStoresReviewReply refuses a longer reply before sending (review_reply_too_long). reply.createdDate is read-only: the source reply date cannot carry. Whether Wix notifies the review author is not documented; site mute covers it. Not exercised live."
143 }
144 ],
145 "mappingGuidance": [
146 "Import order: products first (entityId is a Wix product ID), then reviewer contacts through the crm/contact task (resolve by email, create on miss), then reviews through createStoresReviews(wix, { records: [{ sourceStableKey, review }], readinessGrant }) in chunks of 100. Each record payload is the review item itself, so entityId and author.contactId are checked per record.",
147 "Reviewer contacts are created for reviewers with no contact: show the distinct-reviewer contact count in the plan before consent.",
148 "Moderation, per batch: right after each batch's createStoresReviews and before the task's next evaluateTask, pass that batch's reviews whose created status differs from the source to moderateStoresReviews([{ wix, reviewId, status }]) in one call, each wix the record client bound to that batch's grant: APPROVED for WooCommerce approved; REJECTED for hold, spam and trash. It sends all moderations, then reads them back in shared rounds. A grant from an earlier evaluation is stale: its moderation is refused unsent (moderation-failed, error.requestSent false). A review created earlier (another batch or run) is moderated by gating its key again in the current batch and using that grant. Record the source state in the ledger.",
149 "Merchant replies: after create (and moderation), for every review whose source has a merchant reply call setStoresReviewReply(recordClient, { reviewId, message }) with that review record's own client (bindRecordClient with the review task's grant and the review's sourceStableKey). Keep the reply out of the create payload (reply is read-only on create). The client admits the reply only on the record's own review (its crosswalk target, or a review it read whose product and author are its granted references). Outcomes: verified | already-present (a retry finds the same reply and sends nothing) | not-sent (refused before send, e.g. review_reply_too_long; journal failed with requestSent false) | failed (rejected; failed_retryable) | unverified (sent, read-back differs; Set Reply overwrites, so the retry re-sends safely) | review-not-found. A reply over 1000 characters or its source date goes to the CMS shadow (fallbacks) and the faithfulness ledger.",
150 "Carry the original review date with review.reviewDate (writable at create, format date-time) — the docs designate it as the import path: \"This should match createdDate, except for reviews imported from another system.\" createdDate is read-only and will be the import timestamp, so reviewDate is the only faithful date.",
151 "Map author.authorName from the WooCommerce reviewer display name (maxLength 100) and author.contactId from the resolved contact. If authorName is omitted, Wix falls back to the member name for members and to null otherwise — always send it explicitly so anonymous reviewers do not lose their display name.",
152 "Record in the faithfulness ledger, per run: reviews whose `verified` badge was lost, reviews with source rating 0, reviews whose stripped body exceeded 3000 characters, and reviews dropped by the one-per-author-per-entity collision policy, and merchant replies not written natively (over 1000 characters, or not-sent/failed after retry) with their source reply date."
164 "note": "object page, fetched 2026-08-16. DOC TREE LOCATION: the Reviews API lives under CRM > Community > Feedback & Moderation, NOT under business-solutions/stores — which is why the 2026-08-11 stores docs-survey sweep (docsRoots api-reference/business-solutions/stores) missed it entirely. This entity is filed in the stores domain because namespace supports \"stores\" only and entityId is a Stores product ID; the survey row that claims it lives in the crm domain. Do not 'rediscover' this."
169 "note": "fetched 2026-08-16. Verbatim: \"The Wix Reviews API allows you to: Import reviews from other sites and export reviews to other sites.\" Also: namespace \"stores\" only; \"Every review needs a contact\" (resolve via Query Contacts by email); \"A review author is limited to 1 review per entity. Duplicate reviews receive an error response.\"; rating 1-5."
189 "note": "fetched 2026-08-16. PATCH /reviews/v1/reviews/{reviewId}/reply, required message — replies are a separate call because review.reply is read-only on create."
194 "note": "fetched 2026-08-16. \"To import or migrate multiple reviews, call Bulk Create Review.\" Created reviews land at moderationStatus SUBMITTED and are then moved by moderation rules. Prerequisites stated: Wix Stores must be installed, the products must exist, and each review requires a contact ID."
199 "note": "added specifically so mapper/codegen stop reporting 'No verified native Wix product-review primitive was identified for this workflow'. Broader mapper/codegen/execution support for reviews may still need follow-up work."
204 "note": "Live source verification established that /wc/v3/products/reviews exposes review status and reviewer email and is classified backend_data/sample by the /wc/v3/products* catch-all rule in lib/wp-route-classifier.js. The fixture pins that classification so future narrowing cannot silently drop reviews."
209 "note": "PATCH https://www.wixapis.com/reviews/v1/reviews/{reviewId}/reply, body { message } (required, maxLength 10000); response { review } with reply.message (maxLength 1000), reply.createdDate/updatedDate read-only; one reply per review, a second call overwrites; scope SCOPE.DC-REVIEWS.MANAGE-REVIEWS (read 2026-10-29). Docs-verified; no live probe yet."
210 }
211 ],
212 "notes": "Authored 2026-08-16. Before this file no domain entity claimed a product-review capability, so every run dropped its source reviews into a pending row despite Wix having a native, explicitly import-oriented API. Docs-verified only — nothing here has been exercised live, hence verification=docs / reliability=unknown. A review import writes into CRM (a contact per distinct reviewer) as well as the catalog; that is an ordinary dependency, not a reason for importSafe false. Promote verification after a live subset write that exercises bulk create + the moderation second pass."