Use cases

Ten end-to-end Python recipes, from bulk exports and review automation to analytics reports, listings audits, and bulk posting.

Set your API key once before running any example:

bash
export LISTINGSAPI_KEY="your-api-key"

1. List your locations

Connect and print your first page of locations.

01_quickstart.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
page = client.locations.list(first=5)
for loc in page:
print(loc.name, "--", getattr(loc, "city", "N/A"), getattr(loc, "stateIso", "N/A"))
 
print(f"Locations on page: {len(page)}, has_more: {page.has_more}")

2. Bulk export to CSV

Download every location to a CSV using auto_paging_iter().

02_bulk_export_locations.py
import csv
import listingsapi
 
client = listingsapi.ListingsAPI()
 
locations = list(client.locations.list(first=100).auto_paging_iter())
print(f"Fetched {len(locations)} locations")
 
fields = ["id", "name", "storeId", "street", "city", "stateIso", "postalCode", "countryIso", "phone"]
 
with open("locations_export.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
writer.writeheader()
for loc in locations:
writer.writerow(loc.to_dict())
 
print("Exported to locations_export.csv")

auto_paging_iter() handles all cursor pages automatically. See Locations for the full method reference.


3. Create a location

locations.add() takes every mandatory field as a keyword argument and validates the payload client-side, so a bad request fails fast with a clear message before any network call. Look up a valid sub_category_id with client.subcategories() (use the databaseId).

03_create_location.py
import listingsapi
from listingsapi import ValidationError
 
client = listingsapi.ListingsAPI()
 
subcategories = client.subcategories()
dental = [s for s in subcategories if "dentist" in (s.get("name") or "").lower()]
sub_category_id = dental[0].databaseId if dental else 1432
 
try:
result = client.locations.add(
name="Acme Dental",
description=(
"Acme Dental is a family-owned dental practice serving downtown "
"New York for over fifteen years. Our team offers preventive care, "
"cosmetic dentistry, orthodontics, and emergency appointments, with "
"weekend hours and a patient-first approach that keeps every visit "
"comfortable and affordable for the whole family."
),
sub_category_id=sub_category_id,
country_iso="US",
city="New York",
street="123 Jump Street",
state_iso="NY",
postal_code="10013",
phone="6443859313",
store_id="NYC-001",
)
except ValidationError as e:
print(f"Fix the payload: {e}")
else:
print(f"Created location: {result.location.id}")

The description must be at least 200 characters; publishers use it as the primary listing copy. client.countries() lists supported country and state ISO codes, and client.plan_sites() shows which directories your plan publishes to.


4. Review monitoring

Scan recent reviews and flag negative ones that need a reply.

04_review_monitoring.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
page = client.locations.list(first=5)
 
for loc in page:
print(f"\n--- {loc.name} ---")
reviews = client.reviews.list(loc.id, first=10)
if not reviews:
print(" No recent reviews")
continue
 
for review in reviews:
rating = getattr(review, "rating", "N/A")
author = getattr(review, "authorName", "Anonymous")
site = getattr(review, "siteName", "Unknown")
responded = bool(getattr(review, "responses", None))
flag = " ** NEEDS ATTENTION **" if isinstance(rating, (int, float)) and rating <= 2 else ""
status = "Responded" if responded else "No reply"
print(f" [{rating}] {author} on {site} ({status}){flag}")

See Reviews for filter options including rating_filters, site_urls, and date ranges.


5. Auto-reply to reviews

workflows.auto_reply_to_reviews() fetches recent reviews, filters by rating and response status, and posts replies from your template in one call. Preview with dry_run=True first. The {rating} placeholder is replaced with each review's star rating.

05_auto_reply_reviews.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
LOCATION_ID = 16808 # Replace with your location ID
TEMPLATE = "Thanks for the {rating}-star review! We appreciate you taking the time."
 
# Preview what would be sent
preview = client.workflows.auto_reply_to_reviews(
LOCATION_ID,
template=TEMPLATE,
min_rating=4,
dry_run=True,
)
for entry in preview:
print(f" Would reply to {entry['id']} ({entry['rating']} stars)")
 
# Post the replies for real
results = client.workflows.auto_reply_to_reviews(
LOCATION_ID,
template=TEMPLATE,
min_rating=4,
)
sent = [r for r in results if r["status"] == "sent"]
print(f"Replied to {len(sent)} of {len(results)} matching reviews")

The default min_rating=4 skips negative reviews on purpose: those deserve a personal reply. Handle them individually with reviews.respond():

05b_respond_to_negative.py
import listingsapi
from listingsapi import APIError
 
client = listingsapi.ListingsAPI()
 
negative = client.reviews.list(16808, rating_filters=[1, 2], first=20)
for review in negative:
if getattr(review, "responses", None):
continue
try:
client.reviews.respond(
review.interactionId,
"We are sorry to hear about your experience. Please reach out "
"directly so we can make it right.",
)
print(f"Replied to {getattr(review, 'authorName', 'Anonymous')}")
except APIError as e:
print(f"Failed: {e}")

6. Analytics report

Pull Google profile analytics and review stats for every location.

06_analytics_report.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
FROM_DATE = "2026-01-01"
TO_DATE = "2026-06-30"
 
all_locations = list(client.locations.list(first=100).auto_paging_iter())
print(f"Generating report for {len(all_locations)} locations ({FROM_DATE} to {TO_DATE})\n")
 
for loc in all_locations[:10]:
print(f"--- {loc.name} ---")
 
google = client.analytics.google(loc.id, from_date=FROM_DATE, to_date=TO_DATE)
if google:
print(f" Google: {google.to_dict()}")
 
review_stats = client.reviews.analytics.overview(loc.id, start_date=FROM_DATE, end_date=TO_DATE)
if review_stats:
print(f" Reviews: {review_stats.to_dict()}")
 
sites = client.reviews.analytics.sites_stats(loc.id, start_date=FROM_DATE, end_date=TO_DATE)
if sites:
print(f" Sites: {sites.to_dict()}")
 
print()

See Analytics for bing() and facebook() profile metrics.


7. Weekly reputation report

workflows.weekly_reputation_report() combines reviews, review analytics, Google and Bing profile analytics, and listings sync status into a single report object.

07_reputation_report.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
report = client.workflows.weekly_reputation_report(
16808,
start_date="2026-06-29",
end_date="2026-07-05",
)
 
summary = report.review_summary
print(f"Average rating: {summary.get('averageRating')}")
print(f"Recent reviews: {len(report.recent_reviews)}")
 
analytics = report.analytics
print(f"Google: {analytics.get('google')}")
print(f"Bing: {analytics.get('bing')}")
 
health = report.listings_health
print(f"Listings synced: {health.get('synced')}/{health.get('total')} ({health.get('sync_rate')})")

8. Listings health audit

workflows.listings_health_audit() checks premium listings, voice listings, and duplicates for a location and computes a 0-100 health score.

08_listings_audit.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
page = client.locations.list(first=10)
print(f"Auditing listings for {len(page)} locations\n")
 
for loc in page:
audit = client.workflows.listings_health_audit(loc.id)
print(f"--- {loc.name} ---")
print(f" Health score: {audit.health_score}%")
print(f" Synced: {audit.synced_count}, issues: {audit.issue_count}")
print(f" Voice listings: {len(audit.voice)}, duplicates: {len(audit.duplicates)}")
for issue in audit.issues:
print(f" [{issue.get('syncStatus')}] {issue.get('site')}")
print()

See Listings for the underlying premium, voice, duplicates, and mark-as-duplicate methods.


9. Bulk publish a post

posts.bulk_publish() publishes one post across many locations, defaulting to both Google and Facebook. Location IDs are encoded automatically and the payload is validated client-side before any network call.

09_bulk_publish_post.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
result = client.posts.bulk_publish(
name="Summer hours 2026",
location_ids=[16808, 16809, 16810],
message="We are open late all summer! Come see us until 9pm, Monday through Saturday.",
media_url="https://cdn.example.com/summer-hours.jpg",
cta_type="LEARN_MORE",
cta_url="https://www.acmedental.com/hours",
)
 
post = result.get("socialPost") or {}
print(f"Created bulk post: {post.get('id')}")

Pass a dict as message to customize the copy per site:

09b_per_site_message.py
result = client.posts.bulk_publish(
name="July whitening offer",
location_ids=[16808],
message={
"GOOGLE": "20% off teeth whitening this July. Book online today!",
"FACEBOOK": "July special: 20% off teeth whitening. Tap to book your visit!",
},
sites=["GOOGLE", "FACEBOOK"],
)

For single-site typed posts, use posts.create_announcement(), posts.create_event(), or posts.create_offer().


10. Google connect flow

Generate an OAuth URL, list connected accounts, and review match suggestions.

10_google_connect_flow.py
import listingsapi
 
client = listingsapi.ListingsAPI()
 
# Step 1: Generate an OAuth URL (valid 24 hours)
result = client.connected_accounts.connect_google(
success_url="https://yourapp.com/connect/success",
error_url="https://yourapp.com/connect/error",
)
print(f"Redirect user to: {result.get('url', 'N/A')}")
 
# Step 2: List connected Google accounts
accounts = client.connected_accounts.list(publisher="google")
connected = getattr(accounts, "connectedAccounts", None) or []
print(f"\nConnected Google accounts:")
for acc in connected:
print(f" {acc.email} -- status: {acc.status}")
 
# Step 3: Check match suggestions
suggestions = client.connected_accounts.suggestions(acc.id, page=1, per_page=10)
records = getattr(suggestions, "matchedRecords", None) or []
print(f" Suggestions: {len(records)} matches found")

Confirm matches with connected_accounts.confirm_matches() and link a specific listing to a location with listings.connect().


Where to go next