# listingsAPI Developer Platform — Full Documentation > listingsAPI is a local-marketing platform for managing business listings, reviews, social posts, rankings, and AI-search visibility across hundreds of publishers. The listingsAPI Developer Platform exposes that system as a versioned REST API — a facade over Synup's federated GraphQL services — plus a typed Python SDK, a TypeScript/Node SDK, and an MCP server for LLM clients. This file concatenates the developer guides, a per-tag REST API reference, and the SDK quickstarts. REST base URL `{base_url}` (paths under `/api/v4/`); authenticate with `Authorization: API `. The complete machine-readable contract is at https://listingsapi.com/openapi/spec.json and https://listingsapi.com/openapi/spec.yaml. # Guides ## Getting started Source: https://listingsapi.com/docs/getting-started This guide walks you through signing up, issuing your first API key, and making a request against the listingsAPI REST API. [Sign up](/signup) with your work email, or sign in with Google or a magic link from the [login page](/login). ## 1. Create an account Head to the [sign-up page](/signup) and register with your work email, or use **Continue with Google**. Already have an account? [Sign in](/login) instead. ## 2. Create a project Head to the [dashboard](/dashboard) and create a new project. Each project carries its own keys and usage counters. ## 3. Issue an API key In your project, navigate to **API Keys → New key**. Your key stays viewable in the dashboard with a copy button, so you can grab it again later. Treat it like a password — if it leaks, revoke it and issue a new one. ## 4. Make your first request ```bash curl https://listingsapi.com/api/v4/locations \ -H "Authorization: API $LISTINGSAPI_KEY" ``` You should see a JSON response with your first page of locations. ## Next steps - [Authentication](/docs/authentication) — API keys and key rotation. - [SDKs](/sdks) — typed Python and Node clients. ## Authentication Source: https://listingsapi.com/docs/authentication listingsAPI authenticates server-to-server integrations with API keys. | Header | Format | | --- | --- | | `Authorization` | `API ` | ## API keys Production keys (``) grant full access within their project scope. Create and rotate keys in the [dashboard → API Keys](/dashboard/api-keys) area. Live keys grant full access to your project. Keep them on the server side. # REST API reference ### locations (11 operations) Operations from the locations service. - GET /api/v4/countries — List of supported countries. - GET /api/v4/locations — All locations that belong to an account. - POST /api/v4/locations — Create a new location. - GET /api/v4/locations-by-ids — Fetch location info via cursor ids. - GET /api/v4/locations-by-store-codes — Get locations by the Store Codes. - POST /api/v4/locations/archive — Archive locations. - POST /api/v4/locations/cancel_archive — Cancel archival of locations. - POST /api/v4/locations/photos/star — Mark photos as favourite. - GET /api/v4/locations/search — Search for locations. - POST /api/v4/locations/update — Update information on a location. - GET /api/v4/sub-categories — List of sub-categories. ### posts (7 operations) Operations from the posts service. - POST /api/v4/bulk-posts — Create social posts for multiple locations. - GET /api/v4/bulk-posts/{postId} — Individual social post view page. - GET /api/v4/locations/{locationId}/bulk-posts — List of post campaigns for tags/locations. - GET /api/v4/locations/{locationId}/posts — List of post campaigns for tags/locations. - POST /api/v4/posts — Create social posts for multiple locations. - DELETE /api/v4/posts/{postId} — Delete a social post. - GET /api/v4/posts/{postId} — Individual social post view page. ### interactions (10 operations) Operations from the interactions service. - GET /api/v4/locations/{locationId}/review-analytics-overview — Analytics KPIs for a single location over a date range. - GET /api/v4/locations/{locationId}/reviews — Paginated list of interactions (reviews / Q&A / social) for a single location, with filters. - GET /api/v4/locations/{locationId}/reviews/settings — Per-location interactions settings (connected sites, thresholds). - POST /api/v4/locations/reviews/respond — Post a response to a review / Q&A / social interaction on the source site. - POST /api/v4/locations/reviews/respond/archive — Archive a response item by ID (soft-delete from the local cache). - POST /api/v4/locations/reviews/respond/edit — Edit a previously posted response to a review. - POST /api/v4/locations/reviews/settings/edit — Update per-location interactions settings (connected sites, positive threshold). - GET /api/v4/reviewDetails — Fetch full interaction records by ID. - GET /api/v4/reviews/site-config — Per-site metadata (colors, icons, plan support). - GET /api/v4/rollup_interactions — Paginated list of interactions across many locations (rollup scope). ### insights (3 operations) Operations from the insights service. - GET /api/v4/locations/{locationId}/bing-analytics — Bing Places v4 metrics for a single location over a date range. - GET /api/v4/locations/{locationId}/facebook-analytics — Facebook v4 metrics for a single location over a date range. - GET /api/v4/locations/{locationId}/google-analytics — Google My Business v4 metrics for a single location over a date range. ### v2app (17 operations) Operations from the v2app service. - GET /api/v4/connected-accounts — connectedAccountsInfo - GET /api/v4/connected-accounts/{connectedAccountId}/connection-suggestions — connectionSuggestionsForAccount - GET /api/v4/connected-accounts/{connectedAccountId}/details — connectedAccountDetails - GET /api/v4/connected-accounts/{connectedAccountId}/folders — getFoldersUnderGoogleAccount - POST /api/v4/connected-accounts/confirm-matches — confirmConnectMatches - POST /api/v4/connected-accounts/connect-facebook — bulkConnectLinkForFacebook - POST /api/v4/connected-accounts/connect-google — bulkConnectLinkForGoogle - POST /api/v4/connected-accounts/connect-listing — connectListing - POST /api/v4/connected-accounts/connected-account-listings — Listings belonging to a connected account. - POST /api/v4/connected-accounts/create-location-from-listing — createLocationFromConnectedAccountListing - POST /api/v4/connected-accounts/disconnect-facebook — fbBulkDisconnect - POST /api/v4/connected-accounts/disconnect-google — gmbBulkDisconnect - … 5 more; see https://listingsapi.com/openapi/spec.json ### crawler (6 operations) Operations from the crawler service. - GET /api/v4/locations/{locationId}/listings/duplicates — Get duplicate listings for the given location. - GET /api/v4/locations/{locationId}/listings/premium — Get listings for a location. - GET /api/v4/locations/{locationId}/voice-assistants — Get voice assistants for a location. - GET /api/v4/locations/listings/duplicates — Duplicate listing rollup for tag. - POST /api/v4/locations/listings/mark-as-duplicate — Marking the duplicate listing item. - POST /api/v4/locations/listings/mark-as-not-duplicate — Marking the non duplicate listing item. ### media-manager (4 operations) Operations from the media-manager service. - GET /api/v4/locations/{locationId}/photos — List media files attached to a single location (starred + unstarred). - POST /api/v4/locations/photos — Attach media files to a location as photos (stitched via schemastitcher). - POST /api/v4/locations/photos/remove — Remove photos from a location. - GET /api/v4/locations/photos/requests/{requestId} — Return the status of a bulk add-media-to-location request. # Python SDK ### Python SDK Source: https://listingsapi.com/sdks/python The `listingsapi` package is the official Python client for listingsAPI. It wraps the REST layer with typed resource classes, cursor-based pagination, automatic retries on transient errors, and a named exception for every failure mode, including error payloads the platform returns with HTTP 200. ## Install ```bash pip install listingsapi ``` Requires Python 3.9 or later. The only runtime dependency is `requests`. ## Authenticate ```python import listingsapi # Reads LISTINGSAPI_KEY from the environment automatically client = listingsapi.ListingsAPI() # Or pass the key explicitly client = listingsapi.ListingsAPI(api_key="your-api-key") ``` Get your API key from **API Keys → New key** in the [listingsAPI developer dashboard](/dashboard). The client takes three optional keyword arguments: | Option | Default | Description | |---|---|---| | `base_url` | `https://listingsapi.com` | API host to send requests to | | `timeout` | `240.0` | Per-request timeout in seconds | | `max_retries` | `2` | Automatic retries on 429 and 5xx responses, honoring `Retry-After` | See [Configuration](/sdks/python/configuration) for details. ## Quick example ```python import listingsapi client = listingsapi.ListingsAPI() page = client.locations.list(first=5) for loc in page: print(loc.name, loc.city) print(f"has_more: {page.has_more}") ``` ## Resources Every resource lives under a property of the client: | Property | What it manages | |---|---| | `client.locations` | Business locations: list, search, one-call create, update, archive | | `client.reviews` | Reviews and responses, plus analytics under `client.reviews.analytics` | | `client.posts` | Announcement, event, and offer posts to Google and Facebook | | `client.listings` | Premium and voice listings, duplicates, connect and disconnect | | `client.analytics` | Google, Bing, and Facebook profile insights | | `client.photos` | Location photos: upload, remove, star | | `client.connected_accounts` | OAuth connections to Google and Facebook | | `client.workflows` | Pre-built automations: review auto-reply, reputation report, listings health audit | Four supporting methods live on the client itself: ```python client.plan_sites() # directories included in your plan client.subcategories() # business categories (IDs for locations.add) client.countries() # supported countries and states (ISO codes) client.subscriptions() # active subscriptions for the account ``` ## Pagination Methods that return lists use cursor-based pagination. Every paged method returns a `SyncPage`: iterable, with `.has_more`, `.next_page()`, `.auto_paging_iter()`, and `.total`. ```python # Fetch one page page = client.locations.list(first=25) for loc in page: print(loc.name) # Check if more pages exist if page.has_more: next_page = page.next_page() # Auto-paginate through every result for loc in client.locations.list(first=100).auto_paging_iter(): print(loc.name) ``` ## Response objects All methods return `APIObject` (attribute + dict access) or `SyncPage` (iterable, paged). `APIObject` also supports `.get()` and `.to_dict()`. ```python loc = client.locations.retrieve(16808) # Attribute access print(loc.name) print(loc.city) # Dict-style access print(loc["stateIso"]) # Safe access with a default print(loc.get("website", "no website")) # Raw dict data = loc.to_dict() ``` ## Error handling Every SDK error derives from `listingsapi.ListingsAPIError`. API failures raise `APIError` or one of its subclasses: `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `ValidationError`, `RateLimitError` (with `.retry_after`), and `InternalServerError`. Network failures raise `APIConnectionError`. `APIError` carries `status_code`, `code` (the first platform error code, e.g. `SY10005`), `errors` (every entry normalized to `{"code", "message", "context"}` dicts), and `response_body`. The platform reports some failures inside an HTTP 200 body: top-level `errors[]` payloads (invalid tokens come back as `SY90005`) and mutation envelopes with `success=false`. The SDK raises for those too, so you never inspect `success` flags yourself. ```python import listingsapi client = listingsapi.ListingsAPI() try: client.locations.retrieve(999999) except listingsapi.NotFoundError: print("No such location") except listingsapi.RateLimitError as e: print(f"Rate limited, retry in {e.retry_after}s") except listingsapi.APIError as e: print(e.status_code, e.code) for err in e.errors: print(err["code"], err["message"]) ``` See [Error handling](/sdks/python/error-handling) for the full exception hierarchy. ## Next steps - [Installation](/sdks/python/installation): virtual environments, version pins - [Quickstart](/sdks/python/quickstart): first calls, create a location, respond to a review - [Configuration](/sdks/python/configuration): timeouts, retries, base URL - [Use cases](/sdks/python/use-cases): end-to-end recipes - [Locations](/sdks/python/resources/locations), [Reviews](/sdks/python/resources/reviews), [Listings](/sdks/python/resources/listings), [Analytics](/sdks/python/resources/analytics) ### Installation Source: https://listingsapi.com/sdks/python/installation ## Requirements - Python 3.9 or later - `pip` 21+ ## Install ```bash pip install listingsapi ``` The package is published as `listingsapi` on PyPI. Its only runtime dependency is `requests`, which pip pulls in automatically; no extra install step is needed. ## Virtual environment (recommended) ```bash python -m venv .venv source .venv/bin/activate # Windows: .venv\\Scripts\\activate pip install listingsapi ``` ## Pin the version ```bash pip install "listingsapi==0.5.0" ``` Or add it to your `requirements.txt`: ```bash listingsapi==0.5.0 ``` Or `pyproject.toml`: ```bash dependencies = [ "listingsapi>=0.5,<1.0", ] ``` ## Verify ```python import listingsapi print(listingsapi.__version__) ``` You should see `0.5.0` (or the version you installed). ## Next steps - [Quickstart](/sdks/python/quickstart): connect and make your first API call - [Configuration](/sdks/python/configuration): timeouts, retries, base URL overrides ### Quickstart Source: https://listingsapi.com/sdks/python/quickstart ## 1. Set your API key Get your key from **API Keys → New key** in the [listingsAPI developer dashboard](/dashboard). The SDK reads it from the `LISTINGSAPI_KEY` environment variable automatically. ```bash export LISTINGSAPI_KEY="your-api-key" ``` ## 2. List your locations ```python import listingsapi client = listingsapi.ListingsAPI() page = client.locations.list(first=5) for loc in page: print(loc.name, "--", loc.city, loc.stateIso) print(f"\\nShowing {len(page)} locations, has_more={page.has_more}") ``` ```bash python quickstart.py ``` Expected output: ```bash Acme Dental Downtown -- New York NY Acme Dental Midtown -- New York NY Acme Dental Brooklyn -- Brooklyn NY Showing 3 locations, has_more=False ``` To walk every page automatically, use `client.locations.list(first=100).auto_paging_iter()`; it follows cursor pages so you never manage cursors by hand. ## 3. 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 minimum, category, country, city) before any network call, so a bad payload fails fast with a clear `ValidationError`. ```python import listingsapi client = listingsapi.ListingsAPI() result = client.locations.add( name="Acme Dental Uptown", description=( "Acme Dental Uptown is a family-owned dental practice on the Upper " "West Side of 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="482 Columbus Ave", state_iso="NY", postal_code="10024", phone="6443859313", ) print(result.location.id) print(result.location.name) ``` Publishers use the description as the primary listing copy, which is why the API requires at least 200 characters. Get valid `sub_category_id` values from `client.subcategories()`. The new ID comes back as a base64 string like `TG9jYXRpb246MTY4MDg=`. Numeric and base64 IDs are interchangeable everywhere the SDK takes a location ID, so `client.locations.retrieve(16808)` and `client.locations.retrieve("TG9jYXRpb246MTY4MDg=")` fetch the same location. ## 4. Respond to a review List reviews for a location, then reply using the review's `interactionId`: ```python import listingsapi client = listingsapi.ListingsAPI() reviews = client.reviews.list(16808, first=10, rating_filters=[4, 5]) for review in reviews: print(review.rating, "-", review.get("content")) review = reviews[0] client.reviews.respond( interaction_id=review.interactionId, content="Thank you for the kind review! We look forward to seeing you again soon.", ) ``` If the platform rejects the reply, the SDK raises `ValidationError` even when the HTTP status is 200; you never check `success` flags yourself. ## 5. Handle errors ```python import listingsapi try: client = listingsapi.ListingsAPI(api_key="bad-key") client.locations.list() except listingsapi.AuthenticationError as e: print("Invalid key:", e.code) ``` Invalid keys come back either as HTTP 401 or as an HTTP 200 body carrying the platform code `SY90005`; the SDK raises `AuthenticationError` for both. See [Error handling](/sdks/python/error-handling) for the full exception hierarchy. ## Next steps - [Configuration](/sdks/python/configuration): custom timeouts and retries - [Use cases](/sdks/python/use-cases): end-to-end recipes - [Locations resource](/sdks/python/resources/locations): create, update, search ### Configuration Source: https://listingsapi.com/sdks/python/configuration ## Constructor signature ```python listingsapi.ListingsAPI( api_key: str | None = None, *, base_url: str | None = None, timeout: float = 240.0, max_retries: int = 2, ) ``` All arguments are optional. If `api_key` is omitted the client reads `LISTINGSAPI_KEY` from the environment and raises `AuthenticationError` at construction time if neither is set. ## Options | Option | Type | Default | Description | |---|---|---|---| | `api_key` | `str \| None` | `None` | API key. Falls back to the `LISTINGSAPI_KEY` environment variable. | | `base_url` | `str \| None` | `https://listingsapi.com` | API origin. A trailing slash is stripped automatically. | | `timeout` | `float` | `240.0` | Per-request timeout in seconds (applies to each HTTP request individually). | | `max_retries` | `int` | `2` | Automatic retries on `429` and `5xx` responses. Set to `0` to disable. | ## API key ```bash export LISTINGSAPI_KEY="your-api-key" ``` ```python import listingsapi # From environment (recommended) client = listingsapi.ListingsAPI() # Explicit key client = listingsapi.ListingsAPI(api_key="your-api-key") ``` ## Timeout Default is `240.0` seconds. For short-lived scripts or interactive use you may want a tighter value: ```python client = listingsapi.ListingsAPI(timeout=30.0) ``` The timeout applies to each individual HTTP request. Bulk pagination calls (`auto_paging_iter`) respect the per-request timeout independently. ## Retries The SDK mounts a `urllib3` `Retry` adapter on its `requests` session that automatically retries `429`, `500`, `502`, `503`, and `504` responses for `GET` and `POST` methods. The default is `2` retries with a 0.5-second exponential backoff factor. `Retry-After` headers are honoured, so a `429` that names a wait time is retried after exactly that long. ```python # More aggressive retry for long-running batch jobs client = listingsapi.ListingsAPI(max_retries=5) # Disable retries entirely (e.g. when you want fast failures) client = listingsapi.ListingsAPI(max_retries=0) ``` A `429` that survives every retry surfaces as `RateLimitError`; see [Error handling](/sdks/python/error-handling) for a manual backoff recipe. ## Base URL override Useful for staging environments or internal proxies: ```python client = listingsapi.ListingsAPI(base_url="https://staging.example.com") ``` The trailing slash is stripped automatically. All requests are made under the `/api/v4/` path on this origin. ## Logging The SDK logs through the standard `logging` module under the `listingsapi` logger. At `DEBUG` level it records the method and URL of every request (and query parameters for `GET`); request bodies and your API key are never logged. ```python import logging logging.basicConfig() logging.getLogger("listingsapi").setLevel(logging.DEBUG) client = listingsapi.ListingsAPI() client.locations.list(first=1) ``` ## Next steps - [Quickstart](/sdks/python/quickstart): your first API call - [Error handling](/sdks/python/error-handling): exception types and retry guidance ### Error handling Source: https://listingsapi.com/sdks/python/error-handling All exceptions are importable from the top-level `listingsapi` module. ## Exception hierarchy ``` ListingsAPIError ├── APIError API-reported failure: non-2xx, or an error payload in HTTP 200 │ ├── AuthenticationError 401, or payload codes SY90005 / SY90001 │ ├── PermissionDeniedError 403, or payload code SY90003 │ ├── NotFoundError 404 │ ├── ValidationError 400 / 422, mutation failures, client-side validation │ ├── RateLimitError 429, or payload code RATE_LIMITED (carries .retry_after) │ └── InternalServerError 5xx (safe to retry) └── APIConnectionError network failure: DNS, connection refused, timeout ``` Catch `ListingsAPIError` to handle everything the SDK can raise, `APIError` for anything the API reported, or a specific subclass for targeted handling. ## Exception attributes `APIError` and its subclasses carry: | Attribute | Type | Description | |---|---|---| | `status_code` | `int \| None` | HTTP status code (can be `200` for payload-level errors) | | `code` | `str \| None` | First platform error code, e.g. `SY10005` | | `errors` | `list[dict]` | Normalized error entries, each `{"code", "message", "context"}` | | `response_body` | `str \| None` | Raw response text from the API | | `retry_after` | `float \| None` | `RateLimitError` only; parsed from the `Retry-After` header | `APIConnectionError` inherits directly from `ListingsAPIError` and has none of these attributes; the exception message describes the network failure. ## Catching exceptions ```python import listingsapi client = listingsapi.ListingsAPI() try: location = client.locations.retrieve(16808) except listingsapi.AuthenticationError as e: print("Invalid API key:", e.code) except listingsapi.PermissionDeniedError as e: print("Not allowed:", e.status_code) except listingsapi.NotFoundError as e: print("No such location:", e.status_code) except listingsapi.ValidationError as e: for err in e.errors: print(err["code"], err["message"], err["context"]) except listingsapi.RateLimitError as e: print("Rate limited, retry in", e.retry_after, "seconds") except listingsapi.InternalServerError as e: print("Server error (safe to retry):", e.status_code) except listingsapi.APIConnectionError as e: print("Network failure:", e) except listingsapi.APIError as e: print("Unexpected API error:", e.status_code, e.code, e.response_body) ``` ## Errors inside HTTP 200 responses The platform sometimes reports failures inside a `200` response body. The SDK inspects every response and raises, so you never check `success` flags or scan `errors` arrays yourself: - **Top-level `errors[]` bodies** are mapped by code: `SY90005` and `SY90001` raise `AuthenticationError`, `SY90003` raises `PermissionDeniedError`, `RATE_LIMITED` raises `RateLimitError`, and any other code raises `APIError`. - **Mutation envelopes** (create, update, archive, post publishing) that come back with `success: false` or a per-mutation `errors[]` list raise `ValidationError`. In both cases `status_code` is the actual HTTP status (often `200`), `code` is the first platform code, and `errors` holds every normalized entry: ```python import listingsapi client = listingsapi.ListingsAPI() try: result = client.locations.update({"id": 16808, "phone": "not-a-phone"}) except listingsapi.ValidationError as e: print(e.status_code) # 200: the API accepted the request but rejected the mutation print(e.code) # first platform code, e.g. "SY10005" for err in e.errors: print(err["code"], err["message"], err["context"]) ``` ## Client-side validation `locations.add()` and the post helpers (`posts.create_announcement()`, `posts.create_event()`, `posts.create_offer()`, `posts.bulk_publish()`) validate their payload locally and raise `ValidationError` **before any network call**, so a bad payload fails fast with a clear message instead of a round trip. For `locations.add()`: - `name` must be 2 to 150 characters - `description` must be at least 200 characters - `sub_category_id` is required - `country_iso` is required - `city` is required unless `hide_address=True` (service-area businesses) For the post helpers: - `name` is required and `location_ids` must not be empty - `sites` must be a non-empty subset of `GOOGLE`, `FACEBOOK` - `message` is required; when it is a per-site dict, every target site needs text - `cta_type` and `cta_url` must be provided together, and `cta_type` must be one of `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `GET_OFFER` - `title` is required for events and offers On these locally raised errors `status_code`, `code`, and `response_body` are `None` and `errors` is empty; `str(e)` lists every problem found: ```python import listingsapi client = listingsapi.ListingsAPI() try: client.locations.add( name="Acme Dental", description="Too short.", sub_category_id=1432, country_iso="US", city="New York", ) except listingsapi.ValidationError as e: print(e) # Invalid location: description must be at least 200 characters (got 10) print(e.status_code) # None: raised before any network call ``` ## Automatic retries The SDK retries `429`, `500`, `502`, `503`, and `504` automatically for `GET` and `POST` methods (default `2` retries, 0.5-second exponential backoff factor, `Retry-After` honoured). Configure via the `max_retries` constructor argument; see [Configuration](/sdks/python/configuration). ## Backing off on RateLimitError When a `429` survives the built-in retries, `RateLimitError.retry_after` carries the server's `Retry-After` value in seconds (or `None` if the header was absent). A simple recipe for batch jobs: ```python import random client = listingsapi.ListingsAPI(max_retries=0) # manual retry control def with_backoff(call, max_attempts=5): for attempt in range(max_attempts): try: return call() except listingsapi.RateLimitError as e: if attempt == max_attempts - 1: raise if e.retry_after is not None: delay = e.retry_after else: delay = min(2 ** attempt, 30) + random.random() time.sleep(delay) page = with_backoff(lambda: client.locations.list(first=50)) for location in page: print(location.name) ``` ## Importing exceptions ```python # All exceptions from the top-level module listingsapi.ListingsAPIError listingsapi.APIError listingsapi.AuthenticationError listingsapi.PermissionDeniedError listingsapi.NotFoundError listingsapi.ValidationError listingsapi.RateLimitError listingsapi.InternalServerError listingsapi.APIConnectionError # Or import individually from listingsapi import RateLimitError, ValidationError ``` ## See also - [Configuration](/sdks/python/configuration): `max_retries` and `timeout` - [Quickstart](/sdks/python/quickstart): your first error-handled script ### Use cases Source: https://listingsapi.com/sdks/python/use-cases 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. ```python 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()`. ```python import csv 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](/sdks/python/resources/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`). ```python 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. ```python 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](/sdks/python/resources/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. ```python 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()`: ```python 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. ```python 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](/sdks/python/resources/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. ```python 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. ```python 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](/sdks/python/resources/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. ```python 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: ```python 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. ```python 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 - [Locations](/sdks/python/resources/locations): create, update, search, archive - [Reviews](/sdks/python/resources/reviews): list, respond, analytics - [Listings](/sdks/python/resources/listings): premium, voice, duplicates - [Analytics](/sdks/python/resources/analytics): Google, Bing, Facebook - [Error handling](/sdks/python/error-handling): exception types and retry guidance # Node SDK ### Node SDK Source: https://listingsapi.com/sdks/node `listingsapi-js` is the official Node.js/TypeScript client for listingsAPI v4. It manages locations, directory listings, reviews, posts, photos, and profile analytics from a single flat client, with cursor-based pagination, automatic ID encoding, built-in retries, and typed errors out of the box. ## Install ```bash npm install listingsapi-js ``` ## Authenticate Get your API key from **API Keys → New key** in the [listingsAPI developer dashboard](/dashboard). ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // reads LISTINGSAPI_KEY from env const explicit = new ListingsAPI({ apiKey: 'your-api-key' }); // or pass it directly ``` The constructor also accepts `baseUrl` (default `https://listingsapi.com`), `timeout` (default 240000 ms), and `maxRetries` (default 2). See [Configuration](/sdks/node/configuration). ## Quick example ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const result = await client.fetchAllLocations({ first: 5 }); if (!Array.isArray(result)) { for (const location of result.locations) { console.log(location.name, '--', location.city); } } ``` ## Resources All methods live directly on the client instance: | Methods | What they manage | |---|---| | `fetchAllLocations`, `searchLocations`, `fetchLocationsByIds`, `fetchLocationsByStoreCodes`, `createLocation`, `updateLocation`, `archiveLocations`, `cancelArchiveLocations` | Business locations: list, search, create, update, archive | | `fetchInteractions`, `fetchReviewDetails`, `respondToReview`, `editReviewResponse`, `archiveReviewResponse`, `fetchReviewSettings`, `editReviewSettings`, `fetchReviewSiteConfig`, `fetchReviewPhrases`, `fetchReviewAnalyticsOverview`, `fetchReviewAnalyticsTimeline`, `fetchReviewAnalyticsSitesStats` | Reviews: list, reply, edit and archive responses, settings, phrase and analytics data | | `bulkPublish`, `createAnnouncement`, `createEvent`, `createOffer`, `createPost`, `fetchPost`, `deletePost`, `fetchLocationPosts`, `fetchBulkPost`, `fetchLocationBulkPosts` | Posts: announcements, events, and offers on Google and Facebook | | `fetchPremiumListings`, `fetchVoiceListings`, `fetchDuplicateListings`, `fetchAllDuplicateListings`, `markListingsAsDuplicate`, `markListingsAsNotDuplicate` | Directory listings: premium, voice, duplicates | | `fetchGoogleAnalytics`, `fetchBingAnalytics`, `fetchFacebookAnalytics` | Google, Bing, and Facebook profile analytics | | `fetchLocationPhotos`, `addLocationPhotos`, `removeLocationPhotos`, `starLocationPhotos`, `fetchPhotoUploadStatus` | Location photos | | `fetchConnectedAccounts`, `fetchConnectedAccountDetails`, `fetchConnectedAccountFolders`, `fetchConnectedAccountListings`, `fetchConnectionSuggestions`, `connectGoogleAccount`, `connectFacebookAccount`, `disconnectGoogleAccount`, `disconnectFacebookAccount`, `triggerConnectedAccountMatches`, `confirmConnectedAccountMatches`, `getOauthConnectUrl`, `oauthDisconnect`, `connectListing`, `disconnectListing`, `createGmbListing` | OAuth connections to Google and Facebook, listing links | | `fetchPlanSites`, `fetchCountries`, `fetchSubscriptions`, `fetchSubcategories` | Plan directories, supported countries, subscriptions, business categories | ## Pagination Methods that return lists support cursor-based pagination. ```typescript // Single page const page = await client.fetchAllLocations({ first: 25 }); if (!Array.isArray(page)) { console.log(page.locations); // Location[] console.log(page.pageInfo); // { hasNextPage, hasPreviousPage, startCursor, endCursor } // Next page if (page.pageInfo.hasNextPage) { const next = await client.fetchAllLocations({ first: 25, after: page.pageInfo.endCursor!, }); } } // Auto-paginate: returns a flat Location[] const all = await client.fetchAllLocations({ fetchAll: true, pageSize: 100 }); ``` `searchLocations` and `fetchInteractions` follow the same pattern: a page object by default, a flat array with `fetchAll: true`. ## ID encoding Pass numeric location IDs anywhere: the SDK base64-encodes them automatically. Already-encoded IDs are accepted as-is, and the encoder is exported if you need it yourself. ```typescript import { encodeLocationId } from 'listingsapi-js'; // These are equivalent await client.fetchPremiumListings(16808); await client.fetchPremiumListings('TG9jYXRpb246MTY4MDg='); encodeLocationId(16808); // 'TG9jYXRpb246MTY4MDg=' ``` ## Error handling Every SDK error derives from `ListingsAPIError`. API failures throw `APIError` or one of its subclasses, whether the platform reports them as an HTTP status or inside an HTTP 200 body (invalid tokens and mutation validation arrive as `errors[]` payloads, and mutations can report `success: false`). The SDK throws for all of these, so you never inspect `success` flags yourself. Network failures throw `APIConnectionError`. | Class | Thrown for | |---|---| | `AuthenticationError` | 401, or invalid-token error payloads | | `PermissionDeniedError` | 403, or permission error payloads | | `NotFoundError` | 404 | | `ValidationError` | 400/422, or mutations returning `success: false` / `errors[]` | | `RateLimitError` | 429, carries `retryAfter` in seconds | | `InternalServerError` | 5xx | | `APIConnectionError` | Network failure, nothing reached the API | Every `APIError` carries `statusCode`, `responseBody`, `code` (the first platform error code, e.g. `"SY10005"`), and `errors` (each entry normalized to `{ code, message, context }`). ```typescript import { ListingsAPI, APIError, RateLimitError } from 'listingsapi-js'; const client = new ListingsAPI(); try { await client.fetchAllLocations(); } catch (error) { if (error instanceof RateLimitError) { console.log(error.retryAfter); // seconds to wait } else if (error instanceof APIError) { console.log(error.statusCode); // 401, 404, 500, or 200 for payload-level errors console.log(error.code); // first platform code, e.g. 'SY10005' console.log(error.errors); // [{ code, message, context }, ...] } } ``` The client retries 429 and 5xx responses automatically (2 attempts by default, honoring `Retry-After`). See [Error handling](/sdks/node/error-handling) for retry patterns and status-code guidance. ## Next steps - [Installation](/sdks/node/installation): version pins, ESM vs CJS - [Quickstart](/sdks/node/quickstart): your first working script - [Configuration](/sdks/node/configuration): base URL overrides and TypeScript setup - [Use cases](/sdks/node/use-cases): ten end-to-end recipes - [Locations](/sdks/node/resources/locations), [Reviews](/sdks/node/resources/reviews), [Listings](/sdks/node/resources/listings), [Analytics](/sdks/node/resources/analytics) ### Installation Source: https://listingsapi.com/sdks/node/installation ## Requirements - Node.js 18 or later - npm, pnpm, or yarn The SDK uses native `fetch` (built into Node 18+) and has zero runtime dependencies. ## Install ```bash npm install listingsapi-js ``` ```bash # pnpm pnpm add listingsapi-js # yarn yarn add listingsapi-js ``` ## ESM and CJS `listingsapi-js` ships both an ESM build and a CommonJS build. The correct entry point is chosen automatically based on your project's `type` setting. **ESM** (`"type": "module"` in `package.json` or `.mts`/`.mjs` extension): ```typescript import { ListingsAPI, APIError } from 'listingsapi-js'; ``` **CJS** (default Node module resolution or `.cts`/`.cjs` extension): ```javascript const { ListingsAPI, APIError } = require('listingsapi-js'); ``` ## TypeScript Type definitions are bundled in the package, so no separate `@types/listingsapi-js` is needed. The SDK is written in TypeScript and the `dist/` folder ships `.d.ts` files (and `.d.cts` for the CommonJS build) for all exports. ```bash # No extra step needed: types come with the package npm install listingsapi-js ``` ## Pin the version ```bash npm install listingsapi-js@0.3.0 ``` Or in `package.json`: ```json { "dependencies": { "listingsapi-js": "^0.3.0" } } ``` ## Verify ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI({ apiKey: 'your-api-key' }); console.log('listingsapi-js ready', typeof client.fetchAllLocations); ``` ```bash npx tsx verify.ts ``` ## Next steps - [Quickstart](/sdks/node/quickstart): connect and make your first API call - [Configuration](/sdks/node/configuration): base URL overrides and TypeScript project setup ### Quickstart Source: https://listingsapi.com/sdks/node/quickstart ## 1. Set your API key Get your key from **API Keys → New key** in the [listingsAPI developer dashboard](/dashboard). ```bash export LISTINGSAPI_KEY="your-api-key" ``` `new ListingsAPI()` reads `LISTINGSAPI_KEY` from the environment automatically, or pass `apiKey` explicitly. ## 2. List your locations ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const result = await client.fetchAllLocations({ first: 5 }); if (!Array.isArray(result)) { for (const location of result.locations) { console.log(location.name, '--', location.city, location.stateIso); } console.log('\\nhasNextPage:', result.pageInfo.hasNextPage); } ``` ```bash npx tsx quickstart.ts ``` Expected output: ```bash Acme Dental Midtown -- New York NY Acme Dental Brooklyn -- Brooklyn NY Acme Dental Jersey City -- Jersey City NJ hasNextPage: false ``` `fetchAllLocations` returns an object with `locations` and `pageInfo` by default. Pass `fetchAll: true` and the SDK follows cursor pages automatically, returning a flat `Location[]` instead. ```typescript const all = await client.fetchAllLocations({ fetchAll: true, pageSize: 100 }); if (Array.isArray(all)) { console.log('Total: ' + all.length + ' locations'); } ``` ## 3. Create a location The API requires `name`, `city`, `countryIso`, `subCategoryId`, and a `description` of at least 200 characters. Get valid `subCategoryId` values from `client.fetchSubcategories()` and use each entry's `databaseId`. ```typescript const created = await client.createLocation({ name: 'Acme Dental', storeId: 'ACME-NYC-001', street: '123 Jump Street', city: 'New York', stateIso: 'NY', postalCode: '10013', countryIso: 'US', phone: '6443859313', subCategoryId: 1432, description: 'Acme Dental is a family-owned dental practice serving downtown New York since 2009. ' + 'Our team provides preventive checkups, cosmetic dentistry, orthodontics, and emergency care, ' + 'with same-day appointments, transparent pricing, and a patient-first approach on every visit.', }); console.log(created); ``` If the payload is invalid, the SDK throws a `ValidationError` carrying the platform error entries, even when the API responds with HTTP 200. There is no `success` flag to check. ## 4. Respond to a review Fetch reviews for a location, then reply by interaction ID. Numeric location IDs are accepted everywhere: the SDK base64-encodes them automatically. ```typescript const reviews = await client.fetchInteractions(16808, { startDate: '2026-01-01', endDate: '2026-06-30', first: 10, }); if (!Array.isArray(reviews)) { const unanswered = reviews.interactions.find( (review) => (review.responses ?? []).length === 0, ); if (unanswered) { console.log(unanswered.rating, unanswered.content?.slice(0, 80)); await client.respondToReview( unanswered.id, 'Thank you for the kind words! We look forward to seeing you at your next visit.', ); } } ``` ## 5. Handle errors ```typescript import { ListingsAPI, APIError, AuthenticationError } from 'listingsapi-js'; const client = new ListingsAPI({ apiKey: 'bad-key' }); try { await client.fetchAllLocations(); } catch (error) { if (error instanceof AuthenticationError) { console.log('Invalid API key. Check LISTINGSAPI_KEY.'); } else if (error instanceof APIError) { console.log('Status:', error.statusCode); // can be 200 for payload-level errors console.log('Code:', error.code); // first platform code, e.g. 'SY10005' console.log('Errors:', error.errors); // [{ code, message, context }, ...] } } ``` The SDK throws typed errors for non-2xx responses, for error payloads the platform returns with HTTP 200, and for mutations reporting `success: false`. It also retries 429 and 5xx responses automatically. See [Error handling](/sdks/node/error-handling) for retry patterns and status-code guidance. ## Next steps - [Configuration](/sdks/node/configuration): base URL overrides and TypeScript project setup - [Use cases](/sdks/node/use-cases): ten end-to-end recipes - [Locations](/sdks/node/resources/locations): create, update, search, archive ### Configuration Source: https://listingsapi.com/sdks/node/configuration ## Constructor options `ListingsAPI` accepts a single, fully optional options object: | Option | Type | Required | Default | Description | |---|---|---|---|---| | `apiKey` | `string` | No | `process.env.LISTINGSAPI_KEY` | Your listingsAPI key. When omitted, the client reads the `LISTINGSAPI_KEY` environment variable and throws `AuthenticationError` if neither is set. | | `baseUrl` | `string` | No | `https://listingsapi.com` | Override the API host, useful for proxies or staging environments. Trailing slashes are stripped automatically. | | `timeout` | `number` | No | `240000` | Per-request timeout in milliseconds, enforced with `AbortSignal.timeout`. | | `maxRetries` | `number` | No | `2` | Automatic retries for rate limits, server errors, and network failures. Set to `0` to disable. | ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // reads LISTINGSAPI_KEY from env // Every default spelled out: const custom = new ListingsAPI({ apiKey: process.env.LISTINGSAPI_KEY, baseUrl: 'https://listingsapi.com', timeout: 240000, // 4 minutes per attempt maxRetries: 2, // up to 3 attempts total }); ``` ## API key resolution Never hardcode the key in source. The zero-argument constructor reads `LISTINGSAPI_KEY` from the environment: ```bash export LISTINGSAPI_KEY="your-api-key" ``` ```typescript const client = new ListingsAPI(); ``` Or pass a key you fetched from a secrets manager: ```typescript import { getSecret } from './secrets.js'; const apiKey = await getSecret('LISTINGSAPI_KEY'); const client = new ListingsAPI({ apiKey }); ``` If no key is found in either place, the constructor throws `AuthenticationError` immediately, before any request is made. ## Base URL override Point the client at an internal proxy or a staging environment: ```typescript const client = new ListingsAPI({ baseUrl: 'https://listings-proxy.internal.example.com', }); ``` Trailing slashes are stripped automatically, so `'https://listingsapi.com/'` and `'https://listingsapi.com'` behave the same. ## Timeouts Every attempt is wrapped in `AbortSignal.timeout(timeout)` internally (Node 18+), so you do not need your own `AbortController` around SDK calls. The default is 240000 ms (4 minutes) per attempt. A timed-out attempt counts as a connection failure: the client retries it while attempts remain, and the final failure throws `APIConnectionError`. Lower the timeout for latency-sensitive paths: ```typescript const client = new ListingsAPI({ timeout: 15000 }); // 15 s per attempt ``` ## Automatic retries The client retries responses with status `429`, `500`, `502`, `503`, and `504`, plus network-level failures (DNS errors, refused connections, timeouts). With the default `maxRetries: 2`, each call makes up to 3 attempts. The delay before each retry: 1. If the response carries a `Retry-After` header with a positive number of seconds, the client waits exactly that long. 2. Otherwise it backs off exponentially: 500 ms before the first retry, doubling for each retry after that (1 s, 2 s, and so on). Network failures are retried immediately, without a delay. Once attempts run out, the last response becomes a typed error (`RateLimitError`, `InternalServerError`, and friends; see [Error handling](/sdks/node/error-handling)), and an exhausted network failure throws `APIConnectionError`. ```typescript // Fail fast and own the retry policy yourself: const client = new ListingsAPI({ maxRetries: 0 }); ``` ## Location IDs Anywhere the SDK takes a location ID, numeric IDs are encoded automatically to the API's base64 form (`Location:`), and already encoded IDs pass through unchanged. The helper is exported if you need the encoded form yourself: ```typescript import { encodeLocationId } from 'listingsapi-js'; encodeLocationId(16808); // 'TG9jYXRpb246MTY4MDg=' encodeLocationId('TG9jYXRpb246MTY4MDg='); // returned as-is ``` ## Header format Every request is authenticated with: ``` Authorization: API Content-Type: application/json ``` The SDK sets these headers automatically on every call. You do not need to configure them. ## TypeScript project setup The SDK is TypeScript-first with bundled types, so there is no separate `@types` package to install. Pin the version for reproducible builds: ```bash npm install listingsapi-js@0.3.0 ``` A minimal `tsconfig.json` for a Node 18+ project: ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "outDir": "dist" } } ``` For ESM projects with `"type": "module"` in `package.json`, use `"module": "Node16"` and `"moduleResolution": "Node16"`. For CJS projects, `"module": "CommonJS"` and `"moduleResolution": "Node"` work fine; the package ships both builds, and CommonJS code loads it with `const { ListingsAPI } = require('listingsapi-js')`. ## Using without TypeScript `listingsapi-js` compiles to plain JavaScript with zero runtime dependencies. You can import it from a `.js` or `.mjs` file without a build step: ```javascript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const page = await client.fetchAllLocations({ first: 5 }); for (const location of page.locations) { console.log(location.name, location.city); // Acme Dental Midtown } ``` ## Next steps - [Quickstart](/sdks/node/quickstart), your first working script - [Use cases](/sdks/node/use-cases), end-to-end recipes - [Error handling](/sdks/node/error-handling), the error hierarchy and retry patterns ### Error Handling Source: https://listingsapi.com/sdks/node/error-handling Every error the SDK throws derives from `ListingsAPIError`. API failures throw `APIError` or one of its subclasses, whether the platform reports them as a non-2xx HTTP status or inside an HTTP 200 body. Network failures (DNS errors, refused connections, timeouts) throw `APIConnectionError`. You never inspect `success` flags or raw payloads: a resolved promise means the call succeeded. ## Error hierarchy ``` ListingsAPIError base class for everything the SDK throws ├── APIError any failure reported by the API │ ├── AuthenticationError 401, or invalid-token error payloads │ ├── PermissionDeniedError 403, or permission error payloads │ ├── NotFoundError 404, resource not found │ ├── ValidationError 400/422, mutations reporting success=false │ │ or errors[], and client-side payload checks │ ├── RateLimitError 429, carries retryAfter │ └── InternalServerError 5xx, safe to retry └── APIConnectionError network failure, nothing reached the API ``` All classes are exported from the package root: ```typescript import { ListingsAPIError, APIError, AuthenticationError, PermissionDeniedError, NotFoundError, ValidationError, RateLimitError, InternalServerError, APIConnectionError, } from 'listingsapi-js'; ``` ## APIError properties | Property | Type | Description | |---|---|---| | `statusCode` | `number \| null` | HTTP status of the failed response: `401`, `429`, `500`, or `200` when the error arrived inside a 200 body. `null` for client-side validation failures. | | `code` | `string \| null` | First platform error code, e.g. `SY10005`, when the API sent one. | | `errors` | `ApiErrorEntry[]` | Every error entry, normalized to `{ code, message, context }`. | | `responseBody` | `string \| null` | Raw response body as a string. | | `retryAfter` | `number \| null` | `RateLimitError` only. Seconds to wait before retrying, parsed from the `Retry-After` header, or `null` when the header was absent. | | `message` | `string` | Human-readable summary with the error entries (or raw body) appended. | | `name` | `string` | The concrete class name, e.g. `"RateLimitError"`. | ```typescript import { ListingsAPI, APIError, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI(); try { await client.fetchAllLocations({ first: 10 }); } catch (error) { if (error instanceof APIError) { console.error(error.statusCode); // 401, 429, 500, or 200 for payload errors console.error(error.code); // first platform code, e.g. 'SY10005' console.error(error.errors); // [{ code, message, context }, ...] console.error(error.responseBody); // raw body text } else if (error instanceof APIConnectionError) { console.error('Nothing reached the API:', error.message); } } ``` ## Catching errors by type ### Authentication failures ```typescript import { AuthenticationError } from 'listingsapi-js'; try { await client.fetchAllLocations({ first: 1 }); } catch (error) { if (error instanceof AuthenticationError) { console.error('Invalid API key. Rotate it under API Keys in the dashboard.'); } } ``` ### Missing resources ```typescript import { NotFoundError } from 'listingsapi-js'; try { await client.fetchPremiumListings(99999999); } catch (error) { if (error instanceof NotFoundError) { console.error('Location not found.'); } } ``` ### Rate limits `RateLimitError.retryAfter` holds the server's `Retry-After` value in seconds, or `null` when the header was absent: ```typescript import { RateLimitError } from 'listingsapi-js'; try { await client.fetchAllLocations({ first: 100 }); } catch (error) { if (error instanceof RateLimitError) { const waitSeconds = error.retryAfter ?? 5; console.error('Rate limited, retry in ' + waitSeconds + 's'); } } ``` ### Validation failures Server-side validation failures carry the platform's error entries: ```typescript import { ValidationError } from 'listingsapi-js'; try { await client.updateLocation({ id: 16808, phone: 'not-a-phone-number' }); } catch (error) { if (error instanceof ValidationError) { for (const entry of error.errors) { console.error((entry.code ?? 'unknown') + ': ' + entry.message); } } } ``` ## Errors inside HTTP 200 responses The platform reports some failures with HTTP 200 and an error payload in the body. The SDK inspects every response and throws for these too, so a 200 status never means "check the body yourself": | Payload signal | Thrown as | |---|---| | Top-level `errors[]` with code `SY90005` or `SY90001` | `AuthenticationError` | | Top-level `errors[]` with code `SY90003` | `PermissionDeniedError` | | Top-level `errors[]` with code `RATE_LIMITED` | `RateLimitError` | | Any other top-level `errors[]` | `APIError` | | Mutation result with `errors[]` or `success: false` | `ValidationError` | For mutations (`POST` and `DELETE` helpers), the SDK checks every result object in the response envelope; if any reports `success: false` or carries `errors[]`, it throws `ValidationError` with `statusCode` set to `200`. ```typescript import { ValidationError } from 'listingsapi-js'; try { const result = await client.archiveLocations([16808]); console.log('Archived:', result); } catch (error) { if (error instanceof ValidationError) { // The API answered 200 but rejected the mutation console.error(error.statusCode); // 200 console.error(error.message); } } ``` ## Client-side validation in the post helpers The post helpers (`createAnnouncement`, `createEvent`, `createOffer`, and `bulkPublish`) validate their payload locally and throw `ValidationError` before any network call: - `name` must be non-blank and `locationIds` must not be empty. - `sites` must be a non-empty subset of `GOOGLE` and `FACEBOOK`. - `message` is required; when it is a per-site map, every targeted site needs a non-blank entry. - `ctaType` and `ctaUrl` must be provided together, and `ctaType` must be one of `BOOK`, `ORDER`, `SHOP`, `LEARN_MORE`, `SIGN_UP`, `GET_OFFER`. - `createEvent` and `createOffer` additionally require `title`. - `postType` (on `bulkPublish`) must be one of `ANNOUNCEMENT`, `EVENT`, `OFFER`, `COVID19`, `PRODUCT`. These local errors have `statusCode: null`, which is how you tell them apart from a server-side rejection. The low-level `createPost(body)` method sends its raw body without these checks. ```typescript import { ListingsAPI, ValidationError } from 'listingsapi-js'; const client = new ListingsAPI(); try { await client.createAnnouncement({ name: 'Grand opening', locationIds: [16808], message: 'Acme Dental is now open in Midtown!', ctaType: 'BOOK', // ctaUrl missing: throws before any request is sent }); } catch (error) { if (error instanceof ValidationError && error.statusCode === null) { console.error(error.message); // Invalid post: ctaType and ctaUrl must be provided together } } ``` ## Built-in retries The client itself retries `429` and 5xx responses (honoring the `Retry-After` header) and network failures, twice by default. Errors reach your code only after those attempts are exhausted. See [Configuration](/sdks/node/configuration) for the `maxRetries` and `timeout` options. ## A retryAfter-aware backoff recipe For long-running jobs that should outlast the client's built-in retries, wrap calls in a backoff loop that prefers the server's `retryAfter` hint and falls back to exponential delays: ```typescript import { ListingsAPI, RateLimitError, InternalServerError, APIConnectionError, } from 'listingsapi-js'; const client = new ListingsAPI(); async function withBackoff( fn: () => Promise, maxAttempts = 5, ): Promise { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { const retryable = err instanceof RateLimitError || err instanceof InternalServerError || err instanceof APIConnectionError; if (!retryable || attempt === maxAttempts) throw err; let delayMs = 500 * 2 ** (attempt - 1); if (err instanceof RateLimitError && err.retryAfter !== null) { delayMs = err.retryAfter * 1000; } await new Promise((resolve) => setTimeout(resolve, delayMs)); } } throw new Error('unreachable'); } const page = await withBackoff(() => client.fetchAllLocations({ first: 25 })); ``` `NotFoundError`, `ValidationError`, `AuthenticationError`, and `PermissionDeniedError` are deliberately not retried: repeating those requests cannot succeed without changing the input or the key. ## Network failures and timeouts Anything that prevents a response from arriving (DNS failure, refused connection, or the per-request timeout firing) throws `APIConnectionError`. The SDK never leaks the native `TypeError` or `AbortError` from `fetch`. Timeouts are enforced internally with `AbortSignal.timeout` on every attempt, 240000 ms by default, configurable via the `timeout` constructor option. A timed-out attempt is retried like any other connection failure; only the final failure surfaces: ```typescript import { ListingsAPI, APIConnectionError } from 'listingsapi-js'; const client = new ListingsAPI({ timeout: 15000 }); try { await client.fetchAllLocations({ first: 10 }); } catch (error) { if (error instanceof APIConnectionError) { console.error('Network failure or timeout:', error.message); } } ``` ## Related resources - [Configuration](/sdks/node/configuration), the `timeout` and `maxRetries` options - [Quickstart](/sdks/node/quickstart), minimal error handling example - [Use cases](/sdks/node/use-cases), real-world error handling in complete scripts ### Use Cases Source: https://listingsapi.com/sdks/node/use-cases These recipes are cross-resource workflows, the kind of scripts you reach for when a single method page isn't enough. Each one is a complete, runnable TypeScript program that stitches together two or more resources into something you'd actually deploy. Install a pinned SDK (`npm install listingsapi-js@0.3.0`), export `LISTINGSAPI_KEY` with a key from your [dashboard](/dashboard), and you can run them as-is with `npx tsx .ts`. If you are new to the SDK, read the [Node SDK](/sdks/node) overview first. It covers client construction, auth, errors, and pagination, which every recipe below assumes. ## 1. Quick start: fetch and print locations The minimum viable integration: authenticate, pull one page of locations, and print their names. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // reads LISTINGSAPI_KEY from env const page = await client.fetchAllLocations({ first: 5 }); if (!Array.isArray(page)) { for (const loc of page.locations) { console.log(loc.name + ' - ' + loc.city); } } ``` Without `fetchAll`, `fetchAllLocations` returns an object with `locations`, `pageInfo`, and the `raw` payload. Pass `pageInfo.endCursor` as `after` to get the next page. ## 2. Bulk export to JSON Auto-paginate every location in the account and write the results to disk. ```typescript import { writeFileSync } from 'fs'; const client = new ListingsAPI(); const locations = (await client.fetchAllLocations({ fetchAll: true, pageSize: 100, })) as Location[]; writeFileSync('locations.json', JSON.stringify(locations, null, 2)); console.log('Exported ' + locations.length + ' locations to locations.json'); ``` `fetchAll: true` follows cursor pages automatically and returns a flat `Location[]`. For 10 000+ locations, keep `pageSize` at `100` (the default) so individual requests stay fast. ## 3. Onboard a new location Look up the right category ID with `fetchSubcategories`, then create the location. The `databaseId` of a subcategory is what `createLocation` expects as `subCategoryId`. The API also requires a description of 200+ characters. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const categories = await client.fetchSubcategories(); const dentist = categories.find((c) => c.name === 'Dentist'); if (!dentist) throw new Error('Category not found'); const description = 'Acme Dental is a family-owned dental practice in downtown New York ' + 'offering preventive care, cosmetic dentistry, orthodontics, and ' + 'emergency appointments. Our team has served the neighborhood for over ' + 'twenty years with same-week scheduling and weekend hours.'; const created = await client.createLocation({ name: 'Acme Dental', storeId: 'ACME-NY-001', street: '123 Jump Street', city: 'New York', stateIso: 'NY', postalCode: '10013', countryIso: 'US', phone: '6443859313', subCategoryId: dentist.databaseId, description: description, }); console.log('Created location', created.location?.id); ``` A validation failure (missing field, short description, bad category) throws a `ValidationError` with the platform's error entries on `err.errors`, so there is no `success` flag to inspect. ## 4. Publish a post to Google and Facebook in bulk `bulkPublish` puts one post on both sites across many locations in a single call. It validates the payload client-side, expands the message per site, and encodes numeric location IDs for you. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const result = await client.bulkPublish({ name: 'Holiday hours', locationIds: [16808, 16809, 16810], message: 'Open until 10pm through the holidays!', mediaUrl: 'https://cdn.example.com/holiday.jpg', ctaType: 'LEARN_MORE', ctaUrl: 'https://example.com/holiday-hours', }); console.log('Campaign:', result.socialPost?.id, result.socialPost?.status); // Publishing is asynchronous; poll the bulk campaign for per-location status const bulk = await client.fetchBulkPost(result.socialPost.id); console.log(bulk); ``` `sites` defaults to `['GOOGLE', 'FACEBOOK']`. Pass a per-site map as `message` (`{ GOOGLE: '...', FACEBOOK: '...' }`) when the copy should differ. For single campaigns with typed options, use `createAnnouncement`, `createEvent`, or `createOffer` instead. ## 5. Review monitoring: flag low ratings Scan every location for 1- and 2-star reviews and log the ones that need attention. ```typescript import { ListingsAPI, APIError } from 'listingsapi-js'; const client = new ListingsAPI(); const locations = (await client.fetchAllLocations({ fetchAll: true })) as Location[]; for (const loc of locations) { let reviews: Interaction[]; try { reviews = (await client.fetchInteractions(loc.id, { fetchAll: true, startDate: '2026-01-01', ratingFilters: [1, 2], })) as Interaction[]; } catch (err) { if (err instanceof APIError) { console.error(loc.name + ': ' + err.statusCode); continue; } throw err; } for (const review of reviews) { const who = review.reviewerName ?? 'Anonymous'; console.log('NEEDS ATTENTION [' + loc.name + '] ' + who + ' (' + review.rating + ' stars)'); } } ``` ## 6. Respond to unanswered reviews Auto-reply to every review that does not yet have a response. Use a template or swap `buildResponse` for an LLM call. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const LOCATION_ID = 16808; function buildResponse(review: Interaction): string { const name = review.reviewerName ?? 'there'; if ((review.rating ?? 5) <= 2) { return 'Hi ' + name + ', we are sorry to hear about your experience. Our team will reach out shortly.'; } return 'Hi ' + name + ', thank you for the kind words. We look forward to seeing you again!'; } const reviews = (await client.fetchInteractions(LOCATION_ID, { fetchAll: true, startDate: '2026-01-01', })) as Interaction[]; let responded = 0; for (const review of reviews) { if (review.responses && review.responses.length > 0) continue; await client.respondToReview(review.id, buildResponse(review)); responded++; } console.log('Responded to ' + responded + ' reviews'); ``` ## 7. Analytics report: Google, Bing, Facebook, and review stats Pull profile analytics and review stats for a single location in parallel and print a one-page summary. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const LOCATION_ID = 16808; const range = { fromDate: '2026-01-01', toDate: '2026-06-30' }; const [google, bing, facebook, reviewOverview] = await Promise.all([ client.fetchGoogleAnalytics(LOCATION_ID, range), client.fetchBingAnalytics(LOCATION_ID, range), client.fetchFacebookAnalytics(LOCATION_ID, range), client.fetchReviewAnalyticsOverview(LOCATION_ID, { startDate: range.fromDate, endDate: range.toDate, }), ]); console.log('Google insights:', JSON.stringify(google)); console.log('Bing insights:', JSON.stringify(bing)); console.log('Facebook insights:', JSON.stringify(facebook)); console.log('Review overview:', JSON.stringify(reviewOverview)); ``` Note the parameter split: platform analytics take `fromDate`/`toDate` while review analytics take `startDate`/`endDate`. ## 8. Listings audit: find unsynced and duplicate profiles Sweep one location for premium listings that are out of sync and for duplicate profiles detected by the crawler, then mark the duplicates. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const LOCATION_ID = 16808; const [premium, voice, duplicates] = await Promise.all([ client.fetchPremiumListings(LOCATION_ID), client.fetchVoiceListings(LOCATION_ID), client.fetchDuplicateListings(LOCATION_ID), ]); for (const l of premium) { if (l.syncStatus !== 'SYNCED') { console.log('Not synced: ' + l.site + ' - ' + l.syncStatus); } } console.log('Voice listings: ' + voice.length); console.log('Duplicates detected: ' + duplicates.length); const ids = duplicates .map((d) => d.id) .filter((id): id is string => typeof id === 'string'); if (ids.length > 0) { await client.markListingsAsDuplicate(LOCATION_ID, ids); console.log('Marked ' + ids.length + ' listings as duplicate'); } ``` For an account-wide sweep, `fetchAllDuplicateListings({ page: 1 })` returns a rollup of duplicates across every location. ## 9. Connected accounts: the Google flow Connect a Google account once, match its listings to your locations, and confirm the suggested matches. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); // 1. Get an OAuth URL and send the account owner there const connect = await client.connectGoogleAccount( 'https://example.com/connected', 'https://example.com/connect-error', ); console.log('Send the owner to:', JSON.stringify(connect)); // 2. After the owner completes OAuth, list connected accounts const accounts = await client.fetchConnectedAccounts({ page: 1, perPage: 25 }); console.log(JSON.stringify(accounts)); // 3. Trigger matching between the account's listings and your locations const accountId = 'connected-account-uuid'; await client.triggerConnectedAccountMatches([accountId]); const suggestions = await client.fetchConnectionSuggestions(accountId, { page: 1, perPage: 25, }); console.log(JSON.stringify(suggestions)); // 4. Confirm the matches you agree with await client.confirmConnectedAccountMatches([ 'match-record-id-1', 'match-record-id-2', ]); console.log('Matches confirmed'); ``` To connect a single location instead, use `getOauthConnectUrl(locationId, 'GOOGLE', successUrl, errorUrl)`, and `connectListing(locationId, listingId, accountId)` to link one listing directly. ## 10. Search locations and update a field in bulk Search for locations matching a query, then update a field on each result, for example pushing a new phone number across a city. ```typescript import { ListingsAPI } from 'listingsapi-js'; const client = new ListingsAPI(); const matches = (await client.searchLocations('Brooklyn', { fetchAll: true })) as Location[]; let updated = 0; for (const loc of matches) { await client.updateLocation({ id: loc.id, phone: '7185550100', }); updated++; } console.log('Updated phone on ' + updated + ' Brooklyn locations'); ``` ## Where to go next - [Locations](/sdks/node/resources/locations): full method reference with parameter tables - [Reviews](/sdks/node/resources/reviews): analytics, settings, phrase analysis - [Listings](/sdks/node/resources/listings): premium, voice, duplicates - [Analytics](/sdks/node/resources/analytics): Google, Bing, Facebook insights - [Error handling](/sdks/node/error-handling): `APIError` subclasses and retry strategy # MCP server The listingsAPI MCP server is a hosted Streamable HTTP endpoint for LLM clients such as Claude Code, Cursor, and Windsurf. There is nothing to install — connect to `https://listingsapi.com/mcp` and authenticate with an `Authorization: API ` header. Claude Code CLI: ``` claude mcp add --transport http listingsapi https://listingsapi.com/mcp --header "Authorization: API your-key" ``` Cursor (`~/.cursor/mcp.json`): ```json { "mcpServers": { "listingsapi": { "url": "https://listingsapi.com/mcp", "headers": { "Authorization": "API your-key" } } } } ``` For clients that cannot send custom headers (e.g. Claude Desktop), bridge through `mcp-remote`: `npx -y mcp-remote https://listingsapi.com/mcp --header "Authorization: API your-key"`. Tools: - Control plane: whoami, list_api_keys, create_api_key, revoke_api_key, get_usage, recent_errors, get_subscription. - Data plane: search_docs, get_doc_page, list_endpoints, get_endpoint. --- See also: https://listingsapi.com/llms.txt · https://listingsapi.com/openapi/spec.yaml · https://listingsapi.com/openapi/spec.json · https://listingsapi.com/docs