Audit a business's listings with a listings scan
Check how any business appears across maps, search engines, directories and AI assistants, read the name, address and phone accuracy per directory, and get the results by webhook or by polling.
A listings scan checks how one business is listed across the listingsAPI directory network and tells you, directory by directory, whether it is found and whether its name, address and phone match what you gave. The business does not have to be a location in your account. This guide creates a scan, waits for it to finish, and reads the results.
Use it to:
- audit a prospect before you onboard them, so the first conversation starts with the listings that are missing or wrong;
- check a client's presence and NAP (name, address, phone) consistency before and after you fix their listings;
- give an AI agent a way to answer "how is this business listed?" without adding the business to the account.
1. Create the scan
POST /listings/scans takes the business as a single input object and
returns immediately with the scan in QUEUED. The directories are checked in
the background.
curl -X POST https://listingsapi.com/api/v4/listings/scans \-H "Authorization: API $LISTINGSAPI_KEY" \-H "Content-Type: application/json" \-d @- <<'JSON'{"input": { "name": "Joe's Pizza", "street": "7 Carmine Street", "city": "New York", "stateIso": "NY", "postalCode": "10014", "countryIso": "US", "phone": "2123661182"}}JSONThe response returns the scan, with phone normalized to international
format. results is empty until directories are checked, so keep the id.
{ "data": { "createListingScan": { "reused": false, "scan": { "id": "TGlzdGluZ1NjYW46Mzk0YTRhNWMtZjcwMy00NjZiLTg2NjAtMDU4MTlmNDhkZjMz", "status": "QUEUED", "source": "DIRECTORY_SEARCH", "locationId": null, "resultsAsOf": null, "createdAt": "2026-11-05T07:01:08Z", "startedAt": null, "completedAt": null, "business": { "name": "Joe's Pizza", "street": "7 Carmine Street", "city": "New York", "stateIso": "NY", "postalCode": "10014", "countryIso": "US", "phone": "+12123661182" }, "summary": { "total": 0, "found": 0, "accurate": 0, "inaccurate": 0, "notFound": 0, "pending": 0, "unavailable": 0 }, "failure": null, "results": [] }, "errors": [] } }}The response also includes a quota object, covered in
Costs, limits and errors.
Two things to check before you move on:
reused. When you ask for a business you scanned in the last seven days, listingsAPI returns that scan instead of starting a new one, withreused: trueand its current status, usuallyCOMPLETED. Nothing is charged to your allowance, and no new webhook is sent for it.errors. A rejected request still answers HTTP200. The rejection is indata.createListingScan.errors, andscanisnull. A phone number that is too short looks like this:
{ "data": { "createListingScan": { "reused": false, "scan": null, "errors": [ { "code": "SY83003", "message": "SY83003: Invalid value for phone", "contextInfo": [ { "key": "field", "value": "phone" } ] } ] } }}contextInfo names the field that failed. The other codes you can get are in
the error table.
2. Wait for the results
A scan moves from QUEUED to IN_PROGRESS to COMPLETED or FAILED. Most
finish in one to seven minutes. If a directory has not answered after about 15
minutes, its result is set to UNAVAILABLE and the scan completes with the
other results. Use a webhook if you can, and poll
only as a fallback.
2a. Receive the webhook (recommended)
When the scan reaches a final state, listingsAPI sends one
listing_scan.completed or
listing_scan.failed event to your
webhook URL. Its data.scan is the same object GET returns, results
included, so there is nothing to fetch afterwards.
{ "event": "listing_scan.completed", "timestamp": "2026-11-05T07:01:52Z", "account_id": 12345, "data": { "scan": { "id": "TGlzdGluZ1NjYW46Mzk0YTRhNWMtZjcwMy00NjZiLTg2NjAtMDU4MTlmNDhkZjMz", "status": "COMPLETED", "summary": { "total": 62, "found": 21, "accurate": 18, "inaccurate": 2, "notFound": 41, "pending": 0, "unavailable": 0 }, "results": [ "..." ] } }}The payload is shortened here; the full example is in the
event reference. Events are signed and
delivered like every other event, with one attempt and a five-second budget, so
verify the signature, store the raw body, and acknowledge before you do
anything else. The receiver in
React to events with webhooks already
does this; the only change is one more branch on event.
# After the signature check and the endpoint.verification branch:if body.get("event") in ("listing_scan.completed", "listing_scan.failed"): inbox.put(raw) # commit before the 200; read data.scan from the queue return {"received": True}, 200An event goes only to accounts with webhooks enabled and a verified URL. If yours is not, or if an event never arrives, poll the scan; the scan is kept for 90 days either way. Signing details are in Signatures and verification.
2b. Poll as a fallback
GET /listings/scans/{scanId} returns the scan with whatever results exist so
far. While the scan runs, results fills in as directories answer and
summary.pending counts the ones still open. Poll every 15 seconds and no
faster; each request counts toward your rate limit.
SCAN_ID="TGlzdGluZ1NjYW46Mzk0YTRhNWMtZjcwMy00NjZiLTg2NjAtMDU4MTlmNDhkZjMz" while true; doscan=$(curl -s "https://listingsapi.com/api/v4/listings/scans/$SCAN_ID" \ -H "Authorization: API $LISTINGSAPI_KEY")status=$(echo "$scan" | jq -r '.data.listingScan.status')echo "status: $status"case "$status" in COMPLETED|FAILED) echo "$scan" | jq '.data.listingScan | {status, summary, failure}' break ;; null) echo "$scan" break ;;esacsleep 15doneA scan answered from a recent scan or from one of your own locations is
COMPLETED the moment it is created, so a single GET is enough.
3. Read the results
A completed scan has a summary and one entry in results for each directory
checked, up to about 60 depending on the country. business returns the input
you sent, with phone normalized to international format.
{ "total": 62, "found": 21, "accurate": 18, "inaccurate": 2, "notFound": 41, "pending": 0, "unavailable": 0}total is the number of directories. found is how many list the business,
and it splits into accurate, inaccurate, and any presence-only listings
described below. notFound is how many do not list it. unavailable counts
directories that could not be checked this time, and pending is non-zero only
while the scan runs.
Each result has a status for the directory:
For a FOUND result, accurate says whether the listing matches your input:
truemeans name, address and phone all match.falsemeans at least one does not. A field the listing leaves out counts against it, so a listing with no phone number isfalse.nullmeans the business was found but there was no field-by-field check.
accuracy explains the verdict with a status and a message for each of
name, address and phone. The status is ACCURATE, INACCURATE, or
MISSING, and message is null when the field is accurate. Here is a
Facebook page that matches on name and address but has no phone number:
{ "directory": { "id": 184, "name": "Facebook", "url": "facebook.com" }, "status": "FOUND", "accurate": false, "accuracySource": "DIRECTORY_MATCH", "accuracy": { "name": { "status": "ACCURATE", "message": null }, "address": { "status": "ACCURATE", "message": null }, "phone": { "status": "MISSING", "message": "No phone number on the listing" } }, "listing": { "name": "Joe's Pizza", "street": "7 Carmine St", "city": "New York", "state": "NY", "postalCode": "10014", "country": "US", "fullAddress": "7 Carmine St, New York, NY 10014", "phones": [], "url": "https://www.facebook.com/177353602413952" }, "syncStatus": null}Some directories only confirm that the business exists and do not show enough
detail to compare. These come back FOUND with accurate: null, and they are
counted in found only, not in accurate or inaccurate. Treat them as
present, not as verified:
{ "directory": { "id": 374, "name": "Map Quest", "url": "www.mapquest.com" }, "status": "FOUND", "accurate": null, "accuracySource": "DIRECTORY_MATCH", "accuracy": { "name": { "status": "ACCURATE", "message": null }, "address": { "status": "MISSING", "message": "Not shown" }, "phone": { "status": "MISSING", "message": "Not shown" } }, "listing": { "name": "Joe's Pizza", "street": "7 Carmine St", "city": "New York", "state": "NY", "postalCode": "10014", "country": null, "fullAddress": "7 Carmine St, New York, NY 10014, US", "phones": [], "url": null }, "syncStatus": null}A NOT_FOUND result has no listing and no accuracy:
{ "directory": { "id": 352, "name": "YellowPages", "url": "www.yellowpages.com" }, "status": "NOT_FOUND", "accurate": null, "accuracySource": "DIRECTORY_MATCH", "accuracy": null, "listing": null, "syncStatus": null}Several directories do not hold their own data. Uber, Waze and Lyft show
Google Maps data, and Copilot shows Bing. Their results mirror the source
directory, so they usually repeat what Google Maps or Bing returned, and they
have no listing url. Count them as separate entries in summary, but do not
read them as independent confirmation:
{ "directory": { "id": 350, "name": "Uber", "url": "uber.com" }, "status": "FOUND", "accurate": true, "accuracySource": "DIRECTORY_MATCH", "accuracy": { "name": { "status": "ACCURATE", "message": null }, "address": { "status": "ACCURATE", "message": null }, "phone": { "status": "ACCURATE", "message": null } }, "listing": { "name": "Joe's Pizza", "street": "7 Carmine Street", "city": "New York", "state": "NY", "postalCode": "10014", "country": "US", "fullAddress": "7 Carmine St, New York, NY 10014, USA", "phones": [ "+1 212-366-1182" ], "url": null }, "syncStatus": null}AI assistants such as ChatGPT and Gemini are included too. For them,
listing shows what the assistant knows about the business, which is often just
a name, a full address and a phone number, and there is no per-field
accuracy.
To turn a completed scan into a fix list, print every directory with a wrong or missing field:
curl -s https://listingsapi.com/api/v4/listings/scans/TGlzdGluZ1NjYW46Mzk0YTRhNWMtZjcwMy00NjZiLTg2NjAtMDU4MTlmNDhkZjMz \-H "Authorization: API $LISTINGSAPI_KEY" \| jq -r '.data.listingScan.results[] | select(.accurate == false) | .directory.name as $directory | .accuracy | to_entries[] | select(.value.status != "ACCURATE") | $directory + " " + .key + ": " + (.value.message // .value.status)'For the Joe's Pizza scan this prints two lines: Facebook has no phone number, and Judys Book shows a different one.
Facebook phone: No phone number on the listingJudys Book phone: Phone number does not matchWhere the results come from
source tells you how the scan was answered, and resultsAsOf is when the data
was collected.
Costs, limits and errors
Only a fresh DIRECTORY_SEARCH scan uses your monthly allowance, which depends
on your plan and resets with your billing period. These are free:
- repeating a scan of the same business within seven days. Your own earlier
scan is returned with
reused: true; - a scan that a recent scan of the same business, by any account, can answer.
It is a
RECENT_SCANcopy; - scans that match one of your own locations (
LOCATION_LISTINGS); - scans that end
FAILED, which are refunded.
Scans are kept for 90 days. After that GET returns 404 with SY83005.
The create and list responses include a quota object, so you can see your
position before you start a batch. List returns your scans newest first, in
pages of 1 to 50 with perPage, without results.
curl -s "https://listingsapi.com/api/v4/listings/scans?page=1&perPage=20" \-H "Authorization: API $LISTINGSAPI_KEY" \| jq '.data.listingScans.quota'These are the errors you will see most often. Create returns its errors in the
body with HTTP 200; get and list return the usual HTTP status.
A GET for an id that does not exist returns:
{ "data": { "listingScan": null }, "errors": [ { "message": "SY83005: Listing scan not found" } ]}A scan that ends FAILED has its reason in failure, and results stays
empty:
{ "status": "FAILED", "failure": { "code": "SY83010", "message": "SY83010: Listing scans are temporarily unavailable, try again later", "field": null }}The general error format is in Error codes.
For AI agents
The hosted MCP server exposes this feature as three tools:
listings_scans_create, listings_scans_get and listings_scans_list. An
assistant can start a scan for a business the user names, poll it with
listings_scans_get, and summarize the directories that need attention. As with
REST, creating a scan needs a Write key. Setup is in
Connect AI agents to listingsAPI MCP.
Next steps
- React to events with webhooks: the receiver the scan events arrive on.
- Check listing status and live links and Find and fix listing errors: once a business is a location in your account, track and fix its listings.
- Add a location: onboard the business you audited.
- Reference: Create a listings scan, Get a listings scan, List listings scans, Event reference, Error codes.