Reviews
Every Reviews endpoint with a cURL, Python and Node sample: sources and settings, list, rollup, details, respond, edit, archive, phrases, and analytics.
Reviews are the star ratings and comments customers leave on Google, Facebook
and Yelp. The API stores one copy of each, lets you reply, and computes the
numbers behind a scorecard. The REST paths say reviews; the response keys
and reference page titles say interactions, the older internal name for the
same object. This guide says review throughout. Reads need a Read key.
Replying, editing, archiving and changing settings need a Write key; a
Read key on a write returns 400 with SY90016.
Endpoints at a glance
List the review sources your plan allows
GET /reviews/site-config returns the catalog of review platforms the
account is entitled to. It takes no parameters and is account-scoped, so
cache it rather than calling it per location. The siteUrl values are the
hostnames every other filter in this resource accepts.
curl https://listingsapi.com/api/v4/reviews/site-config \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "interactionSiteConfig": [ { "site": "Google", "siteUrl": "maps.google.com" }, { "site": "Facebook", "siteUrl": "facebook.com" }, { "site": "Yelp", "siteUrl": "yelp.com" } ] }}Read data.interactionSiteConfig[]. The example is a Launch-plan account;
the exact set depends on plan and vertical. Use siteUrl for the siteUrls
filter on the list endpoints and for siteUrls[].name when editing
settings.
List the sources configured for a location
GET /locations/{locationId}/reviews/settings returns one entry per
platform the plan supports, with the URL reviews are pulled from. It is the
read side of the edit endpoint below. The path accepts the base64 id or the
plain number.
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/reviews/settings \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "interactionsSetting": { "siteSettings": [ { "name": "maps.google.com", "url": null }, { "name": "facebook.com", "url": null }, { "name": "yelp.com", "url": null } ] } }}Read data.interactionsSetting.siteSettings[]. A non-null url means the
platform is connected and reviews are being pulled from that page. A null
url means the platform is available but not configured. Google and
Facebook fill in on their own when you connect those accounts through OAuth
and cannot be overwritten by the edit endpoint. Yelp has to be named
explicitly.
Edit a location's review settings
POST /locations/reviews/settings/edit sets which sources a location is
monitored on and the star rating that counts as positive. Fields go directly
in the body, not in an input wrapper. Adding a source queues a scrape, so
the new site's reviews arrive later, not in this response.
curl -X POST https://listingsapi.com/api/v4/locations/reviews/settings/edit \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "positiveInteractionThreshold": 4, "siteUrls": [ { "name": "maps.google.com", "url": null }, { "name": "yelp.com", "url": "https://www.yelp.com/biz/blue-bottle-coffee-oakland" } ]}'{ "data": { "editInteractionsSetting": { "errors": null, "interactionSetting": { "locationId": "1800289", "positiveInteractionThreshold": 4, "siteSettings": [ { "name": "maps.google.com", "url": null, "monitored": true, "canRespond": true }, { "name": "yelp.com", "url": "https://www.yelp.com/biz/blue-bottle-coffee-oakland", "monitored": true, "canRespond": false } ] } } }}Read data.editInteractionsSetting.interactionSetting.siteSettings[] for the
stored selection. monitored says the source is active; canRespond says
whether owner replies can be posted there through the API. A source rejected
by validation, such as an unreachable URL or a site outside the plan, comes
back in errors with the setting unchanged, so check errors on a 200.
The SDKs raise ValidationError in that case.
List reviews for a location
GET /locations/{locationId}/reviews is a cursor connection at
data.interactions, newest first. first defaults to 20, so one call is
never the full history. Array filters are JSON strings on the query string
and must be URL-encoded; the cURL sample uses -G with --data-urlencode
for that.
Plus the optional category, categories, and single-value ratingFilter
listed on the reference page.
curl -G "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/reviews" \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'ratingFilters=[1,2]' \--data-urlencode 'responseStatus=["PENDING"]' \--data-urlencode 'sortOrder=["NEWEST_FIRST"]' \--data-urlencode 'first=25'{ "data": { "interactions": { "pageInfo": { "hasNextPage": true, "endCursor": "SW50ZXJhY3Rpb246MV8xNTU0..." }, "edges": [ { "node": { "id": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "source": "maps.google.com", "content": "nice place for family together", "authorName": "vaibhav nakate", "rating": 5, "date": "2019-04-02T22:11:13.036997+00:00", "type": "Review", "responded": false, "responseCount": 0, "canRespond": true, "permalink": "https://maps.google.com/maps?cid=..." }, "cursor": "SW50ZXJhY3Rpb246MV8xNTU0..." } ], "totalCount": 425 } }}Read edges[].node. The id is what you pass when replying, and
canRespond says whether that source accepts owner replies. totalCount is
the number of matches across the whole result set, not this page. Stop
paging when hasNextPage is false or a page returns empty edges; a
boundary that lands on the oldest review is followed by one empty page.
Sweep every location with the rollup
GET /rollup_interactions is the same cursor connection across every
location the key can see, at data.rollupInteractions, with a numeric
locationId on each node. It takes the same filters as the per-location
list. There is no location filter; a locationIds argument is ignored, not
rejected. Neither SDK wraps this endpoint yet, so the samples use requests
and fetch.
curl -G "https://listingsapi.com/api/v4/rollup_interactions" \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'responseStatus=["PENDING"]' \--data-urlencode 'sortOrder=["NEWEST_FIRST"]' \--data-urlencode 'first=50'{ "data": { "rollupInteractions": { "pageInfo": { "hasNextPage": true, "hasPreviousPage": false }, "edges": [ { "node": { "id": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "source": "maps.google.com", "content": "Excellent service, highly recommend.", "rating": 5, "responded": false, "canRespond": true, "locationId": 1800289 }, "cursor": "SW50ZXJhY3Rpb246OWUyNmQyYTg=" } ], "totalCount": 1284 } }}Group nodes by locationId, the databaseId of the store. The rollup's
pageInfo may omit endCursor, so take the last edge's cursor as after;
the two values are equivalent. Bound scheduled runs with startDate and
endDate rather than re-reading history, and honor Retry-After on a
429.
Fetch reviews by id
GET /reviewDetails returns fresh copies of stored reviews without paging.
The response is a plain array at data.interactionDetails.interactions, no
edges or pageInfo, and each review includes its responses. Use it to
re-hydrate a watchlist and to find the responseId you need for edit or
archive.
curl -G "https://listingsapi.com/api/v4/reviewDetails" \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'interactionIds=["b2fa765e-c62b-4e0b-b1d6-1c67c855f5e0"]'{ "data": { "interactionDetails": { "interactions": [ { "id": "b2fa765e-c62b-4e0b-b1d6-1c67c855f5e0", "source": "maps.google.com", "content": "Great place for dessert!", "rating": 5, "responded": true, "responseCount": 1, "canRespond": true, "responses": [ { "id": "2090753a-ece6-4837-8336-8494ad308523", "content": "Thank you so much for the kind words!", "interactionStatus": "CONFIRMED", "editedResponse": false } ] } ] } }}responses[].id is the responseId for the edit and archive endpoints. An
id from another account returns 200 with interactionDetails: null and an
SY90001 error, so check for null before indexing.
Review and reply fields
The attributes reference is a docs-only page that lists every field on the review object and the reply object. The fields you will use most:
Each reply in responses[] carries id, content, authorName, date,
interactionStatus, editedResponse, respondedWith (template id) and
remarks (failure detail). interactionStatus moves through these states:
Post an owner reply
POST /locations/reviews/respond posts a public reply on the source
platform. Fields go directly in the body. The reply is accepted at once and
delivered asynchronously, so interactionStatus starts at CREATED.
Check canRespond on the review first.
curl -X POST https://listingsapi.com/api/v4/locations/reviews/respond \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "interactionId": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "responseContent": "Thank you for visiting Jenny Home: we are glad you enjoyed your experience!"}'{ "data": { "respondToInteraction": { "interaction": { "id": "2090753a-ece6-4837-8336-8494ad308523", "interactionId": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "content": "Thank you for visiting Jenny Home: we are glad you enjoyed your experience!", "interactionStatus": "CREATED", "editedResponse": false }, "errors": null } }}Keep interaction.id: it is the responseId for edit and archive. A
rejected reply comes back as 200 with errors populated inside
data.respondToInteraction, so read the body; the SDKs raise
ValidationError. Re-read the review by id after a short wait to see the
reply reach COMPLETED or CONFIRMED.
Edit a reply
POST /locations/reviews/respond/edit changes the text of an existing reply.
The edit is re-submitted to the platform, so interactionStatus returns to
CREATED and editedResponse becomes true once it lands. Prefer this over
archive and re-post on platforms that allow one reply.
curl -X POST https://listingsapi.com/api/v4/locations/reviews/respond/edit \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "reviewId": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "responseId": "2090753a-ece6-4837-8336-8494ad308523", "responseContent": "Thank you again: we have updated our reply to include our new store hours."}'{ "data": { "editResponse": { "status": "true", "reviewId": "9e26d2a8-a6ed-42ea-8454-47d1c92ea841", "responseId": "2090753a-ece6-4837-8336-8494ad308523" } }}status is the string "true" when the edit is accepted. Confirm the new
wording with a details call and check the reply's interactionStatus.
Archive a reply
POST /locations/reviews/respond/archive removes a reply from the review's
tracked responses on our side. It is a soft delete: a reply already live on
Google, Yelp or Facebook stays there. Use it to clear a draft that failed or
was superseded; use edit for anything published.
curl -X POST https://listingsapi.com/api/v4/locations/reviews/respond/archive \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "responseId": "2090753a-ece6-4837-8336-8494ad308523" }'{ "data": { "archiveResponse": { "status": "true" } }}After archiving, the review's responseCount drops and responded reflects
the remaining replies.
List the phrases customers repeat
GET /review-phrases returns a plain array at data.newReviewPhrases, one
entry per recurring phrase, each with stats computed over only the reviews
containing it. It is not paginated; cap it with phraseCount. Every filter
is optional and they compose, so ratingFilters=[1,2] with a startDate
gives last quarter's complaints.
Plus the optional categories and the deprecated single-value category and
ratingFilter listed on the reference page.
curl -G "https://listingsapi.com/api/v4/review-phrases" \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'locationIds=[1800289]' \--data-urlencode 'ratingFilters=[1,2]' \--data-urlencode 'startDate=2026-06-01' \--data-urlencode 'endDate=2026-08-31' \--data-urlencode 'phraseCount=10'{ "data": { "newReviewPhrases": [ { "reviewPhrase": "friendly staff", "phraseStats": { "stats": [ { "name": "total-reviews", "value": 46, "delta": 12.5, "abs": 5 }, { "name": "overall-rating", "value": 4.7, "delta": 1.1, "abs": 0.05 } ] } }, { "reviewPhrase": "long wait", "phraseStats": { "stats": [ { "name": "total-reviews", "value": 11, "delta": 37.5, "abs": 3 }, { "name": "overall-rating", "value": 2.9, "delta": -8.4, "abs": -0.27 } ] } } ] }}total-reviews is how often the phrase appears, overall-rating is the
average rating of those reviews, delta is the percentage change against
the previous period and abs is the absolute change. A 403 with SY90003
means the key cannot read one of the requested locations.
Read the review KPIs for a location
GET /locations/{locationId}/review-analytics-overview returns four stats at
data.interactionsAnalyticsStats.stats. The date range is optional; it
narrows the window the values and their deltas are computed over. Each
delta is the percentage change against the previous period of the same
length.
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/review-analytics-overview?startDate=2026-08-01&endDate=2026-08-31" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "interactionsAnalyticsStats": { "stats": [ { "name": "total-reviews", "value": 128, "delta": 6.67 }, { "name": "new-reviews", "value": 9, "delta": -25 }, { "name": "overall-rating", "value": 4.43, "delta": 2.31 }, { "name": "review-response-rate", "value": 62.5, "delta": 12.5 } ] } }}A fresh account with no prior period reports a delta of 100, so do not
read the first month's deltas as growth.
Pull the daily review timeline
GET /locations/{locationId}/review-analytics-timeline returns one entry per
day at data.interactionsChartData.data. Pass startDate and endDate
whenever the chart's range matters; omitting them lets the service pick its
own window. Add site to draw one source on its own.
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/review-analytics-timeline?startDate=2026-08-01&endDate=2026-08-31&site=maps.google.com" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "interactionsChartData": { "data": [ { "date": "2026-08-01", "interactionCount": 4, "averageRating": 4.5 }, { "date": "2026-08-02", "interactionCount": 0, "averageRating": null }, { "date": "2026-08-03", "interactionCount": 7, "averageRating": 4.285714285714286 } ] } }}Plot interactionCount as bars and averageRating as a line. The average
is that day's alone, not a running one, and it is null on a day with no
reviews, so leave a gap rather than drawing zero.
Break review analytics down by source
GET /locations/{locationId}/review-analytics-sites-stats returns one entry
per review source at data.interactionsSitesStats.stats. Use it to find
which platform moved the average.
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/review-analytics-sites-stats?startDate=2026-08-01&endDate=2026-08-31" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "interactionsSitesStats": { "stats": [ { "key": "google", "siteUrl": "maps.google.com", "averageRating": { "value": 4.6, "delta": 2.2 }, "totalInteractions": { "value": 412, "delta": 6.1 }, "newInteractions": { "value": 24, "delta": 33.3 }, "recency": "2026-09-06T14:12:00Z", "baseRating": 4.5, "baseInteractions": 388, "humanizedRecency": "3 days ago" }, { "key": "facebook", "siteUrl": "facebook.com", "averageRating": { "value": 4.1, "delta": -1.4 }, "newInteractions": { "value": 0, "delta": -100 }, "recency": null, "humanizedRecency": null } ] } }}averageRating, totalInteractions and newInteractions each carry a
value and a delta; baseRating and baseInteractions are the values at
the start of the period the delta is measured from. recency is the newest
review's timestamp, null when the source has never returned one.
newInteractions of 0 with a recency months old usually means the
source connection went stale, so check the location's sources.
Errors you will see
Mutations can also return 200 with errors populated inside data.<op>,
so read the body of every write. The full list is in
Error codes.
Next steps
- Fetch reviews: build a pending queue and get told by webhook.
- Respond to reviews: reply, confirm, edit, archive.
- Review analytics: scorecard, chart, complaints panel, daily snapshots.
- Analytics: Google, Facebook and Bing profile metrics for the same location.
- Reference landing: Reviews. SDKs: Python reviews, Node reviews.