GuideAnleitungen/Reviews

Diese Anleitung ist auf Englisch verfügbar.

How do I fetch and respond to reviews through an API?

How to Fetch and Respond to Reviews via API

Fetch reviews from Google, Facebook and custom review sites, filter them, post owner replies, add internal notes and read review analytics with the Synup API.

Auf einen Blick

Was diese Anleitung leistet
Pull every review for a location across platforms, find the ones that need attention, post a reply that is pushed to the source platform, and measure the results.
Verwendete APIs
Referenz
Reviews
Voraussetzungen
  • A location with at least one connected review source (Google or Facebook) or a custom review URL
  • An API key with read and write access to Reviews
  • The location id and its client id, which every reviews call requires together
Typische Anwendungsfälle
  • A review inbox inside your own product
  • An automated workflow that drafts replies for negative reviews and routes them for approval
  • A reporting feature that shows rating, volume and response rate per location

Two calls do the core of it. A paginated GET pulls a location's reviews, from every connected platform plus any custom pages you added, each with its rating, text, sentiment and reply state. A POST sends an owner reply, which Synup pushes back to the platform the review came from. The rest is the filters that make the inbox usable and the analytics that turn a pile of reviews into the one number an owner actually watches.

How reviews flow through the API

  1. PlatformsGoogle, Facebook, plus custom pages such as Yelp or TripAdvisor
  2. Synup review syncReviews fetched, scored for sentiment, deduplicated
  3. Your inboxGET /api/v1/reviewsFiltered by status, rating, sentiment, date
  4. ReplyPOST /api/v1/reviews/replyPushed to the source platform
  5. MeasureGET /api/v1/reviews/analyticsRating, volume, response rate, themes
From the platform to your inbox and back

Every reviews call takes locationId and clientId together. Where the location came from doesn't matter. The Google Business Profile guide covers connecting the account that brings Google reviews in.

Review sources

GET /api/v1/reviews/sources lists where a location's reviews come from: the connected platforms plus any custom review-page URLs, each with its status and whether it's hidden. To add a page that isn't a connected platform, a Yelp or TripAdvisor listing say, post its URL to POST /api/v1/reviews/sources and Synup detects the platform from the URL. It doesn't validate the URL on the way in, so a wrong one surfaces later as a failed fetch on that source, not as an error on the add call.

curl "https://ai.synup.com/api/v1/reviews/sources?locationId=LOCATION_ID&clientId=CLIENT_ID" \
  -H "Authorization: Bearer $SYNUP_API_KEY"

curl -X POST https://ai.synup.com/api/v1/reviews/sources \
  -H "Authorization: Bearer $SYNUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "locationId": "LOCATION_ID", "clientId": "CLIENT_ID", "url": "https://www.yelp.com/biz/grove-street-dental-san-francisco" }'

Fetch the reviews that need attention

GET /api/v1/reviews returns a page of reviews for one location, up to 100 per page, with a nextCursor. The filters combine, so an inbox view is one request: status=unreplied for what is waiting, ratings=1,2 for the ones that hurt, sentiment=negative when the text is worse than the stars, from and to for a window, sort=newest for a feed.

curl "https://ai.synup.com/api/v1/reviews?locationId=LOCATION_ID&clientId=CLIENT_ID&status=unreplied&ratings=1,2,3&sort=newest&limit=100" \
  -H "Authorization: Bearer $SYNUP_API_KEY"
const API = "https://ai.synup.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.SYNUP_API_KEY}`,
  "Content-Type": "application/json",
};

async function* reviews(locationId, clientId, filters = {}) {
  let cursor = "";
  do {
    const params = new URLSearchParams({ locationId, clientId, limit: "100", cursor, ...filters });
    const res = await fetch(`${API}/reviews?${params}`, { headers });
    if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
    const { data } = await res.json();
    yield* data.reviews; // { id, author, rating, body, reviewDate, platform, sentiment, reply, reviewUrl, respondable }
    cursor = data.nextCursor ?? "";
  } while (cursor);
}

for await (const review of reviews("LOCATION_ID", "CLIENT_ID", { status: "unreplied", ratings: "1,2,3", sort: "newest" })) {
  console.log(review.rating, review.platform, review.body.slice(0, 80));
}
import os
import requests

API = "https://ai.synup.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SYNUP_API_KEY']}"}

def reviews(location_id, client_id, **filters):
    cursor = ""
    while True:
        params = {"locationId": location_id, "clientId": client_id, "limit": 100, "cursor": cursor, **filters}
        r = requests.get(f"{API}/reviews", headers=HEADERS, params=params, timeout=30)
        r.raise_for_status()
        data = r.json()["data"]
        yield from data["reviews"]  # id, author, rating, body, reviewDate, platform, sentiment, reply, reviewUrl, respondable
        cursor = data.get("nextCursor") or ""
        if not cursor:
            return

for review in reviews("LOCATION_ID", "CLIENT_ID", status="unreplied", ratings="1,2,3", sort="newest"):
    print(review["rating"], review["platform"], review["body"][:80])

Each review also carries a topics array, the themes Synup pulled out of the text (wait times, staff, pricing), which is what a per-topic inbox filter keys on. And it carries respondable, which is false once a review has been removed from its source or comes from a source that doesn't accept replies. Check it before offering a reply button. Pass deleted=1 to list the reviews that have disappeared from their platform instead of the live ones.

Post a reply

POST /api/v1/reviews/reply takes the review id and the reply text, posts the owner reply and pushes it to the source platform. The response tells you whether the push happened now (pushed: true, status: posted) or is queued (pending), and when it failed whether a retry might succeed (retryable).

curl -X POST https://ai.synup.com/api/v1/reviews/reply \
  -H "Authorization: Bearer $SYNUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reviewId": "REVIEW_ID",
    "text": "Thank you for letting us know. I am sorry the wait was longer than it should have been. Please call us on 415 555 0142 and ask for Maria so we can make it right."
  }'
async function reply(reviewId, text) {
  const res = await fetch(`${API}/reviews/reply`, {
    method: "POST",
    headers,
    body: JSON.stringify({ reviewId, text }),
  });
  if (res.status === 422) return { status: "not_respondable", error: (await res.json()).error };
  if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error}`);
  const { data } = await res.json();
  return data; // { ok, pushed, status: posted|pending|failed|draft, externalReplyId, retryable }
}
def reply(review_id, text):
    r = requests.post(f"{API}/reviews/reply", headers=HEADERS, timeout=30,
                      json={"reviewId": review_id, "text": text})
    if r.status_code == 422:
        return {"status": "not_respondable", "error": r.json()["error"]}
    r.raise_for_status()
    return r.json()["data"]  # ok, pushed, status (posted|pending|failed|draft), externalReplyId, retryable

A 422 means the review can't be replied to, usually because it was removed from its source. A failed push with retryable: true is worth one retry after a pause. With retryable: false, stop retrying and have someone look at the platform.

Keep the internal conversation out of the reply

POST /api/v1/reviews/notes attaches a note to a review that only your team sees, and GET /api/v1/reviews/notes reads them back. Use notes for the handoff: "escalated to the store manager", "customer called back", "draft reply awaiting approval". They never reach the platform.

curl -X POST https://ai.synup.com/api/v1/reviews/notes \
  -H "Authorization: Bearer $SYNUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reviewId": "REVIEW_ID", "body": "Escalated to the downtown manager; reply drafted, awaiting approval." }'

Measure

Three endpoints turn the review list into reporting, from portfolio-wide down to a single topic.

  • GET /api/v1/reviews/summary rolls up rating distribution, volume and trend across a client or the whole agency for a date range, filterable by tags. One call for a portfolio view.
  • GET /api/v1/reviews/analytics is the per-location deep dive: KPIs with period-over-period deltas over 7, 30 or 90 days, a monthly trend of volume, rating, sentiment mix and response rate over 3, 6 or 12 months, a per-platform breakdown, day-of-week timing, an agency benchmark, themes and an AI summary.
  • GET /api/v1/reviews/topic-trends tracks what customers talk about: a time series per topic, rising and declining movers, and emerging or faded topics over 30, 90 or 180 days.
curl "https://ai.synup.com/api/v1/reviews/analytics?locationId=LOCATION_ID&clientId=CLIENT_ID&kpiDays=30&trendMonths=6" \
  -H "Authorization: Bearer $SYNUP_API_KEY"

Response rate is the number most owners watch. It comes straight from the analytics trend, and it is the metric an automated reply workflow moves first.

Building an automated review-response workflow

Today you poll for this. review.received sits in the webhook event catalog as a planned event, and the loop below switches to it unchanged once it goes live.

  1. PollGET /reviews?status=unreplied&sort=newestEvery 15 minutes per active location
  2. Route4 and 5 stars: auto-reply template. 3 and below: draft plus human approval
  3. DraftPOST /reviews/notesStore the draft as a note so the team sees it in Synup too
  4. ApproveYour UI, or a Slack message with an approve button
  5. ReplyPOST /reviews/replyCheck pushed and status; retry once if retryable
A safe automation loop

Track the review ids you've already seen so an edit on the platform doesn't read as a new review, and never reply twice. reply on the review row is non-null once a reply exists.

Gotchas

SituationWhat happensWhat to do
clientId left out400Both ids are required on every reviews call.
Reply to a removed review422Mark it as not respondable in your inbox.
Reply to a custom-source review (Yelp, TripAdvisor)respondable: false, reply failsRead-only. Reply on the platform itself.
status: pending on a replyPush queued, not yet confirmedRe-read the review later. reply is set when it lands.
status: failed, retryable: trueTemporary platform errorRetry once after a minute.
Same review under two sourcesTwo rows with different platformsExpected. They are separate listings' reviews.