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

MethodPathWhat it doesReference
GET/reviews/site-configReview sources the account's plan allows.List review sources for an account
GET/locations/{locationId}/reviews/settingsSources configured for one location.List review sources for a location
POST/locations/reviews/settings/editSet a location's sources and positive threshold.Edit review settings
GET/locations/{locationId}/reviewsCursor list of one location's reviews.List reviews
GET/rollup_interactionsCursor list across every location.Get rollup reviews
GET/reviewDetailsFresh copies of reviews by id, with replies.Get reviews by ids
Field reference for review and reply objects.Review attributes
POST/locations/reviews/respondPost an owner reply.Respond to a review
POST/locations/reviews/respond/editChange a reply's text.Edit a response
POST/locations/reviews/respond/archiveRemove a reply from tracking.Archive a response
GET/review-phrasesPhrases customers repeat, with stats.List review phrases
GET/locations/{locationId}/review-analytics-overviewFour KPIs with period deltas.Review analytics overview
GET/locations/{locationId}/review-analytics-timelineDaily count and average rating.Review analytics timeline
GET/locations/{locationId}/review-analytics-sites-statsPer-source rating, volume, recency.Review analytics by site

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.

NameInRequiredNotes
noneNo parameters.
Account sources
curl https://listingsapi.com/api/v4/reviews/site-config \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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.

NameInRequiredNotes
locationIdpathYesBase64 Relay id or numeric databaseId.
Location sources
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/reviews/settings \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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.

NameInRequiredNotes
locationIdbodyYesBase64 Relay id only. A numeric id in the body fails with SY90002.
siteUrlsbodyNoThe complete set of sources, as name and url pairs. Replaces the current selection.
siteUrls[].namebodyNoSite key from siteSettings[].name, such as maps.google.com or yelp.com.
siteUrls[].urlbodyNoThe location's page on that site. null or omitted stops monitoring a source that needs a URL.
positiveInteractionThresholdbodyNoRating at or above which a review counts as positive. Sent alone, it leaves the sources untouched.
Edit settings
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" }  ]}'
JSON
{  "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.

NameInRequiredNotes
locationIdpathYesBase64 Relay id or numeric databaseId.
first, afterqueryNoForward paging. Pass pageInfo.endCursor as after.
last, beforequeryNoBackward paging. pageInfo has no startCursor, so pass the first edge's cursor.
siteUrlsqueryNoJSON array of hostnames, for example ["maps.google.com"].
ratingFiltersqueryNoJSON array of star ratings, for example [1,2].
responseStatusqueryNoJSON array: ["PENDING"] or ["RESPONDED"].
sortOrderqueryNoJSON array: ["NEWEST_FIRST"], ["OLDEST_FIRST"] or ["LAST_RESPONDED"].
startDate, endDatequeryNoYYYY-MM-DD, plain strings.
searchStringqueryNoFull-text search on the review body.

Plus the optional category, categories, and single-value ratingFilter listed on the reference page.

List reviews
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'
JSON
{  "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.

FilterValues
responseStatusRESPONDED, PENDING
sortOrderNEWEST_FIRST, OLDEST_FIRST, LAST_RESPONDED
categoryREVIEW, plus other buckets exposed per account

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.

NameInRequiredNotes
tagqueryNoLocation tag to scope to. Defaults to all. Omit unless you tag locations.
first, after, last, beforequeryNoCursor paging, as on the list endpoint.
siteUrls, ratingFilters, responseStatus, sortOrderqueryNoJSON arrays, URL-encoded, same values as the list endpoint.
startDate, endDate, searchStringqueryNoSame as the list endpoint.
Account rollup
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'
JSON
{  "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.

NameInRequiredNotes
interactionIdsqueryYesJSON array of review UUIDs, URL-encoded. Omitting it returns SY90006.
Details by id
curl -G "https://listingsapi.com/api/v4/reviewDetails" \-H "Authorization: API $LISTINGSAPI_KEY" \--data-urlencode 'interactionIds=["b2fa765e-c62b-4e0b-b1d6-1c67c855f5e0"]'
JSON
{  "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:

FieldTypeMeaning
id, interactionIdstringThe same UUID; pass it as interactionId when replying.
sourcestringHostname of the site, such as maps.google.com.
content, authorName, rating, datemixedBody text, reviewer, 1 to 5 stars, ISO 8601 timestamp.
typestringReview, Question, Comment, Recommendation, Post, Photo, Message or Response.
categorystringHigh-level bucket, Review or Social.
responded, responseCount, responsesmixedWhether an owner reply exists, how many, and the reply objects.
canRespondbooleanWhether a reply can be posted to this source through the API.
permalinkstringLink to the review on the source site.
locationIdintegerNumeric databaseId of the location.

Each reply in responses[] carries id, content, authorName, date, interactionStatus, editedResponse, respondedWith (template id) and remarks (failure detail). interactionStatus moves through these states:

StatusMeaning
CREATEDAccepted and queued for the publisher.
COMPLETEDPosted to the publisher. Some platforms stop here.
CONFIRMEDPosted and confirmed by the publisher.
FAILEDThe publisher rejected it. remarks says why.

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.

NameInRequiredNotes
interactionIdbodyYesThe review's id.
responseContentbodyYesReply text.
respondedWithbodyNoResponse-template id, if the reply came from a template.
Respond
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!"}'
JSON
{  "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.

NameInRequiredNotes
reviewIdbodyYesThe review's id.
responseIdbodyYesThe reply's id from responses[].
responseContentbodyYesNew reply text.
templateIdbodyNoResponse-template id to re-attribute the reply to.
Edit 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."}'
JSON
{  "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.

NameInRequiredNotes
responseIdbodyYesThe reply's id from responses[].
Archive reply
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" }'
JSON
{  "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.

NameInRequiredNotes
locationIdsqueryNoJSON array of numeric location ids, for example [1800289]. Omit to cover every location the key can read.
siteUrlsqueryNoJSON array of hostnames.
startDate, endDatequeryNoYYYY-MM-DD.
ratingFiltersqueryNoJSON array of star ratings.
responseStatusqueryNoJSON array of RESPONDED or PENDING.
sortOrderqueryNoNEWEST_FIRST, OLDEST_FIRST, LAST_RESPONDED, LOWEST_RATING or HIGHEST_RATING.
phraseCountqueryNoMaximum phrases to return.
searchStringqueryNoOnly phrases containing this substring.

Plus the optional categories and the deprecated single-value category and ratingFilter listed on the reference page.

Phrases
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'
JSON
{  "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.

NameInRequiredNotes
locationIdpathYesBase64 Relay id or numeric databaseId.
startDate, endDatequeryNoYYYY-MM-DD. Optional here, unlike the timeline.
Overview
curl "https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/review-analytics-overview?startDate=2026-08-01&endDate=2026-08-31" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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 }      ]    }  }}
namevalueRead it as
total-reviewscountAll reviews for the location.
new-reviewscountReviews received in the period.
overall-rating1 to 5Average star rating.
review-response-ratepercentShare of reviews with an owner reply.

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.

NameInRequiredNotes
locationIdpathYesBase64 Relay id or numeric databaseId.
startDate, endDatequeryNoYYYY-MM-DD. Pass both for a predictable window.
sitequeryNoOne hostname from siteSettings[].name, such as maps.google.com. Neither SDK exposes it; use the REST call.
Timeline
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"
JSON
{  "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.

NameInRequiredNotes
locationIdpathYesBase64 Relay id or numeric databaseId.
startDate, endDatequeryNoYYYY-MM-DD. Deltas compare this period against the previous one of equal length.
By source
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"
JSON
{  "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

CodeMeaningFix
SY90016Read key used on a write.Use a Write key.
SY90001Not authorized: bad key, or a review id from another account. On /reviewDetails it arrives as 200 with interactionDetails: null.Check the key and the id.
SY90002Invalid id. A numeric locationId in the settings-edit body.Send the base64 Relay id.
SY90003403, the key cannot read or change this location.Use a key with access to it.
SY90006interactionIds missing on /reviewDetails.Pass a JSON array of ids.
SY50121Second owner reply to a Google review.Edit the existing reply.
429Rate limit.Wait for Retry-After. See Rate limits.

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