> 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/models.md).

# Models

## The Error object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}}}}
```

## The Review object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"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"}}}}}}
```

## The AnalyticsScores object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"AnalyticsScores":{"type":"object","description":"Aggregate reputation scores for the key's node subtree.","properties":{"node_id":{"type":"string","format":"uuid"},"overall_score":{"type":["number","null"],"description":"Overall score on a 0–5 scale (one decimal)."},"overall_score_normalized":{"type":["number","null"],"description":"Overall score normalised to 0–100 (may carry one decimal; round for whole-percent display)."},"total_reviews":{"type":"integer"},"recommend_rate":{"type":["number","null"]},"response_rate":{"type":["number","null"]},"platform_scores":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"score":{"type":["number","null"],"description":"Platform score on a 0–5 scale (one decimal)."},"score_normalized":{"type":["number","null"],"description":"Platform score normalised to 0–100 (may carry one decimal; round for whole-percent display)."},"reviews":{"type":"integer"}}}}}}}}}
```

## The Node object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Node":{"type":"object","description":"A hierarchy node (business unit / property) within the key's scope.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"parent_id":{"type":["string","null"],"format":"uuid"},"type":{"type":"string"},"attributes":{"type":"array","description":"A property's value of every property attribute on the account, in the order of `GET /property-attributes`. A group (any node that is not a `property`) holds none, so its list is empty. See **Property attributes**.","items":{"$ref":"#/components/schemas/NodeAttribute"}}}},"NodeAttribute":{"type":"object","description":"One attribute's value at a property: the value the reports group it by. That is the property's own, or, when it holds none, the value of the nearest property above it. `value_id` and `value` are null when it holds none.","properties":{"attribute_id":{"type":"string","format":"uuid"},"attribute":{"type":"string"},"value_id":{"type":["string","null"],"format":"uuid"},"value":{"type":["string","null"]}}}}}}
```

## The NodeAttribute object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"NodeAttribute":{"type":"object","description":"One attribute's value at a property: the value the reports group it by. That is the property's own, or, when it holds none, the value of the nearest property above it. `value_id` and `value` are null when it holds none.","properties":{"attribute_id":{"type":"string","format":"uuid"},"attribute":{"type":"string"},"value_id":{"type":["string","null"],"format":"uuid"},"value":{"type":["string","null"]}}}}}}
```

## The PropertyAttribute object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"PropertyAttribute":{"type":"object","description":"A property attribute the account has defined, with its values in the order the account set them.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"values":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"}}}}}}}}}
```

## The Survey object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Survey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"node_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"primary_language":{"type":"string"},"published_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The SurveyDetail object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyDetail":{"allOf":[{"$ref":"#/components/schemas/Survey"},{"type":"object","properties":{"published_definition":{"type":"object","description":"The published survey definition (pages/questions)."}}}]},"Survey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"node_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"primary_language":{"type":"string"},"published_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The SurveyResponse object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponse":{"type":"object","description":"A survey response. Respondent PII (email, personalisation context, tokens) is never exposed.","properties":{"id":{"type":"string","format":"uuid"},"survey_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"channel":{"type":"string"},"overall_score":{"type":["number","null"]},"started_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The SurveyAnswer object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyAnswer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"question_id":{"type":"string"},"question_path":{"type":"string"},"question_type":{"type":"string"},"value":{"description":"The answer value; shape depends on question_type."},"numeric_value":{"type":["number","null"]},"comment":{"type":["string","null"]},"answered_at":{"type":"string","format":"date-time"}}}}}}
```

## The Recommendation object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Recommendation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"priority":{"type":"string"},"impact":{"type":["string","null"]},"canonical_label_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The Mention object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Mention":{"type":"object","description":"A topic mention extracted from a review. `is_hidden` mirrors the source review's flag.","properties":{"id":{"type":"string","format":"uuid"},"review_id":{"type":"string","format":"uuid"},"source_field":{"type":"string"},"text":{"type":"string"},"sentiment":{"type":["string","null"]},"canonical_label_id":{"type":"string","format":"uuid"},"custom_category_id":{"type":["string","null"],"format":"uuid"},"confidence":{"type":["number","null"]},"is_hidden":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The Topic object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Topic":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"language":{"type":["string","null"]},"parent_label_id":{"type":["string","null"],"format":"uuid"}}}}}}
```

## The WhoAmI object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WhoAmI":{"type":"object","properties":{"account_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"scopes":{"type":"array","items":{"type":"string"}}}}}}}
```

## The WebhookEndpoint object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WebhookEndpoint":{"type":"object","description":"A registered outbound webhook endpoint. The signing secret is never returned after creation.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["review.received","review.replied","review.alert_triggered","survey.response_completed","guest.created","guest.unsubscribed"]}},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}
```

## The WebhookEndpointInput object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WebhookEndpointInput":{"type":"object","required":["name","url","events"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"url":{"type":"string","format":"uri","description":"Delivery URL. Must be http(s); private/reserved addresses are rejected at delivery time."},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["review.received","review.replied","review.alert_triggered","survey.response_completed","guest.created","guest.unsubscribed"]}},"is_active":{"type":"boolean","description":"PATCH only — pause/resume deliveries without deleting the endpoint."}}}}}}
```

## The WebhookEndpointCreated object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WebhookEndpointCreated":{"type":"object","required":["secret","endpoint"],"properties":{"secret":{"type":"string","description":"The signing secret (`whsec_…`). Returned exactly once — store it now; you cannot retrieve it again."},"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}}},"WebhookEndpoint":{"type":"object","description":"A registered outbound webhook endpoint. The signing secret is never returned after creation.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["review.received","review.replied","review.alert_triggered","survey.response_completed","guest.created","guest.unsubscribed"]}},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}
```

## The WebhookDelivery object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"event_type":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempt_count":{"type":"integer"},"last_status_code":{"type":["integer","null"]},"last_attempt_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}}
```

## The ReservationInput object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"ReservationInput":{"type":"object","required":["external_id"],"description":"A reservation (stay) to create or update for the guest, mirroring what a PMS integration syncs. Identity is (property, external_id): a repeat push of the same external_id updates the stay in place (e.g. a status change from booked to checked_out) rather than creating a duplicate. Pushing stays lets you build audiences on stay status, booking channel, and recently-ended stays.","properties":{"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Required — your booking reference; the stay's dedupe key."},"status":{"type":"string","enum":["booked","arrived","checked_out","cancelled","no_show","unknown"],"description":"Canonical stay status. Defaults to `booked` when omitted."},"raw_status":{"type":"string","maxLength":120,"description":"Your own status label, kept verbatim for display."},"check_in":{"type":"string","format":"date-time","description":"Stay start. ISO-8601 date or date-time."},"check_out":{"type":"string","format":"date-time","description":"Stay end. ISO-8601 date or date-time — drives the recently-ended-stay audience filter."},"checked_in_at":{"type":"string","format":"date-time"},"checked_out_at":{"type":"string","format":"date-time"},"channel":{"type":"string","maxLength":120,"description":"Booking origin, e.g. booking.com."},"custom_fields":{"type":"object","additionalProperties":{"type":["string","number","boolean"]},"description":"Merged into the stay's custom fields; omitted keys are preserved."}}}}}}
```

## The GuestPushInput object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"GuestPushInput":{"type":"object","required":["email"],"description":"A guest to create or update at one property. **With `external_id`**, identity is (property, `external_id`): a repeat push with the same `external_id` updates that guest, even when its email changed — this is how you correct an email. **Without `external_id`**, identity is (property, email): a repeat push of the same email updates that guest, and a different email creates a NEW guest. Optionally include a `reservation` to upsert the guest's stay in the same call.","properties":{"property_id":{"type":"string","format":"uuid","description":"The property (leaf node) to attach the guest to. Optional when the key is bound to a single property; required when the key is bound to a group. Must be a property within the key's node scope."},"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Optional — your own customer id. When sent, it is the guest's identity: re-push the same `external_id` with a corrected `email` and the same guest (and its reservations) gets the new address. The first push carrying an `external_id` for a guest you created earlier without one adopts that guest rather than duplicating it; from then on, keep sending the `external_id`. Two different `external_id`s are two guests, even with the same email."},"email":{"type":"string","format":"email","maxLength":320,"description":"Required — normalized (trimmed + lower-cased). Without `external_id` it is the guest's identity: a different email is a different guest."},"salutation":{"type":"string","maxLength":50},"first_name":{"type":"string","maxLength":200},"last_name":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":40},"country_code":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 (e.g. DE)."},"language_code":{"type":"string","maxLength":5,"description":"e.g. de or de-DE."},"custom_fields":{"type":"object","additionalProperties":{"type":["string","number","boolean"]},"description":"Merged into the guest's custom fields; omitted keys are preserved."},"marketing_consent":{"type":"boolean","description":"When set, records invitation-email consent. An explicit prior withdrawal is never overridden by a grant (GDPR)."},"reservation":{"$ref":"#/components/schemas/ReservationInput"}}},"ReservationInput":{"type":"object","required":["external_id"],"description":"A reservation (stay) to create or update for the guest, mirroring what a PMS integration syncs. Identity is (property, external_id): a repeat push of the same external_id updates the stay in place (e.g. a status change from booked to checked_out) rather than creating a duplicate. Pushing stays lets you build audiences on stay status, booking channel, and recently-ended stays.","properties":{"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Required — your booking reference; the stay's dedupe key."},"status":{"type":"string","enum":["booked","arrived","checked_out","cancelled","no_show","unknown"],"description":"Canonical stay status. Defaults to `booked` when omitted."},"raw_status":{"type":"string","maxLength":120,"description":"Your own status label, kept verbatim for display."},"check_in":{"type":"string","format":"date-time","description":"Stay start. ISO-8601 date or date-time."},"check_out":{"type":"string","format":"date-time","description":"Stay end. ISO-8601 date or date-time — drives the recently-ended-stay audience filter."},"checked_in_at":{"type":"string","format":"date-time"},"checked_out_at":{"type":"string","format":"date-time"},"channel":{"type":"string","maxLength":120,"description":"Booking origin, e.g. booking.com."},"custom_fields":{"type":"object","additionalProperties":{"type":["string","number","boolean"]},"description":"Merged into the stay's custom fields; omitted keys are preserved."}}}}}}
```

## The Reservation object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Reservation":{"type":"object","description":"A reservation (stay) as stored. Returned inside a `Guest` when the push included a `reservation`.","properties":{"id":{"type":"string","format":"uuid"},"external_id":{"type":"string"},"status":{"type":"string","enum":["booked","arrived","checked_out","cancelled","no_show","unknown"]},"raw_status":{"type":["string","null"]},"check_in":{"type":["string","null"],"format":"date-time"},"check_out":{"type":["string","null"],"format":"date-time"},"checked_in_at":{"type":["string","null"],"format":"date-time"},"checked_out_at":{"type":["string","null"],"format":"date-time"},"channel":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}
```

## The Guest object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"Guest":{"type":"object","description":"A guest record as stored. `source` is `api` for guests created through this endpoint.","properties":{"id":{"type":"string","format":"uuid"},"property_id":{"type":"string","format":"uuid"},"source":{"type":"string"},"email":{"type":["string","null"]},"salutation":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"phone":{"type":["string","null"]},"country_code":{"type":["string","null"]},"language_code":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"reservation":{"$ref":"#/components/schemas/Reservation","description":"Present only when the push included a reservation."}}},"Reservation":{"type":"object","description":"A reservation (stay) as stored. Returned inside a `Guest` when the push included a `reservation`.","properties":{"id":{"type":"string","format":"uuid"},"external_id":{"type":"string"},"status":{"type":"string","enum":["booked","arrived","checked_out","cancelled","no_show","unknown"]},"raw_status":{"type":["string","null"]},"check_in":{"type":["string","null"],"format":"date-time"},"check_out":{"type":["string","null"],"format":"date-time"},"checked_in_at":{"type":["string","null"],"format":"date-time"},"checked_out_at":{"type":["string","null"],"format":"date-time"},"channel":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}
```

## The PushSource object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"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"}}}}}}
```

## The ReviewPushItem object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"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."}}}}}}
```

## The ReviewPushInput object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"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."}}}}}}
```

## The ReviewPushOutcome object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"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."}}}}}}
```

## The ReviewPushResult object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"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."}}}}}}
```

## The SurveyResponsePushItem object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponsePushItem":{"type":"object","required":["external_id","rating","responded_at","answers"],"additionalProperties":false,"properties":{"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Your id for this response: its identity within the source. A repeat push replaces the stored response and its answers."},"rating":{"type":"number","description":"The response's overall score, on the batch's `rating_scale`."},"responded_at":{"anyOf":[{"type":"string","format":"date"},{"type":"string","format":"date-time"}],"description":"When the response was completed. 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."},"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."},"answers":{"type":"object","description":"Question label (in any language) → answer. A number answers a rating question (on the batch's `rating_scale`), a string a free-text question, and null leaves it unanswered; at least one question must be answered. A question keeps the type — and, for a rating, the scale — it was first given, and a source's survey holds at most 200 questions. Free-text answers become the inbox review's text.","additionalProperties":{"type":["number","string","null"]},"maxProperties":100}}}}}}
```

## The SurveyResponsePushInput object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponsePushInput":{"type":"object","required":["source","rating_scale","responses"],"additionalProperties":false,"description":"A batch of completed survey responses 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."},"responses":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/SurveyResponsePushItem"}}}},"SurveyResponsePushItem":{"type":"object","required":["external_id","rating","responded_at","answers"],"additionalProperties":false,"properties":{"external_id":{"type":"string","minLength":1,"maxLength":255,"description":"Your id for this response: its identity within the source. A repeat push replaces the stored response and its answers."},"rating":{"type":"number","description":"The response's overall score, on the batch's `rating_scale`."},"responded_at":{"anyOf":[{"type":"string","format":"date"},{"type":"string","format":"date-time"}],"description":"When the response was completed. 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."},"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."},"answers":{"type":"object","description":"Question label (in any language) → answer. A number answers a rating question (on the batch's `rating_scale`), a string a free-text question, and null leaves it unanswered; at least one question must be answered. A question keeps the type — and, for a rating, the scale — it was first given, and a source's survey holds at most 200 questions. Free-text answers become the inbox review's text.","additionalProperties":{"type":["number","string","null"]},"maxProperties":100}}}}}}
```

## The SurveyResponsePushOutcome object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponsePushOutcome":{"type":"object","required":["external_id","status","response_id","review_id"],"properties":{"external_id":{"type":"string"},"status":{"type":"string","enum":["created","updated","skipped"]},"response_id":{"type":["string","null"],"format":"uuid","description":"The stored survey response; null when skipped."},"review_id":{"type":["string","null"],"format":"uuid","description":"The review it appears as in the inbox and in `GET /reviews`; 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."}}}}}}
```

## The SurveyResponsePushResult object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponsePushResult":{"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/SurveyResponsePushOutcome"}}}},"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"}}},"SurveyResponsePushOutcome":{"type":"object","required":["external_id","status","response_id","review_id"],"properties":{"external_id":{"type":"string"},"status":{"type":"string","enum":["created","updated","skipped"]},"response_id":{"type":["string","null"],"format":"uuid","description":"The stored survey response; null when skipped."},"review_id":{"type":["string","null"],"format":"uuid","description":"The review it appears as in the inbox and in `GET /reviews`; 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."}}}}}}
```

## The WebhookEnvelope object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"WebhookEnvelope":{"type":"object","description":"Every delivery POSTs this envelope. Verify the `X-CA-Signature: sha256=<hex>` header — an HMAC-SHA256 of the raw request body keyed by your endpoint's signing secret — before trusting it.","required":["id","event","created_at","data"],"properties":{"id":{"type":"string","format":"uuid","description":"Delivery id (also in the X-CA-Delivery-Id header); stable across retries — use it to dedupe."},"event":{"type":"string","enum":["review.received","review.replied","review.alert_triggered","survey.response_completed","guest.created","guest.unsubscribed"]},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"The event payload; shape depends on `event` (see the schemas below)."}}}}}}
```

## The ReviewReceivedData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"ReviewReceivedData":{"type":"object","description":"Carries the whole review, so you do not need a follow-up `GET /reviews/{id}` to act on the event. `review` is the same shape that endpoint returns, and `sentiment` may still be null — unrated reviews are classified asynchronously, after this event fires. `review_id`, `node_id` and `platform` are duplicated at the top level for consumers written against the original payload.\n\nWhich reviews fire this event: those ingested from a connected portal, plus `manual` and `upload` (CSV uploads and reviews pushed through `POST /reviews`). Only recent reviews fire it, so a first sync or a backfill of older history does not flood your endpoint. Survey responses do NOT — whether collected by a Customer Alliance survey or pushed through `POST /survey-responses` — because a survey-bridged review is written by a different path that emits no webhook; do not wait on this event for them, and read `GET /surveys/{id}/responses` for your own surveys instead. Every review delivered here is also served by `GET /reviews`.","properties":{"review_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"platform":{"type":"string"},"review":{"$ref":"#/components/schemas/Review"}}},"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"}}}}}}
```

## The ReviewRepliedData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"ReviewRepliedData":{"type":"object","properties":{"review_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"reply_id":{"type":"string","format":"uuid"}}}}}}
```

## The ReviewAlertTriggeredData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"ReviewAlertTriggeredData":{"type":"object","description":"Note: this payload uses camelCase keys (historical).","properties":{"alertId":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"type":{"type":"string"},"message":{"type":"string"},"context":{"type":"object"}}}}}}
```

## The SurveyResponseCompletedData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"SurveyResponseCompletedData":{"type":"object","properties":{"survey_id":{"type":"string","format":"uuid"},"response_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"overall_score":{"type":["number","null"]}}}}}}
```

## The GuestCreatedData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"GuestCreatedData":{"type":"object","properties":{"guest_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"source":{"type":"string"}}}}}}
```

## The GuestUnsubscribedData object

```json
{"openapi":"3.1.0","info":{"title":"Customer Alliance API","version":"4.2.0"},"components":{"schemas":{"GuestUnsubscribedData":{"type":"object","properties":{"guest_id":{"type":["string","null"],"format":"uuid"},"unsubscribe_id":{"type":"string","format":"uuid"}}}}}}
```
