Check listing status and get live links

Read every publisher's sync status for a location, collect the live listing URLs, summarize coverage, check voice assistants, and get notified by webhook instead of polling.

Once a location exists, one listing per publisher in your plan moves through submission on its own. This guide shows how to read where each one stands, pull the URLs of the listings that are live, and stop polling once webhooks are set up.

1. Fetch the premium listings for a location

GET /locations/{locationId}/listings/premium returns one record per premium publisher in the plan. It is a plain array, not a paginated list. The sibling route /listings/additional returns the free directories with the same shape; there is no listingType query parameter.

Premium listings
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/listings/premium \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "listingsForLocation": [      {        "site": { "name": "Google Maps", "url": "maps.google.com" },        "syncStatus": "REQUIRING_ACTION",        "displayStatus": "Connect your account to sync the listing",        "actionRequired": true,        "listingUrl": null,        "syncIssue": null,        "lastUpdate": "2026-07-06T18:33:28Z"      },      {        "site": { "name": "BeLocalFocussed", "url": "belocalfocussed.com" },        "syncStatus": "SYNCED",        "displayStatus": "Synced",        "actionRequired": false,        "listingUrl": "https://belocalfocussed.com/jenny-home-26x0ffkd0e",        "syncIssue": null,        "lastUpdate": "2026-07-06T18:33:30Z"      }    ]  }}

Each row pairs a site with a machine-readable syncStatus and a human-readable displayStatus. lastUpdate tells you when the row last changed, which is useful for spotting a submission that has stalled.

2. Read the status of each publisher

syncStatus is the field to branch on. displayStatus is the text to show a person, and syncIssue carries the publisher's reason when there is one.

syncStatusMeaningWhat to do
AVAILABLEReady to submit, not started yet.Nothing. It moves on its own.
IN_PROGRESSSubmitted to the publisher, awaiting a response.Wait. Check lastUpdate if it has been weeks.
PENDING_APPROVALThe publisher has it and is reviewing it.Wait.
SYNCED or COMPLETEDLive. listingUrl is populated.Collect the link (step 3).
REQUIRING_ACTIONBlocked on you. actionRequired is true.Read displayStatus, then see Find and fix listing errors.
FAILEDThe publisher rejected or could not process it.Read syncIssue, fix the location, re-poll.
CANNOT_SUBMITThis publisher cannot take this location at all.Usually a country or category the publisher does not support.

Google Maps, Facebook, and Apple stay at REQUIRING_ACTION until the matching account is connected. That is expected for a new location, not a fault.

listingUrl is only set on live rows. Filter to SYNCED (and COMPLETED) and keep the URL with the site name so you can render a "see it live" list or store it against your own record.

Live links
curl -s https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/listings/premium \-H "Authorization: API $LISTINGSAPI_KEY" \| jq '[.data.listingsForLocation[]       | select(.listingUrl != null)       | {site: .site.name, url: .listingUrl}]'

A SYNCED row with a null listingUrl does happen: some directories do not expose a public page URL. Treat the status as the source of truth and the link as a bonus.

4. Build a coverage summary

Counting rows by syncStatus gives a one-line health figure per location: how many publishers are live, how many are still in flight, and how many are blocked. This is the number to put on a dashboard.

Coverage
curl -s https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/listings/premium \-H "Authorization: API $LISTINGSAPI_KEY" \| jq '.data.listingsForLocation      | group_by(.syncStatus)      | map({status: .[0].syncStatus, count: length})'

Run this across many locations by paging GET /locations and calling the premium endpoint once per location. Each call counts against your rate limit, so leave a gap between them and honor Retry-After on a 429. Find and fix listing errors shows the loop as an account-wide sweep.

5. Check voice assistants

Voice surfaces (Google Assistant, Alexa, Siri, Cortana, Bixby) have their own endpoint, GET /locations/{locationId}/voice-assistants, with a smaller status set. Siri and Google Assistant depend on the Google connection, so a location without one shows a blocker here too.

Voice assistants
curl https://listingsapi.com/api/v4/locations/TG9jYXRpb246MTgwMDI4OQ==/voice-assistants \-H "Authorization: API $LISTINGSAPI_KEY"
JSON
{  "data": {    "voiceAssistantsForLocation": [      {        "voiceIdentifier": "GOOGLE_ASSISTANT",        "name": "Google Assistant",        "syncStatus": "REQUIRING_ACTION",        "actionRequired": true,        "syncIssue": null,        "errors": []      },      {        "voiceIdentifier": "SIRI",        "name": "Siri",        "syncStatus": "IN_PROGRESS",        "actionRequired": false,        "syncIssue": "Google connection is required.",        "errors": []      }    ]  }}
syncStatusMeaning
NOT_SUBMITTEDNot sent to the assistant yet.
IN_PROGRESSSubmission underway.
REQUIRING_ACTIONBlocked on you; read syncIssue.
LIVEPublished.

There is no live link for a voice assistant; LIVE is the end state.

6. Get told instead of polling

If you have a webhook URL configured at /dashboard/webhooks, the listing.submission event fires each time a publisher submission for one location reaches a terminal state. It carries status (success, incomplete, or canceled), the site, and live_link on success, which is the same URL you collected in step 3.

Route listing.submission
import base64 def handle(payload: dict) -> None:  if payload["event"] != "listing.submission":      return   data = payload["data"]  # "TG9jYXRpb246Mjc5Mzgx" -> "Location:279381"  global_id = base64.b64decode(data["location_id"]).decode()  database_id = int(global_id.split(":")[1])   site = data["site"]["name"]  status = payload["status"]   if status == "success":      live_link = payload.get("live_link")      save_live_link(database_id, site, live_link)  elif status == "incomplete":      flag_for_review(database_id, site, payload.get("error_message"))

Respond with 200 within 5 seconds and do the work afterwards; there are no retries. For the receiver skeleton and signature check, see React to events with webhooks.

Next steps