> For the complete documentation index, see [llms.txt](https://docs.customer-alliance.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.customer-alliance.com/reviews.md).

# Reviews

Collected reviews and owner replies, and pushing reviews in from other platforms.

## List reviews for the key's node subtree (newest first)

> Requires \`reviews:read\`. Returns every review on your account's subtree, including platforms that may not be redisplayed publicly and first-party sources — this is your own data, the same set your dashboard shows. If you are rendering the response to the public, pass \`?display=public\`.\
> \
> \*\*Responses to your Customer Alliance surveys are never returned here\*\*, with or without \`display=public\`. A survey-bridged review is a representation of a survey response that \`GET /surveys/{id}/responses\` already serves; returning it from both would make a consumer summing across them count the same feedback twice. To work with survey feedback, use the surveys endpoints. Survey responses imported from another tool (\`POST /survey-responses\` or a CSV upload) are the exception: the surveys endpoints do not serve them, so they are returned here as \`upload\` reviews.\
> \
> Hidden reviews are included and marked \`is\_hidden=true\` — filter on that flag to exclude them (unlike the public widget/certificate surfaces, which omit hidden reviews entirely).

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"tags":[{"name":"Reviews","description":"Collected reviews and owner replies, and pushing reviews in from other platforms."}],"servers":[{"url":"https://app.customer-alliance.com/api/v4","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key created in Settings → API (`ca_live_…`)."}},"parameters":{"cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from a prior response's next_cursor."},"limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"display":{"name":"display","in":"query","schema":{"type":"string","enum":["public"]},"description":"Pass `public` when the response will be shown to the public — a custom widget, an embed, anything a site visitor reads. It excludes two groups of reviews:\n\n- **Portals whose content may not be republished.** Some review portals license redisplay of their content as a paid product, or have terms that have not been cleared for it. Those reviews are yours to read, analyse and act on, but not to republish.\n- **Sources whose authorship was never verified** — reviews entered by hand (`manual`) and imported ones (`upload`: CSV uploads, and everything pushed through `POST /reviews` or `POST /survey-responses`). Legitimate internal signal, but nobody confirmed a guest wrote them.\n\nOnly the exact value `public` opts in. Any other value is ignored and you get the default, unfiltered behaviour — a typo must not silently withhold data.\n\nOmit it — the default — to read everything on your account. Responses to your Customer Alliance surveys are never served by these endpoints either way; see `GET /surveys/{id}/responses`."}},"schemas":{"Review":{"type":"object","description":"A collected review. Hidden reviews are included and flagged `is_hidden`; the owner's own reply text is `owner_reply`.","properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string"},"external_id":{"type":"string","description":"The review's id on the source platform."},"author":{"type":["string","null"],"description":"Null when the review is anonymised."},"rating":{"type":["number","null"],"description":"Rating on a 0–5 scale."},"rating_normalized":{"type":["number","null"],"description":"Rating normalised to 0–100."},"title":{"type":["string","null"]},"text":{"type":["string","null"]},"url":{"type":["string","null"],"description":"Where to manage this review on the source platform — not a guest-facing link. May require a portal login and may embed internal property identifiers. Do not republish it publicly."},"language":{"type":["string","null"]},"sentiment":{"type":["string","null"],"description":"positive | neutral | negative | null."},"owner_reply":{"type":["string","null"]},"replied_at":{"type":["string","null"],"format":"date-time"},"reviewed_at":{"type":"string","format":"date-time"},"node_id":{"type":"string","format":"uuid"},"is_hidden":{"type":"boolean"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key lacks the required scope, or the account's plan does not include it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded. Honour the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/reviews":{"get":{"summary":"List reviews for the key's node subtree (newest first)","description":"Requires `reviews:read`. Returns every review on your account's subtree, including platforms that may not be redisplayed publicly and first-party sources — this is your own data, the same set your dashboard shows. If you are rendering the response to the public, pass `?display=public`.\n\n**Responses to your Customer Alliance surveys are never returned here**, with or without `display=public`. A survey-bridged review is a representation of a survey response that `GET /surveys/{id}/responses` already serves; returning it from both would make a consumer summing across them count the same feedback twice. To work with survey feedback, use the surveys endpoints. Survey responses imported from another tool (`POST /survey-responses` or a CSV upload) are the exception: the surveys endpoints do not serve them, so they are returned here as `upload` reviews.\n\nHidden reviews are included and marked `is_hidden=true` — filter on that flag to exclude them (unlike the public widget/certificate surfaces, which omit hidden reviews entirely).","operationId":"listReviews","tags":["Reviews"],"parameters":[{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/display"}],"responses":{"200":{"description":"A page of reviews.","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"next_cursor":{"type":["string","null"],"description":"Pass as `?cursor=` to fetch the next page; null on the last page."},"has_more":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}
```

## Push reviews from another platform into a property

> Requires \`reviews:write\`. Writes up to 100 reviews from a platform Customer Alliance has no connector for, under a named \`source\` — the automated form of the review inbox's CSV upload. They reach the inbox, sentiment, AI insights, alerts and the \`review\.received\` webhook, and are kept out of reputation scores and public surfaces. See \*\*Pushing reviews and survey responses\*\*.\
> \
> Identity is (source, \`external\_id\`): a repeat push updates the stored review rather than duplicating it. Validation is all-or-nothing (\`400\`, naming the first failing field). Each item's outcome is in \`results\`; an item that cannot be written is \`skipped\` with a \`reason\`, never dropped.

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"tags":[{"name":"Reviews","description":"Collected reviews and owner replies, and pushing reviews in from other platforms."}],"servers":[{"url":"https://app.customer-alliance.com/api/v4","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key created in Settings → API (`ca_live_…`)."}},"schemas":{"ReviewPushInput":{"type":"object","required":["source","rating_scale","reviews"],"additionalProperties":false,"description":"A batch of reviews for one property, from one source. Validation is all-or-nothing.","properties":{"source":{"type":"string","minLength":1,"maxLength":120,"description":"Where the data comes from, e.g. Zenchef. Created on first use and matched case-insensitively after that — the same sources the review inbox's CSV upload offers."},"property_id":{"type":"string","format":"uuid","description":"The property (leaf node) to write to. Optional when the key is bound to a single property; required when it is bound to a group. Must be within the key's node scope."},"rating_scale":{"type":"integer","enum":[5,10,100],"description":"The scale every rating in the batch is on."},"reviews":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/ReviewPushItem"}}}},"ReviewPushItem":{"type":"object","required":["external_id","rating","reviewed_at"],"additionalProperties":false,"properties":{"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Your id for this review: its identity within the source. A repeat push replaces the stored review; a title or owner reply already stored is kept when the push omits it."},"rating":{"type":"number","description":"On the batch's `rating_scale`: 1–5, 1–10 or 0–100."},"reviewed_at":{"anyOf":[{"type":"string","format":"date"},{"type":"string","format":"date-time"}],"description":"When the review was written. An ISO-8601 date (kept as that calendar day) or a date-time with its zone (`Z` or an offset such as `+02:00`); a date-time without a zone is refused."},"title":{"type":["string","null"],"maxLength":500},"text":{"type":["string","null"],"maxLength":20000},"author_name":{"type":["string","null"],"maxLength":200,"description":"Defaults to the name of the guest matched by `email`, when there is one. Not stored when the hotel has anonymized a review from the same `email`: the item is stored anonymized."},"email":{"type":["string","null"],"format":"email","description":"The guest's email. Links the item to a guest with that email at the property and makes it reachable by GDPR erasure and export; an email erased under GDPR is skipped. Never returned by the read endpoints."},"language":{"type":["string","null"],"description":"e.g. en or en-GB."},"owner_reply":{"type":["string","null"],"maxLength":20000,"description":"A reply already published on the source platform."},"owner_replied_at":{"anyOf":[{"type":"string","format":"date"},{"type":"string","format":"date-time"},{"type":"null"}],"description":"When the reply was published. An ISO-8601 date (kept as that calendar day) or a date-time with its zone (`Z` or an offset such as `+02:00`); a date-time without a zone is refused."},"anonymized":{"type":"boolean","description":"The source says the reviewer is anonymous; no author name is stored."}}},"ReviewPushResult":{"type":"object","required":["source","property_id","created","updated","skipped","results"],"properties":{"source":{"$ref":"#/components/schemas/PushSource"},"property_id":{"type":"string","format":"uuid"},"created":{"type":"integer"},"updated":{"type":"integer"},"skipped":{"type":"integer"},"results":{"type":"array","description":"One outcome per item, in request order.","items":{"$ref":"#/components/schemas/ReviewPushOutcome"}}}},"PushSource":{"type":"object","description":"The upload source a push landed under — the same sources the review inbox's CSV upload offers.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"ReviewPushOutcome":{"type":"object","required":["external_id","status","review_id"],"properties":{"external_id":{"type":"string"},"status":{"type":"string","enum":["created","updated","skipped"]},"review_id":{"type":["string","null"],"format":"uuid","description":"The stored review; null when skipped."},"reason":{"type":"string","enum":["erased","other_property"],"description":"Present when `status` is `skipped`. `erased`: the email belongs to a guest erased under GDPR. `other_property`: this external_id already belongs to a different property in this source; a push never moves a record between properties."}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}},"responses":{"BadRequest":{"description":"Invalid request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key lacks the required scope, or the account's plan does not include it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded. Honour the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/reviews":{"post":{"summary":"Push reviews from another platform into a property","description":"Requires `reviews:write`. Writes up to 100 reviews from a platform Customer Alliance has no connector for, under a named `source` — the automated form of the review inbox's CSV upload. They reach the inbox, sentiment, AI insights, alerts and the `review.received` webhook, and are kept out of reputation scores and public surfaces. See **Pushing reviews and survey responses**.\n\nIdentity is (source, `external_id`): a repeat push updates the stored review rather than duplicating it. Validation is all-or-nothing (`400`, naming the first failing field). Each item's outcome is in `results`; an item that cannot be written is `skipped` with a `reason`, never dropped.","operationId":"pushReviews","tags":["Reviews"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewPushInput"}}}},"responses":{"200":{"description":"The batch was processed; see each item's `status`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewPushResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"property_id is missing, out of scope, or not a property (leaf) node.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}
```

## Get a single review by id

> Requires \`reviews:read\`. Returns any review on your account's subtree by default, including hidden ones (marked \`is\_hidden=true\`) and platforms that may not be redisplayed publicly. If you are rendering the review to the public, pass \`?display=public\`; a review excluded under that opt-in responds \`404\`.\
> \
> \*\*Responses to your Customer Alliance surveys are never returned here\*\*, with or without \`display=public\`. A survey-bridged review is a representation of a survey response that \`GET /surveys/{id}/responses\` already serves; returning it from both would make a consumer summing across them count the same feedback twice. To work with survey feedback, use the surveys endpoints. A survey-bridged review id responds \`404\` here. (Imported survey responses are \`upload\` reviews and are returned.)

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"tags":[{"name":"Reviews","description":"Collected reviews and owner replies, and pushing reviews in from other platforms."}],"servers":[{"url":"https://app.customer-alliance.com/api/v4","description":"Production"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key created in Settings → API (`ca_live_…`)."}},"parameters":{"display":{"name":"display","in":"query","schema":{"type":"string","enum":["public"]},"description":"Pass `public` when the response will be shown to the public — a custom widget, an embed, anything a site visitor reads. It excludes two groups of reviews:\n\n- **Portals whose content may not be republished.** Some review portals license redisplay of their content as a paid product, or have terms that have not been cleared for it. Those reviews are yours to read, analyse and act on, but not to republish.\n- **Sources whose authorship was never verified** — reviews entered by hand (`manual`) and imported ones (`upload`: CSV uploads, and everything pushed through `POST /reviews` or `POST /survey-responses`). Legitimate internal signal, but nobody confirmed a guest wrote them.\n\nOnly the exact value `public` opts in. Any other value is ignored and you get the default, unfiltered behaviour — a typo must not silently withhold data.\n\nOmit it — the default — to read everything on your account. Responses to your Customer Alliance surveys are never served by these endpoints either way; see `GET /surveys/{id}/responses`."}},"schemas":{"Review":{"type":"object","description":"A collected review. Hidden reviews are included and flagged `is_hidden`; the owner's own reply text is `owner_reply`.","properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string"},"external_id":{"type":"string","description":"The review's id on the source platform."},"author":{"type":["string","null"],"description":"Null when the review is anonymised."},"rating":{"type":["number","null"],"description":"Rating on a 0–5 scale."},"rating_normalized":{"type":["number","null"],"description":"Rating normalised to 0–100."},"title":{"type":["string","null"]},"text":{"type":["string","null"]},"url":{"type":["string","null"],"description":"Where to manage this review on the source platform — not a guest-facing link. May require a portal login and may embed internal property identifiers. Do not republish it publicly."},"language":{"type":["string","null"]},"sentiment":{"type":["string","null"],"description":"positive | neutral | negative | null."},"owner_reply":{"type":["string","null"]},"replied_at":{"type":["string","null"],"format":"date-time"},"reviewed_at":{"type":"string","format":"date-time"},"node_id":{"type":"string","format":"uuid"},"is_hidden":{"type":"boolean"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key lacks the required scope, or the account's plan does not include it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Not found or outside the key's scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/reviews/{id}":{"get":{"summary":"Get a single review by id","description":"Requires `reviews:read`. Returns any review on your account's subtree by default, including hidden ones (marked `is_hidden=true`) and platforms that may not be redisplayed publicly. If you are rendering the review to the public, pass `?display=public`; a review excluded under that opt-in responds `404`.\n\n**Responses to your Customer Alliance surveys are never returned here**, with or without `display=public`. A survey-bridged review is a representation of a survey response that `GET /surveys/{id}/responses` already serves; returning it from both would make a consumer summing across them count the same feedback twice. To work with survey feedback, use the surveys endpoints. A survey-bridged review id responds `404` here. (Imported survey responses are `upload` reviews and are returned.)","operationId":"getReview","tags":["Reviews"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/display"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
```
