The official Python SDK for listingsAPI: business listings, reviews, posts, and analytics for local marketing, in one typed client.
import listingsapi
client = listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY from env
for location in client.locations.list(first=10):
print(location.name, location.city)Full API reference and guides: docs.listingsapi.com
- Installation
- Authentication
- Quickstart
- Pagination
- Locations
- Reviews
- Posts
- Listings
- Analytics
- Photos
- Connected accounts
- Supporting APIs
- Workflows: pre-built automations
- Method reference
- Error handling
- Configuration
- Examples and development
pip install listingsapiRequires Python 3.9+. The only runtime dependency is requests.
Get an API key from your listingsAPI dashboard and export it:
export LISTINGSAPI_KEY="your-api-key"import listingsapi
client = listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY
client = listingsapi.ListingsAPI(api_key="your-key") # or pass it explicitlyAuto-reply to positive reviews in ten lines:
import listingsapi
client = listingsapi.ListingsAPI()
results = client.workflows.auto_reply_to_reviews(
16808,
template="Thanks for the {rating}-star review! We appreciate your support.",
min_rating=4,
)
print(f"Replied to {len(results)} reviews")Audit your listings health in three:
audit = client.workflows.listings_health_audit(16808)
print(f"Health score: {audit.health_score}%")
print(f"Synced: {audit.synced_count}, Issues: {audit.issue_count}")List endpoints return a SyncPage. Iterate it directly, page manually, or let it walk every page for you:
page = client.locations.list(first=50)
for loc in page:
print(loc.name)
if page.has_more: # manual paging
next_page = page.next_page()
for loc in page.auto_paging_iter(): # every record, all pages
print(loc.name)Responses are APIObjects: dot access (loc.name), dict access (loc["stateIso"]), and loc.to_dict() all work.
Create a location in one call. locations.add() takes every mandatory field as a keyword argument and validates them client-side (name length, 200-character description, category, country, city) before any network call:
result = client.locations.add(
name="Acme Dental",
description=(
"Acme Dental is a family-owned dental practice in downtown New York "
"offering preventive care, cosmetic dentistry, orthodontics, and "
"emergency appointments. Our board-certified team combines modern "
"equipment with a gentle, patient-first approach."
),
sub_category_id=1432,
country_iso="US",
city="New York",
street="123 Jump Street",
postal_code="10013",
phone="6443859313",
)
print(result.location.id)Optional keyword arguments: state_iso, website, store_id, hide_address, business_hours, folder_ids, service_area, place_action_links, enabled_site_ids (publish to a subset of your plan's sites), and additional_fields for anything else. Get valid category IDs from client.subcategories().
Find and fetch:
location = client.locations.retrieve(16808)
page = client.locations.search("cafe", first=20)
locations = client.locations.list_by_ids([16808, 16749])
locations = client.locations.list_by_store_codes(["STORE01", "STORE02"])Update and lifecycle:
client.locations.update({"id": 16808, "phone": "5559876543"})
client.locations.archive([16808])
client.locations.cancel_archive([16808], "manual", "ops@example.com")locations.create(input_dict) is also available when you already have a raw camelCase payload.
# List with filters, or walk everything
page = client.reviews.list(16808, first=20, rating_filters=[1, 2])
for review in client.reviews.list(16808, first=50).auto_paging_iter():
print(review.rating, review.content)
# Respond, edit, archive
client.reviews.respond(interaction_id="uuid-here", content="Thank you!")
client.reviews.edit_response(review_id="...", response_id="...", content="Updated reply")
client.reviews.archive_response(response_id="...")
# Analytics and phrases
overview = client.reviews.analytics.overview(16808, start_date="2024-01-01")
timeline = client.reviews.analytics.timeline(16808)
sites = client.reviews.analytics.sites_stats(16808)
phrases = client.reviews.phrases(["TG9jYXRpb246MTY4MDg="], start_date="2024-01-01")Publish announcements, events, and offers to Google and Facebook.
posts.bulk_publish() is the one-call way to put the same post on both sites across many locations. It expands your message per site, encodes location IDs, and validates the payload client-side before any network call:
result = client.posts.bulk_publish(
name="Holiday hours",
location_ids=[16808, 16809, 16810],
message="Open until 10pm through the holidays!",
media_url="https://cdn.example.com/holiday.jpg",
cta_type="LEARN_MORE",
cta_url="https://example.com/holiday-hours",
)
print(result.socialPost.id, result.socialPost.status) # INPROGRESS, publishes asyncPass a dict as message for per-site copy: {"GOOGLE": "...", "FACEBOOK": "..."}. sites defaults to both.
Typed creates for single campaigns:
# Announcement: plain message, optional CTA and image
result = client.posts.create_announcement(
name="Grand Opening",
location_ids=[16808],
message="We are now open!",
sites=["GOOGLE"],
cta_type="LEARN_MORE",
cta_url="https://example.com/opening",
)
# Event: title plus start/end window
result = client.posts.create_event(
name="Live music",
location_ids=[16808],
message="Join us Friday!",
title="Jazz Night",
start_day="2026-08-01",
end_day="2026-08-01",
start_time="7:00pm",
end_time="10:00pm",
)
# Offer: coupon, discount, terms
result = client.posts.create_offer(
name="Summer sale",
location_ids=[16808],
message="20% off all week!",
title="Summer Sale",
coupon_code="SUMMER20",
discount="20%",
redeem_url="https://example.com/sale",
start_day="2026-08-01",
end_day="2026-08-07",
)Read, monitor, and delete:
post = client.posts.retrieve("U29jaWFsUG9zdDo0NDEyMg==") # per-site publish status + analytics
page = client.posts.list_for_location(16808, page=1, per_page=10)
bulk = client.posts.bulk_retrieve("QnVsa1Bvc3Q6OTk=") # per-location publish status
campaigns = client.posts.bulk_list_for_location(16808)
client.posts.delete("U29jaWFsUG9zdDo0NDEyMg==")Publishing is asynchronous: creates return status: INPROGRESS; poll retrieve() until SUCCESS and check publishDetails[].submissionError for per-site failures.
premium = client.listings.premium(16808)
voice = client.listings.voice(16808)
# Duplicates
dupes = client.listings.duplicates(16808)
client.listings.mark_duplicate(16808, ["listing-item-id"])
client.listings.mark_not_duplicate(16808, ["listing-item-id"])
# Connect and disconnect
client.listings.connect(16808, "listing-id", "account-id")
client.listings.disconnect(16808, "GOOGLE")google = client.analytics.google(16808, from_date="2024-01-01")
bing = client.analytics.bing(16808)
facebook = client.analytics.facebook(16808)photos = client.photos.list(16808)
client.photos.add(16808, [{"photo": "https://example.com/img.jpg", "type": "ADDITIONAL"}])
client.photos.remove(16808, ["photo-id"])
client.photos.star(16808, ["media-id"], starred=True)
status = client.photos.upload_status("request-id")accounts = client.connected_accounts.list()
url = client.connected_accounts.connect_google("https://ok.com", "https://err.com")
client.connected_accounts.disconnect_google("account-id")
# OAuth for a single location
url = client.connected_accounts.oauth_url(16808, "GOOGLE", "https://ok.com", "https://err.com")
client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")sites = client.plan_sites() # directories included in your plan
countries = client.countries() # supported countries and states
subcategories = client.subcategories() # business categories (IDs for locations.add)
subs = client.subscriptions() # active subscriptionsHigh-level functions that combine multiple API calls into real product features.
# Auto-reply to positive reviews (dry_run previews without sending)
results = client.workflows.auto_reply_to_reviews(
16808,
template="Thanks for the {rating}-star review!",
min_rating=4,
dry_run=True,
)
# Weekly reputation report
report = client.workflows.weekly_reputation_report(16808)
print(report.review_summary, report.analytics, report.listings_health)
# Listings health audit
audit = client.workflows.listings_health_audit(16808)
print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")| Method | Description |
|---|---|
client.plan_sites() |
Directories included in your plan |
client.countries() |
Supported countries and states (ISO codes) |
client.subcategories() |
Business categories (IDs for locations.add) |
client.subscriptions() |
Active subscriptions for the account |
| Method | Description |
|---|---|
locations.list(first, after, before, last) |
List locations with cursor pagination |
locations.retrieve(location_id) |
Fetch a single location by ID |
locations.list_by_ids(location_ids) |
Fetch specific locations by ID |
locations.list_by_store_codes(store_codes) |
Fetch locations by store code |
locations.search(query, fields, first, after) |
Search by name, address, or store ID |
locations.add(**fields) |
One-call create with keyword args and client-side validation |
locations.create(input_dict) |
Create from a raw camelCase payload |
locations.update(input_dict) |
Update fields on a location (pass id plus changes) |
locations.archive(location_ids) |
Archive locations |
locations.cancel_archive(location_ids, selection_type, changed_by) |
Cancel a pending archive |
| Method | Description |
|---|---|
posts.bulk_publish(**fields) |
One post to Google and Facebook across many locations |
posts.create_announcement(**fields) |
Create an ANNOUNCEMENT post |
posts.create_event(**fields) |
Create an EVENT post with title and date window |
posts.create_offer(**fields) |
Create an OFFER post with coupon and terms |
posts.create(body) |
Create from a raw flat payload |
posts.retrieve(post_id) |
Get a post with per-site publish status and analytics |
posts.delete(post_id) |
Delete a post from every published site |
posts.list_for_location(location_id, tag, page, per_page) |
List post campaigns for a location |
posts.bulk_retrieve(bulk_post_id) |
Get a bulk campaign with per-location status |
posts.bulk_list_for_location(location_id, tag, page, per_page) |
List bulk campaigns touching a location |
| Method | Description |
|---|---|
reviews.list(location_id, first, rating_filters, ...) |
List reviews/interactions with filters |
reviews.details(interaction_ids) |
Detailed review data for specific interactions |
reviews.respond(interaction_id, content) |
Reply to a review |
reviews.edit_response(review_id, response_id, content) |
Edit a reply |
reviews.archive_response(response_id) |
Archive a reply |
reviews.settings(location_id) |
Review notification settings |
reviews.edit_settings(location_id, site_urls) |
Update notification settings |
reviews.site_config() |
Review site configuration for the account |
reviews.phrases(location_ids, start_date, ...) |
Frequently used phrases across reviews |
reviews.analytics.overview(location_id, start_date, end_date) |
Review analytics overview |
reviews.analytics.timeline(location_id, ...) |
Review volume/rating over time |
reviews.analytics.sites_stats(location_id, ...) |
Per-site review stats |
| Method | Description |
|---|---|
listings.premium(location_id) |
Premium directory listings |
listings.voice(location_id) |
Voice assistant listings |
listings.duplicates(location_id) |
Duplicate listings for a location |
listings.all_duplicates(tag, page) |
Duplicates across all locations |
listings.mark_duplicate(location_id, listing_item_ids) |
Mark listings as duplicates |
listings.mark_not_duplicate(location_id, listing_item_ids) |
Unmark duplicates |
listings.connect(location_id, listing_id, account_id) |
Connect a listing to a profile |
listings.disconnect(location_id, site) |
Disconnect a listing |
listings.create_gmb(location_id, ...) |
Create a Google Business Profile listing |
| Method | Description |
|---|---|
analytics.google(location_id, from_date, to_date) |
Google Business Profile insights |
analytics.bing(location_id, from_date, to_date) |
Bing Places insights |
analytics.facebook(location_id, from_date, to_date) |
Facebook page insights |
| Method | Description |
|---|---|
photos.list(location_id) |
List photos for a location |
photos.add(location_id, photos) |
Upload photos by URL |
photos.remove(location_id, photo_ids) |
Delete photos |
photos.star(location_id, media_ids, starred) |
Star or unstar photos |
photos.upload_status(request_id) |
Status of a bulk photo upload |
| Method | Description |
|---|---|
connected_accounts.list(...) |
List connected Google/Facebook accounts |
connected_accounts.details(connected_account_id) |
Account details |
connected_accounts.folders(connected_account_id, folder_name) |
Folders/location groups in the account |
connected_accounts.suggestions(...) |
Suggested listing-to-location matches |
connected_accounts.listings(...) |
Listings available in a connected account |
connected_accounts.connect_google(success_url, error_url) |
Start Google OAuth connect |
connected_accounts.connect_facebook(success_url, error_url) |
Start Facebook OAuth connect |
connected_accounts.disconnect_google(connected_account_id) |
Disconnect a Google account |
connected_accounts.disconnect_facebook(connected_account_id) |
Disconnect a Facebook account |
connected_accounts.trigger_matches(connected_account_ids) |
Trigger profile-to-location matching |
connected_accounts.confirm_matches(match_record_ids) |
Confirm suggested matches |
connected_accounts.oauth_url(location_id, site, success_url, error_url) |
OAuth connect URL for one location |
connected_accounts.oauth_disconnect(location_id, site) |
Disconnect one location's profile |
| Method | Description |
|---|---|
workflows.auto_reply_to_reviews(location_id, template, min_rating, dry_run) |
Auto-reply to unanswered positive reviews |
workflows.weekly_reputation_report(location_id) |
Reviews + analytics + listings health in one report |
workflows.listings_health_audit(location_id) |
Sync status and duplicate audit with health score |
Every SDK error derives from listingsapi.ListingsAPIError. API failures raise APIError or one of its subclasses, whether the platform reports them as an HTTP status or inside the response body. The platform returns some failures (invalid tokens, mutation validation) as HTTP 200 with an errors[] payload, and the SDK raises for those too, so you never have to inspect success flags yourself.
ListingsAPIError base class for everything below
├── APIError any failure reported by the API
│ ├── AuthenticationError 401 or invalid-token error payloads
│ ├── PermissionDeniedError 403 or permission error payloads
│ ├── NotFoundError 404
│ ├── ValidationError 400/422, or mutations with success=false / errors[]
│ ├── RateLimitError 429, carries retry_after
│ └── InternalServerError 5xx, safe to retry
└── APIConnectionError network failure, nothing reached the API
| Attribute | Meaning |
|---|---|
status_code |
HTTP status of the response (can be 200 for payload-level errors) |
code |
First platform error code, e.g. "SY10005" |
errors |
Every error entry, normalized to {"code", "message", "context"} |
response_body |
Raw response text |
retry_after |
Seconds to wait (RateLimitError only) |
import listingsapi
client = listingsapi.ListingsAPI()
try:
result = client.locations.add(
name="Acme Dental",
description=DESCRIPTION, # min 200 characters
sub_category_id=1432,
country_iso="US",
city="New York",
)
except listingsapi.ValidationError as e:
# Raised client-side for missing/short mandatory fields, or by the API
# (e.g. SY10005 description too short, SY10072 duplicate storeId)
for err in e.errors:
print(err["code"], err["message"])
except listingsapi.AuthenticationError:
print("Invalid API key. Check LISTINGSAPI_KEY.")
except listingsapi.RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after}s.")
except listingsapi.APIConnectionError:
print("Network error. Could not reach the API.")The client automatically retries read requests (GET) on 429 and 5xx responses — up to max_retries attempts (default 2), honoring Retry-After. Writes (creates, replies, posts, deletes) are not retried automatically: they are not idempotent, so a blind retry after a completed-but-5xx write could duplicate the record. If you need to retry a write, do it yourself only when the operation is safe to repeat. A RateLimitError means retries were exhausted; back off for e.retry_after seconds before trying again:
import time
import listingsapi
def with_backoff(fn, attempts=3):
for attempt in range(attempts):
try:
return fn()
except listingsapi.RateLimitError as e:
if attempt == attempts - 1:
raise
time.sleep(e.retry_after or 2 ** attempt)
page = with_backoff(lambda: client.locations.list(first=50))See examples/12_error_handling.py for a runnable walkthrough.
client = listingsapi.ListingsAPI(
api_key="your-api-key",
base_url="https://listingsapi.com", # default
timeout=300.0,
max_retries=3,
)| Option | Default | Description |
|---|---|---|
api_key |
LISTINGSAPI_KEY env var |
Your API key |
base_url |
https://listingsapi.com |
API host |
timeout |
240.0 |
Request timeout in seconds |
max_retries |
2 |
Automatic retries on 429 and 5xx for read (GET) requests; writes are never auto-retried |
Runnable scripts for every area live in examples/, from a 10-line quickstart to a full FastAPI backend.
git clone https://github.com/Listings-API/listingsapi-python-sdk.git
cd listingsapi-python-sdk
pip install -e ".[dev]"
pytest