Skip to content

Repository files navigation

listingsAPI

listingsAPI Python SDK

PyPI versionPyPI downloadsPython 3.9+License: MITWebsite

The official Python SDK for listingsAPI: business listings, reviews, posts, and analytics for local marketing, in one typed client.

importlistingsapiclient=listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY from envforlocationinclient.locations.list(first=10):
print(location.name, location.city)

Full API reference and guides: docs.listingsapi.com

Contents

Installation

pip install listingsapi

Requires Python 3.9+. The only runtime dependency is requests.

Authentication

Get an API key from your listingsAPI dashboard and export it:

export LISTINGSAPI_KEY="your-api-key"
importlistingsapiclient=listingsapi.ListingsAPI() # reads LISTINGSAPI_KEYclient=listingsapi.ListingsAPI(api_key="your-key") # or pass it explicitly

Quickstart

Auto-reply to positive reviews in ten lines:

importlistingsapiclient=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}")

Pagination

List endpoints return a SyncPage. Iterate it directly, page manually, or let it walk every page for you:

page=client.locations.list(first=50)
forlocinpage:
print(loc.name)
print(page.total) # total records matching the queryifpage.has_more: # manual pagingnext_page=page.next_page()
forlocinpage.auto_paging_iter(): # every record, all pagesprint(loc.name)

auto_paging_iter() walks to the end of the result set and stops there, so it is safe to use on large histories. client.reviews.list(...) exposes page.total the same way, from the feed's totalCount.

Responses are APIObjects: dot access (loc.name), dict access (loc["stateIso"]), and loc.to_dict() all work.

Locations

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.

Reviews

# List with filters, or walk everythingpage=client.reviews.list(16808, first=20, rating_filters=[1, 2])
forreviewinclient.reviews.list(16808, first=50).auto_paging_iter():
print(review.rating, review.content)
# Respond, edit, archiveclient.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 phrasesoverview=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")

Posts

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 async

Pass 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 imageresult=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 windowresult=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, termsresult=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 + analyticspage=client.posts.list_for_location(16808, page=1, per_page=10)
bulk=client.posts.bulk_retrieve("QnVsa1Bvc3Q6OTk=") # per-location publish statuscampaigns=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.

Listings

premium=client.listings.premium(16808)
voice=client.listings.voice(16808)
# Duplicatesdupes=client.listings.duplicates(16808)
client.listings.mark_duplicate(16808, ["listing-item-id"])
client.listings.mark_not_duplicate(16808, ["listing-item-id"])
# Connect and disconnectclient.listings.connect(16808, "listing-id", "account-id")
client.listings.disconnect(16808, "GOOGLE")

Analytics

google=client.analytics.google(16808, from_date="2024-01-01")
bing=client.analytics.bing(16808)
facebook=client.analytics.facebook(16808)

Photos

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")

Connected accounts

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 locationurl=client.connected_accounts.oauth_url(16808, "GOOGLE", "https://ok.com", "https://err.com")
client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")

Supporting APIs

sites=client.plan_sites() # directories included in your plancountries=client.countries() # supported countries and statessubcategories=client.subcategories() # business categories (IDs for locations.add)subs=client.subscriptions() # active subscriptions

Workflows: pre-built automations

High-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 reportreport=client.workflows.weekly_reputation_report(16808)
print(report.review_summary, report.analytics, report.listings_health)
# Listings health auditaudit=client.workflows.listings_health_audit(16808)
print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")

Method reference

Supporting APIs (client-level)

MethodDescription
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

Locations

MethodDescription
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

Posts

MethodDescription
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

Reviews

MethodDescription
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

Listings

MethodDescription
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

Analytics

MethodDescription
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

Photos

MethodDescription
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

Connected accounts

MethodDescription
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

Workflows

MethodDescription
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

Error handling

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
AttributeMeaning
status_codeHTTP status of the response (can be 200 for payload-level errors)
codeFirst platform error code, e.g. "SY10005"
errorsEvery error entry, normalized to {"code", "message", "context"}
response_bodyRaw response text
retry_afterSeconds to wait (RateLimitError only)
importlistingsapiclient=listingsapi.ListingsAPI()
try:
result=client.locations.add(
name="Acme Dental",
description=DESCRIPTION, # min 200 characterssub_category_id=1432,
country_iso="US",
city="New York",
)
exceptlistingsapi.ValidationErrorase:
# Raised client-side for missing/short mandatory fields, or by the API# (e.g. SY10005 description too short, SY10072 duplicate storeId)forerrine.errors:
print(err["code"], err["message"])
exceptlistingsapi.AuthenticationError:
print("Invalid API key. Check LISTINGSAPI_KEY.")
exceptlistingsapi.RateLimitErrorase:
print(f"Rate limited. Retry after {e.retry_after}s.")
exceptlistingsapi.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:

importtimeimportlistingsapidefwith_backoff(fn, attempts=3):
forattemptinrange(attempts):
try:
returnfn()
exceptlistingsapi.RateLimitErrorase:
ifattempt==attempts-1:
raisetime.sleep(e.retry_afteror2**attempt)
page=with_backoff(lambda: client.locations.list(first=50))

See examples/12_error_handling.py for a runnable walkthrough.

Configuration

client=listingsapi.ListingsAPI(
api_key="your-api-key",
base_url="https://listingsapi.com", # defaulttimeout=300.0,
max_retries=3,
)
OptionDefaultDescription
api_keyLISTINGSAPI_KEY env varYour API key
base_urlhttps://listingsapi.comAPI host
timeout240.0Request timeout in seconds
max_retries2Automatic retries on 429 and 5xx for read (GET) requests; writes are never auto-retried

Examples and development

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

License

MIT