Error handling

Exception hierarchy, error attributes, HTTP 200 payload errors, client-side validation, and retry guidance for the listingsAPI Python SDK.

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:

AttributeTypeDescription
status_codeint | NoneHTTP status code (can be 200 for payload-level errors)
codestr | NoneFirst platform error code, e.g. SY10005
errorslist[dict]Normalized error entries, each {"code", "message", "context"}
response_bodystr | NoneRaw response text from the API
retry_afterfloat | NoneRateLimitError 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.

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
import time
 
import listingsapi
 
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
import listingsapi
 
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