Find and suppress duplicate listings
List the duplicate listings detected for a location and across the account, confirm the real ones for suppression, clear false positives, and track each through to fixed.
A duplicate is a second entry for the same business on the same directory. It splits reviews and ranking signals and confuses customers. Detection runs on its own; your job is to review what it found, confirm the true duplicates, and dismiss the ones that are legitimate.
Every duplicate carries a status that tells you where it is in the cleanup:
1. List duplicates for one location
GET /locations/{locationId}/listings/duplicates returns a plain array of
site groups. Each group is a directory with a listings array of suspected
duplicates, and each entry has a listingItemId, a liveLink you can open to
judge it, and a status.
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/listings/duplicates \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "duplicateListingsForLocation": [ { "id": "U2l0ZToxNTA=", "name": "City Squares", "url": "citysquares.com", "listings": [ { "id": "RHVwbGljYXRlOjIzNTk4NDUw", "listingItemId": "TGlzdGluZ0l0ZW06MjM1OTg0NTA=", "name": "Woodbury Train Station", "liveLink": "http://citysquares.com/b/woodbury-train-station-23598450", "status": "POTENTIAL" } ] } ] }}An empty array means nothing was detected. Keep listingItemId: it is the
value both mark endpoints take. The Node SDK flattens the groups into one
array per duplicate, so d.id there is the value to pass on.
2. Roll up duplicates across the account
GET /locations/listings/duplicates is the account-wide view. It is an
offset page: page and perPage in, records[] plus pageInfo.totalRecords
and pageInfo.totalPages out. status defaults to POTENTIAL and tag to
all, so a bare call returns every open duplicate the key can see. Add
siteId (a plan-sites id) to narrow to one directory.
curl "https://listingsapi.com/api/v4/locations/listings/duplicates?status=POTENTIAL&page=1&perPage=50" \-H "Authorization: API $LISTINGSAPI_KEY"{ "data": { "duplicateListingsRollup": { "pageInfo": { "hasNextPage": true, "totalPages": 34, "totalRecords": 1673 }, "records": [ { "id": "3329229", "name": "Woodbury Train Station", "duplicateProcessingState": "POTENTIAL", "liveLink": "http://citysquares.com/b/woodbury-train-station-23598450", "locationId": "73451", "siteName": "City Squares", "siteUrl": "citysquares.com" } ] } }}The rollup tells you where the duplicates are; duplicateProcessingState is
the same lifecycle status as above. To act on one, take its locationId to
the per-location endpoint in step 1 and read the listingItemId there. Walk
pages one at a time; a tight loop is the quickest way to a 429.
3. Confirm a duplicate so it is suppressed
Once a reviewer agrees an entry is a duplicate, call
POST /locations/listings/mark-as-duplicate with the listingItemId values
under input. Batch every confirmed id for a location into one call. locationId
is optional and scopes the request to that location.
curl -X POST https://listingsapi.com/api/v4/locations/listings/mark-as-duplicate \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "listingItemIds": ["TGlzdGluZ0l0ZW06MjM1OTg0NTA="] }}'{ "data": { "markAsDuplicate": { "success": true, "errors": null } }}success: true means the items were accepted and moved to PROCESSING. A
200 with success: false, or a 400, comes with an errors array whose
entries carry code and message; an unknown or malformed id is the usual
cause. The SDKs raise ValidationError in that case.
4. Clear a false positive
Detection is conservative. A franchise sibling, a second legitimate profile,
or a renamed location can all look like duplicates.
POST /locations/listings/mark-as-not-duplicate takes the identical body
and removes the entry from both lists for good. It is also how you reverse a
mistaken step 3.
curl -X POST https://listingsapi.com/api/v4/locations/listings/mark-as-not-duplicate \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d '{ "input": { "locationId": "TG9jYXRpb246MTgwMDI4OQ==", "listingItemIds": ["TGlzdGluZ0l0ZW06MjM1OTg0NTA="] }}'The response is data.markAsNotDuplicate with the same success and
errors fields as step 3.
5. Track progress
Re-read the rollup with status=PROCESSING to see what is still with the
publishers, and with status=FIXED to confirm what is gone. Neither SDK
exposes the status filter yet, so call the endpoint directly with the same
Authorization header.
curl "https://listingsapi.com/api/v4/locations/listings/duplicates?status=PROCESSING&page=1&perPage=50" \-H "Authorization: API $LISTINGSAPI_KEY" curl "https://listingsapi.com/api/v4/locations/listings/duplicates?status=FIXED&page=1&perPage=50" \-H "Authorization: API $LISTINGSAPI_KEY"Asking for perPage=1 and reading pageInfo.totalRecords gives the count
for each status in four cheap calls. A FAILED record whose liveLink still
resolves needs a human; a FAILED record whose link is dead was fixed by the
publisher on its own and can be dismissed with step 4.
Next steps
- Check listing status and live links: the primary listing for each directory, which is what a duplicate competes with.
- Find and fix listing errors: the other cleanup pass.
- Reference: Get duplicate listings for a location, Get all duplicate listings for an account, Mark listing as duplicate, Mark listing as not a duplicate.