Skip to main content

Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing

Project description

listingsAPI Python SDK

PyPI Python License: MIT

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

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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

listingsapi-0.5.0.tar.gz (37.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

listingsapi-0.5.0-py3-none-any.whl (32.1 kB view details)

Uploaded Python 3

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

Hashes for listingsapi-0.5.0.tar.gz
Algorithm Hash digest
SHA256 1b2994149e4dc97c82462b0f6c68335f04880d46e100c2c32c82f529062c8765
MD5 8fc1b90e59ef980bc29d4c59b746a808
BLAKE2b-256 412bcb22297f86728c43b3b92f992863322424bc808dff9565fa11ab529811e7

See more details on using hashes here.

Provenance

The following attestation bundles were made for listingsapi-0.5.0.tar.gz:

Publisher: publish.yml on synup/listingsapi-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for listingsapi-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 191c778fb4a9c0646a1d93f746e833642e57fa45ef4227742d438694b31aba29
MD5 dc0ecf95b36de0a42b04cb37f38d6771
BLAKE2b-256 00524b595417c297c25492ebfebfd1dcb25624233ca3bdfb9446c3a4bda651d1

See more details on using hashes here.

Provenance

The following attestation bundles were made for listingsapi-0.5.0-py3-none-any.whl:

Publisher: publish.yml on synup/listingsapi-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page