{"openapi":"3.1.0","info":{"title":"Customer Alliance API","summary":"Review management, surveys & feedback for hospitality — as an API.","version":"4.2.0","description":"The Customer Alliance API gives you programmatic access to your reviews, surveys, analytics and AI insights, lets you push guests, reviews and survey responses in from your other systems, and lets you subscribe to events via outbound webhooks.\n\n## Getting started\n\n1. In the dashboard, go to **Settings → API** and create a key, granting only the scopes you need.\n2. Copy the key (`ca_live_…`) — it's shown once.\n3. Send it as a Bearer token on every request:\n\n```bash\ncurl https://app.customer-alliance.com/api/v4/reviews \\\n  -H \"Authorization: Bearer ca_live_your_key_here\"\n```\n\nCall `GET /whoami` to confirm the key resolves to the account, node and scopes you expect.\n\n## Authentication\n\nEvery request needs `Authorization: Bearer <key>`. Keys are created and scoped in **Settings → API**.\n\n## Scopes\n\nEach key holds a subset of `reviews:read`, `surveys:read`, `analytics:read`, `insights:read`, `nodes:read`, `webhooks:manage`, `guests:write`, `reviews:write`, `surveys:write`. A call missing its scope returns `403`.\n\n## Hierarchy\n\nA key is bound to one node in your account hierarchy; **read** endpoints cover that node and its subtree, and **write** endpoints (`POST /guests`, `POST /reviews`, `POST /survey-responses`) write to one property within it. The webhook-management endpoints (`/webhooks*`) are the exception — they operate account-wide and ignore the key's node binding (see **Webhooks**) — and so are the attribute definitions of `GET /property-attributes`, which describe the account rather than a property (see **Property attributes**).\n\n## Display scope\n\nBy default, read endpoints return everything on your account's subtree. An API key authenticates the account owner reading their own data — the same data the dashboard shows — so nothing is withheld from you.\n\nIf the response will be shown to the public, pass `?display=public`:\n\n```bash\ncurl \"https://app.customer-alliance.com/api/v4/reviews?display=public\" \\\n  -H \"Authorization: Bearer ca_live_your_key_here\"\n```\n\nSupported on `GET /reviews` and `GET /reviews/{id}` (`reviews:read`), and `GET /insights/mentions` (`insights:read`).\n\nIt excludes two groups: review portals whose content may not be republished (some license redisplay as a paid product, or have terms that have not been cleared for it), and 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`).\n\nOnly the exact value `public` opts in. Any other value is ignored and you get the default, unfiltered behaviour — deliberately, so that a typo cannot silently withhold data. Rather than tracking which platforms may be republished yourself, use `?display=public` and let the API apply the current set.\n\n> **Upgrading an existing integration.** `GET /reviews` previously withheld these platforms from every caller. It no longer does. If you use it to power anything the public sees, add `?display=public` before upgrading — without it you will begin receiving reviews from platforms that may not be republished. If you use it for internal analytics, reporting or warehousing, you need to do nothing; you simply get more complete data.\n\n### Survey responses\n\nResponses to your Customer Alliance surveys are never returned by `GET /reviews`, `GET /reviews/{id}` or `GET /insights/mentions`, with or without `display=public`. This is not a publishing restriction: a survey-bridged review is a representation of a response that `GET /surveys/{id}/responses` already serves, and 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.\n\nSurvey responses imported from another tool — pushed through `POST /survey-responses` or uploaded as a CSV — are the reverse: they are returned by the review endpoints as `upload` reviews, and the surveys endpoints do not list them. Either way, each response is served exactly once.\n\n### Hidden reviews\n\nHidden reviews *are* returned by `GET /reviews`, flagged `is_hidden: true`. Filter on that flag if you do not want them. (The public widget and certificate surfaces omit them entirely — a different surface, not this API.)\n\n## Pushing reviews and survey responses\n\nFeed reviews and survey responses from a platform Customer Alliance has no connector for — a booking or restaurant platform, your own survey tool — straight into your account. It is the automated form of the review inbox's CSV upload:\n\n- `POST /reviews` (`reviews:write`) pushes reviews.\n- `POST /survey-responses` (`surveys:write`) pushes survey responses with their answers.\n\nBoth scopes need a plan that includes bulk import, and can only be granted by someone allowed to reply to reviews.\n\n### Where the data goes\n\nPushed data lands exactly where a CSV upload lands:\n\n- in the **review inbox**, under the source name you send (for example `Zenchef`), filterable like any other source;\n- in **sentiment and AI insights**;\n- for pushed **reviews**, in your review **alert rules** and the `review.received` webhook — for recent reviews only, so a backfill of old ones triggers neither, and pushed survey responses trigger neither;\n- **not** in reputation scores, KPIs or trends, including `GET /analytics/scores` — those reflect reviews from connected portals only;\n- **not** on public surfaces: widgets, certificates and `?display=public` exclude imported reviews.\n\nPushed survey responses are kept in one survey per source, created and maintained for you and hidden from the survey list and survey statistics. Each response also appears in the inbox as a review, whose text is the response's free-text answers.\n\n### The batch\n\nA request carries one `source`, one `rating_scale` (`5`, `10` or `100`) and 1–100 items for one property:\n\n```bash\ncurl -X POST https://app.customer-alliance.com/api/v4/reviews \\\n  -H \"Authorization: Bearer ca_live_your_key_here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"source\": \"Zenchef\",\n    \"property_id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n    \"rating_scale\": 5,\n    \"reviews\": [\n      { \"external_id\": \"zc-1001\", \"rating\": 4, \"reviewed_at\": \"2026-09-01\", \"text\": \"Lovely terrace, slow service.\", \"author_name\": \"Jane D.\", \"language\": \"en\" }\n    ]\n  }'\n```\n\n- **`source`** names where the data came from. It is created on first use and matched case-insensitively after that. It is the same list of sources the inbox's CSV upload offers, so pushing under a name you already upload to continues that source.\n- **`property_id`** is optional when the key is bound to a single property and required when it is bound to a group — the same rule as `POST /guests`.\n- **`external_id`** is your own id for the item, and its identity: pushing the same `external_id` to the same source again replaces the stored copy rather than creating a duplicate — a review re-pushed without `text` loses its text — while a title or owner reply already on a review is kept, so a reply posted from Customer Alliance is never erased by a re-push. A CSV upload with the same `external_id` to the same source lands on the same record.\n- **`email`** (optional) links the item to a guest with that email at the property, and makes it reachable by GDPR erasure and data export.\n- Dates are ISO-8601. A date (`2026-09-01`) is kept as that calendar day; a date-time must carry its zone (`2026-09-01T18:30:00Z`, or an offset such as `+02:00`) and is refused without one, since its instant could only be guessed.\n\nValidation is all-or-nothing: if any item is invalid the request returns `400` naming the first failing field (for example `reviews.3.rating`) and nothing is written.\n\n### Survey answers\n\n`answers` maps each question's label — in any language — to its answer. A **number** answers a rating question on the batch's `rating_scale`, a **string** answers a free-text question, and `null` leaves it unanswered:\n\n```json\n{ \"external_id\": \"nv-9\", \"rating\": 4, \"responded_at\": \"2026-09-01\",\n  \"answers\": { \"Cleanliness\": 5, \"What could we improve?\": \"Breakfast could start earlier.\" } }\n```\n\nA question keeps the type — and, for a rating, the scale — it was first given. Answering it later with the other type, or on a different `rating_scale`, returns `422`, and nothing in that request is written. A source's survey holds at most 200 questions. Re-pushing a response replaces its answers.\n\n### The response\n\n`200`, with an outcome per item. The counts always add up to the number of items sent:\n\n```json\n{\n  \"source\": { \"id\": \"8d0f…\", \"name\": \"Zenchef\" },\n  \"property_id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"created\": 1, \"updated\": 0, \"skipped\": 1,\n  \"results\": [\n    { \"external_id\": \"zc-1001\", \"status\": \"created\", \"review_id\": \"5b2f…\" },\n    { \"external_id\": \"zc-1002\", \"status\": \"skipped\", \"review_id\": null, \"reason\": \"erased\" }\n  ]\n}\n```\n\nAn item is `skipped` — reported, never silently dropped — when:\n\n- `reason: \"erased\"` — its `email` belongs to a guest erased under GDPR, whose data cannot be re-introduced;\n- `reason: \"other_property\"` — its `external_id` already belongs to a different property in this source. A push never moves a record between properties.\n\n## Pagination\n\nList endpoints are cursor-paginated: pass the previous response's `next_cursor` as `?cursor=`, up to `?limit=` (max 100).\n\n## Rate limiting\n\nRequests are rate-limited per key (fixed window). A `429` response includes a `Retry-After` header — honour it before retrying.\n\n## Webhooks\n\nRegister endpoints to receive signed event deliveries (see **Webhook events**). Verify the `X-CA-Signature` HMAC on every delivery before trusting it.\n\nWebhook endpoints and event delivery are **account-scoped**: a `webhooks:manage` key manages every endpoint in the account and its endpoints receive events from the whole account, regardless of the node the key is bound to. Node binding applies to read endpoints only.\n\n## Property attributes\n\nProperty attributes are the account's own groupings of its properties, such as brand, operating model, region or theme. An administrator defines each attribute and its values under **Settings → Properties**, and gives each property one value per attribute, or none. Custom reports group and filter by them, and scheduled reports can be limited to them. The API exposes them so a BI tool can reproduce the same groups.\n\nTwo reads, both with `nodes:read`:\n\n- `GET /property-attributes` lists the attributes and their values, in the order the account set them.\n- `GET /nodes` gives each property an `attributes` list with one entry per attribute.\n\n```bash\ncurl https://app.customer-alliance.com/api/v4/nodes \\\n  -H \"Authorization: Bearer ca_live_your_key_here\"\n```\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"3fa85f64-…\", \"name\": \"Hotel Berlin\", \"parent_id\": \"9b1c…\", \"type\": \"property\",\n      \"attributes\": [\n        { \"attribute_id\": \"c2e1…\", \"attribute\": \"Brand\", \"value_id\": \"7d40…\", \"value\": \"Dorint\" },\n        { \"attribute_id\": \"e8a3…\", \"attribute\": \"Operating model\", \"value_id\": null, \"value\": null }\n      ]\n    }\n  ]\n}\n```\n\n### How the reports group properties\n\nTo match the reports exactly:\n\n- **None is a group.** A property with no value of an attribute is listed with `value_id: null`. The reports show those properties as their own None row, so keep them rather than drop them.\n- **A property inside another takes its value.** A property nested in another property (a hotel's spa, say) that holds no value of its own is grouped with the nearest property above it, and `GET /nodes` returns that inherited value.\n- **Groups hold no attributes.** Regions and other groups in the hierarchy have an empty `attributes` list. Group properties by their attribute values, not by where they sit in the tree.\n- **Join on ids, not names.** An attribute or value can be renamed and keeps its id. A value removed from an attribute no longer appears on any property.\n\nA key reads the values of the properties in its own subtree only. The attribute definitions are the account's, so every key with `nodes:read` lists them all.","termsOfService":"https://www.customer-alliance.com/en/terms-and-conditions/","contact":{"name":"Customer Alliance Support","email":"info@customer-alliance.com","url":"https://www.customer-alliance.com/"}},"externalDocs":{"description":"Customer Alliance","url":"https://www.customer-alliance.com/"},"servers":[{"url":"https://app.customer-alliance.com/api/v4","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Identity","description":"Who the key is."},{"name":"Reviews","description":"Collected reviews and owner replies, and pushing reviews in from other platforms."},{"name":"Analytics","description":"Score and rating aggregates."},{"name":"Nodes","description":"Hierarchy discovery, and the property attributes that group your properties."},{"name":"Surveys","description":"Surveys, responses and answers (PII-free), and pushing responses in from other survey tools."},{"name":"Insights","description":"AI recommendations, mentions and topics."},{"name":"Guests","description":"Push guests into a property (write-only; grants no read of guest data)."},{"name":"Webhooks","description":"Manage outbound webhook endpoints and inspect deliveries. Account-scoped: these endpoints ignore the key's node binding (unlike the read endpoints)."},{"name":"Webhook events","description":"The signed events Customer Alliance POSTs to your endpoints."}],"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`."}},"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"}}}},"NotFound":{"description":"Not found or outside the key's scope.","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"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}},"example":{"error":"Invalid API key"}},"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","example":"google"},"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"}}},"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"}}}}}},"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","example":"property"},"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","example":"Brand"},"value_id":{"type":["string","null"],"format":"uuid"},"value":{"type":["string","null"],"example":"Dorint"}}},"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","example":"Brand"},"values":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string","example":"Dorint"}}}}}},"Survey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"node_id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"published"},"primary_language":{"type":"string","example":"en"},"published_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"SurveyDetail":{"allOf":[{"$ref":"#/components/schemas/Survey"},{"type":"object","properties":{"published_definition":{"type":"object","description":"The published survey definition (pages/questions)."}}}]},"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","example":"completed"},"channel":{"type":"string","example":"email"},"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"}}},"SurveyAnswer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"question_id":{"type":"string"},"question_path":{"type":"string"},"question_type":{"type":"string","example":"rating"},"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"}}},"Recommendation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"body":{"type":"string"},"priority":{"type":"string","example":"high"},"impact":{"type":["string","null"]},"canonical_label_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"}}},"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","example":"text"},"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"}}},"Topic":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"language":{"type":["string","null"]},"parent_label_id":{"type":["string","null"],"format":"uuid"}}},"WhoAmI":{"type":"object","properties":{"account_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"scopes":{"type":"array","items":{"type":"string"}}},"example":{"account_id":"11111111-1111-1111-1111-111111111111","node_id":"42222222-2222-2222-2222-222222222222","scopes":["reviews:read","webhooks:manage"]}},"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"}}},"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."}},"example":{"name":"Prod integration","url":"https://example.com/hooks/ca","events":["review.received","survey.response_completed"]}},"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.","example":"whsec_XXXXXXXXXXXXXXXXXXXXXXXX"},"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}}},"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"}}},"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."}},"example":{"external_id":"BK-1001","status":"checked_out","check_in":"2026-08-01","check_out":"2026-08-04","channel":"booking.com"}},"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"}},"example":{"property_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","external_id":"CUST-48213","email":"jane.doe@example.com","first_name":"Jane","last_name":"Doe","marketing_consent":true,"reservation":{"external_id":"BK-1001","status":"checked_out","check_in":"2026-08-01","check_out":"2026-08-04","channel":"booking.com"}}},"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"}}},"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","example":"api"},"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."}}},"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"}}},"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."}}},"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"}}},"example":{"source":"Zenchef","property_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","rating_scale":5,"reviews":[{"external_id":"zc-1001","rating":4,"reviewed_at":"2026-09-01","text":"Lovely terrace, slow service.","author_name":"Jane D.","language":"en"}]}},"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."}}},"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"}}}},"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}}},"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"}}},"example":{"source":"Navili","property_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","rating_scale":5,"responses":[{"external_id":"nv-9","rating":4,"responded_at":"2026-09-01","answers":{"Cleanliness":5,"What could we improve?":"Breakfast could start earlier."}}]}},"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."}}},"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"}}}},"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)."}}},"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"}}},"ReviewRepliedData":{"type":"object","properties":{"review_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"reply_id":{"type":"string","format":"uuid"}}},"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"}}},"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"]}}},"GuestCreatedData":{"type":"object","properties":{"guest_id":{"type":"string","format":"uuid"},"node_id":{"type":"string","format":"uuid"},"source":{"type":"string","example":"import"}}},"GuestUnsubscribedData":{"type":"object","properties":{"guest_id":{"type":["string","null"],"format":"uuid"},"unsubscribe_id":{"type":"string","format":"uuid"}}}}},"paths":{"/whoami":{"get":{"summary":"The authenticated key's account, node and scopes","operationId":"whoami","tags":["Identity"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmI"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/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"}}},"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"}}}},"/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"}}}},"/analytics/scores":{"get":{"summary":"Aggregate reputation scores for the key's node subtree","description":"Requires `analytics:read`.","operationId":"getAnalyticsScores","tags":["Analytics"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsScores"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/nodes":{"get":{"summary":"List hierarchy nodes within the key's scope","description":"Requires `nodes:read`. Each property carries its value of every property attribute (`attributes`), as the reports group it. A group's list is empty. See **Property attributes**.","operationId":"listNodes","tags":["Nodes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Node"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/property-attributes":{"get":{"summary":"List the account's property attributes and their values","description":"Requires `nodes:read`. The attributes the account has defined (brand, operating model, region…) and each one's values, in the order the account set them. Account-wide: which property holds which value is on `GET /nodes`, within the key's subtree. See **Property attributes**.","operationId":"listPropertyAttributes","tags":["Nodes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PropertyAttribute"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/surveys":{"get":{"summary":"List published surveys whose owner node is in the key's subtree","description":"Requires `surveys:read`.","operationId":"listSurveys","tags":["Surveys"],"parameters":[{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"A page of surveys.","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Survey"}},"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"}}}},"/surveys/{id}":{"get":{"summary":"Get a single published survey (with its published definition)","description":"Requires `surveys:read`.","operationId":"getSurvey","tags":["Surveys"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SurveyDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/surveys/{id}/responses":{"get":{"summary":"List responses for a survey (newest first)","description":"Requires `surveys:read`. Respondent PII is not included.","operationId":"listSurveyResponses","tags":["Surveys"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"A page of responses.","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SurveyResponse"}},"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"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/surveys/{id}/responses/{responseId}/answers":{"get":{"summary":"List the answers for a single response","description":"Requires `surveys:read`.","operationId":"listResponseAnswers","tags":["Surveys"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"responseId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SurveyAnswer"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/survey-responses":{"post":{"summary":"Push survey responses from another tool into a property","description":"Requires `surveys:write`. Writes up to 100 completed responses, with their answers, from a survey tool Customer Alliance does not run, under a named `source` — the automated form of the review inbox's survey CSV upload. Each source's responses are kept in one survey managed for you and hidden from the survey endpoints and statistics; each response also appears in the inbox, and in `GET /reviews`, as an `upload` review. See **Pushing reviews and survey responses**.\n\nIn `answers`, a number answers a rating question and a string a free-text one. A question keeps the type — and, for a rating, the scale — it was first given: answering it otherwise later is a `422`, as is growing the source's survey past its question limit, and nothing is written. Identity is (source, `external_id`): a repeat push replaces that response's answers. Each item's outcome is in `results`.","operationId":"pushSurveyResponses","tags":["Surveys"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SurveyResponsePushInput"}}}},"responses":{"200":{"description":"The batch was processed; see each item's `status`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SurveyResponsePushResult"}}}},"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; or the batch contradicts a question's established type or scale, or would take the source's survey past its question limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/insights/recommendations":{"get":{"summary":"List AI recommendations for the account (newest first)","description":"Requires `insights:read`.","operationId":"listRecommendations","tags":["Insights"],"parameters":[{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"A page of recommendations.","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Recommendation"}},"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"}}}},"/insights/mentions":{"get":{"summary":"List topic mentions extracted from reviews in the key's subtree (newest first)","description":"Requires `insights:read`. Returns mentions from every review on your account's subtree by default — a mention is a snippet of a review you can already read, so the scope matches `GET /reviews`. If you are rendering mentions to the public, pass `?display=public`.\n\n**Mentions from responses to your Customer Alliance surveys are never returned here**, with or without `display=public`, for the same reason as `GET /reviews`: the response is already served by `GET /surveys/{id}/responses`.","operationId":"listMentions","tags":["Insights"],"parameters":[{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/display"}],"responses":{"200":{"description":"A page of mentions.","content":{"application/json":{"schema":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Mention"}},"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"}}}},"/insights/topics":{"get":{"summary":"List the canonical topic labels readable by the account","description":"Requires `insights:read`.","operationId":"listTopics","tags":["Insights"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Topic"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/guests":{"post":{"summary":"Create or update a guest at a property","description":"Requires `guests:write`. Push a guest into the Guests section — for a CRM/PMS/booking integration to feed guests in. Write-only: this scope grants no read of guest data. Identity is (property, `external_id`) when you send your own customer id in `external_id`, so a corrected email updates the same guest; without it, identity is (property, email), so a repeat push of the same email updates that guest (`200`) rather than creating a duplicate (`201` on first create), and a different email is a different guest. Include an optional `reservation` to upsert the guest's stay in the same call (identity (property, reservation.external_id)), so you can build audiences on stay status, booking channel, and recently-ended stays. A guest erased under GDPR is never repopulated (`409`).","operationId":"pushGuest","tags":["Guests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestPushInput"}}}},"responses":{"200":{"description":"Existing guest updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Guest"}}}},"201":{"description":"Guest created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Guest"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"The guest (or the email it is being corrected to) has been erased under GDPR, or `reservation.external_id` already belongs to a different guest. Nothing is written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"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"}}}},"/webhooks":{"get":{"summary":"List the account's outbound webhook endpoints","description":"Requires `webhooks:manage`. Signing secrets are never returned.","operationId":"listWebhooks","tags":["Webhooks"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Register an outbound webhook endpoint","description":"Requires `webhooks:manage`. The signing secret is returned exactly once — store it now.","operationId":"createWebhook","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointInput"}}}},"responses":{"201":{"description":"Created — secret shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreated"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Per-account endpoint limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}":{"patch":{"summary":"Update a webhook endpoint","description":"Requires `webhooks:manage`. Any subset of name / url / events / is_active.","operationId":"updateWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointInput"}}}},"responses":{"200":{"description":"OK"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"summary":"Delete a webhook endpoint (cascades its deliveries)","description":"Requires `webhooks:manage`.","operationId":"deleteWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{id}/deliveries":{"get":{"summary":"Recent delivery attempts for one endpoint (newest first)","description":"Requires `webhooks:manage`.","operationId":"listWebhookDeliveries","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhooks/{id}/test":{"post":{"summary":"Send a test `ping` delivery to one endpoint","description":"Requires `webhooks:manage`. Queues a signed ping regardless of the endpoint's subscribed events.","operationId":"testWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK — ping queued."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"webhooks":{"review.received":{"post":{"summary":"`review.received` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_review_received","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"review.received"},"data":{"$ref":"#/components/schemas/ReviewReceivedData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}},"review.replied":{"post":{"summary":"`review.replied` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_review_replied","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"review.replied"},"data":{"$ref":"#/components/schemas/ReviewRepliedData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}},"review.alert_triggered":{"post":{"summary":"`review.alert_triggered` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_review_alert_triggered","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"review.alert_triggered"},"data":{"$ref":"#/components/schemas/ReviewAlertTriggeredData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}},"survey.response_completed":{"post":{"summary":"`survey.response_completed` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_survey_response_completed","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"survey.response_completed"},"data":{"$ref":"#/components/schemas/SurveyResponseCompletedData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}},"guest.created":{"post":{"summary":"`guest.created` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_guest_created","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"guest.created"},"data":{"$ref":"#/components/schemas/GuestCreatedData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}},"guest.unsubscribed":{"post":{"summary":"`guest.unsubscribed` delivered to your endpoint","description":"Your endpoint receives this POST. Respond 2xx to acknowledge; non-2xx (and 3xx) are retried with backoff.","operationId":"webhook_guest_unsubscribed","tags":["Webhook events"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"const":"guest.unsubscribed"},"data":{"$ref":"#/components/schemas/GuestUnsubscribedData"}}}]}}}},"responses":{"200":{"description":"Acknowledged."}}}}}}