> 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/pushing-reviews-and-survey-responses.md).

# Pushing reviews and survey responses

Feed 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:

* `POST /reviews` (`reviews:write`) pushes reviews.
* `POST /survey-responses` (`surveys:write`) pushes survey responses with their answers.

Both scopes need a plan that includes bulk import, and can only be granted by someone allowed to reply to reviews.

#### Where the data goes

Pushed data lands exactly where a CSV upload lands:

* in the **review inbox**, under the source name you send (for example `Zenchef`), filterable like any other source;
* in **sentiment and AI insights**;
* 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;
* **not** in reputation scores, KPIs or trends, including `GET /analytics/scores` — those reflect reviews from connected portals only;
* **not** on public surfaces: widgets, certificates and `?display=public` exclude imported reviews.

Pushed 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.

#### The batch

A request carries one `source`, one `rating_scale` (`5`, `10` or `100`) and 1–100 items for one property:

```bash
curl -X POST https://app.customer-alliance.com/api/v4/reviews \
  -H "Authorization: Bearer ca_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "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" }
    ]
  }'
```

* **`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.
* **`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`.
* **`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.
* **`email`** (optional) links the item to a guest with that email at the property, and makes it reachable by GDPR erasure and data export.
* 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.

Validation 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.

#### Survey answers

`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:

```json
{ "external_id": "nv-9", "rating": 4, "responded_at": "2026-09-01",
  "answers": { "Cleanliness": 5, "What could we improve?": "Breakfast could start earlier." } }
```

A 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.

#### The response

`200`, with an outcome per item. The counts always add up to the number of items sent:

```json
{
  "source": { "id": "8d0f…", "name": "Zenchef" },
  "property_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created": 1, "updated": 0, "skipped": 1,
  "results": [
    { "external_id": "zc-1001", "status": "created", "review_id": "5b2f…" },
    { "external_id": "zc-1002", "status": "skipped", "review_id": null, "reason": "erased" }
  ]
}
```

An item is `skipped` — reported, never silently dropped — when:

* `reason: "erased"` — its `email` belongs to a guest erased under GDPR, whose data cannot be re-introduced;
* `reason: "other_property"` — its `external_id` already belongs to a different property in this source. A push never moves a record between properties.
