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:

StatusMeaningYour move
POTENTIALDetected, nobody has acted on it.Review it (steps 3 and 4).
PROCESSINGYou confirmed it; suppression is under way with the publisher.Wait. Re-read later (step 5).
FIXEDThe publisher removed or merged it.Done.
FAILEDSuppression did not go through.Check the live link; raise it with support if it is still up.

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.

Duplicates for a location
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/listings/duplicates \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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.

Account rollup
curl "https://listingsapi.com/api/v4/locations/listings/duplicates?status=POTENTIAL&page=1&perPage=50" \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "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.

Mark as duplicate
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="]  }}'
JSON
{  "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.

Mark as not duplicate
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.

Progress
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