Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing
Project description
listingsAPI Python SDK
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
Contents
- 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
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"
import listingsapi
client = listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY
client = listingsapi.ListingsAPI(api_key="your-key") # or pass it explicitly
Quickstart
Auto-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}")
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)
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.
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 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")
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 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.
Listings
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")
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 location
url = 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 plan
countries = client.countries() # supported countries and states
subcategories = 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 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 reference
Supporting APIs (client-level)
| 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 |
Locations
| 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 |
Posts
| 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 |
Reviews
| 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 |
Listings
| 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 |
Analytics
| 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 |
Photos
| 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 |
Connected accounts
| 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 |
Workflows
| 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 |
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
| 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 429 and 5xx responses (default 2 attempts, honoring Retry-After). 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.
Configuration
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 |
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/synup/listingsapi-python-sdk.git
cd listingsapi-python-sdk
pip install -e ".[dev]"
pytest
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file listingsapi-0.5.0.tar.gz.
File metadata
- Download URL: listingsapi-0.5.0.tar.gz
- Upload date:
- Size: 37.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b2994149e4dc97c82462b0f6c68335f04880d46e100c2c32c82f529062c8765
|
|
| MD5 |
8fc1b90e59ef980bc29d4c59b746a808
|
|
| BLAKE2b-256 |
412bcb22297f86728c43b3b92f992863322424bc808dff9565fa11ab529811e7
|
Provenance
The following attestation bundles were made for listingsapi-0.5.0.tar.gz:
Publisher:
publish.yml on synup/listingsapi-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
listingsapi-0.5.0.tar.gz -
Subject digest:
1b2994149e4dc97c82462b0f6c68335f04880d46e100c2c32c82f529062c8765 - Sigstore transparency entry: 2136005721
- Sigstore integration time:
-
Permalink:
synup/listingsapi-python-sdk@291a0bddce8551e9251a6d5d8cb9949ec9f421cd -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/synup
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@291a0bddce8551e9251a6d5d8cb9949ec9f421cd -
Trigger Event:
push
-
Statement type:
File details
Details for the file listingsapi-0.5.0-py3-none-any.whl.
File metadata
- Download URL: listingsapi-0.5.0-py3-none-any.whl
- Upload date:
- Size: 32.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
191c778fb4a9c0646a1d93f746e833642e57fa45ef4227742d438694b31aba29
|
|
| MD5 |
dc0ecf95b36de0a42b04cb37f38d6771
|
|
| BLAKE2b-256 |
00524b595417c297c25492ebfebfd1dcb25624233ca3bdfb9446c3a4bda651d1
|
Provenance
The following attestation bundles were made for listingsapi-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on synup/listingsapi-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
listingsapi-0.5.0-py3-none-any.whl -
Subject digest:
191c778fb4a9c0646a1d93f746e833642e57fa45ef4227742d438694b31aba29 - Sigstore transparency entry: 2136005759
- Sigstore integration time:
-
Permalink:
synup/listingsapi-python-sdk@291a0bddce8551e9251a6d5d8cb9949ec9f421cd -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/synup
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@291a0bddce8551e9251a6d5d8cb9949ec9f421cd -
Trigger Event:
push
-
Statement type: