listingsapi 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 listingsAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,577 @@
1
+ Metadata-Version: 2.4
2
+ Name: listingsapi
3
+ Version: 0.5.0
4
+ Summary: Python SDK for listingsAPI — listings, reviews, posts, and analytics for local marketing
5
+ Author-email: listingsAPI <support@listingsapi.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 listingsAPI
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://www.listingsapi.com
29
+ Project-URL: Documentation, https://docs.listingsapi.com/sdks/python
30
+ Project-URL: Source, https://github.com/synup/listingsapi-python-sdk
31
+ Project-URL: Issues, https://github.com/synup/listingsapi-python-sdk/issues
32
+ Keywords: listingsapi,local-seo,listings,reviews,gbp,api
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: requests>=2.28
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=7; extra == "dev"
39
+ Requires-Dist: requests-mock>=1.11; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # listingsAPI Python SDK
43
+
44
+ [![PyPI](https://img.shields.io/pypi/v/listingsapi)](https://pypi.org/project/listingsapi/)
45
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://pypi.org/project/listingsapi/)
46
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
47
+
48
+ The official Python SDK for [listingsAPI](https://www.listingsapi.com): business listings, reviews, posts, and analytics for local marketing, in one typed client.
49
+
50
+ ```python
51
+ import listingsapi
52
+
53
+ client = listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY from env
54
+
55
+ for location in client.locations.list(first=10):
56
+ print(location.name, location.city)
57
+ ```
58
+
59
+ Full API reference and guides: [docs.listingsapi.com](https://docs.listingsapi.com)
60
+
61
+ ## Contents
62
+
63
+ - [Installation](#installation)
64
+ - [Authentication](#authentication)
65
+ - [Quickstart](#quickstart)
66
+ - [Pagination](#pagination)
67
+ - [Locations](#locations)
68
+ - [Reviews](#reviews)
69
+ - [Posts](#posts)
70
+ - [Listings](#listings)
71
+ - [Analytics](#analytics)
72
+ - [Photos](#photos)
73
+ - [Connected accounts](#connected-accounts)
74
+ - [Supporting APIs](#supporting-apis)
75
+ - [Workflows: pre-built automations](#workflows-pre-built-automations)
76
+ - [Method reference](#method-reference)
77
+ - [Error handling](#error-handling)
78
+ - [Configuration](#configuration)
79
+ - [Examples and development](#examples-and-development)
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install listingsapi
85
+ ```
86
+
87
+ Requires Python 3.9+. The only runtime dependency is `requests`.
88
+
89
+ ## Authentication
90
+
91
+ Get an API key from your [listingsAPI dashboard](https://listingsapi.com/dashboard/api-keys) and export it:
92
+
93
+ ```bash
94
+ export LISTINGSAPI_KEY="your-api-key"
95
+ ```
96
+
97
+ ```python
98
+ import listingsapi
99
+
100
+ client = listingsapi.ListingsAPI() # reads LISTINGSAPI_KEY
101
+ client = listingsapi.ListingsAPI(api_key="your-key") # or pass it explicitly
102
+ ```
103
+
104
+ ## Quickstart
105
+
106
+ Auto-reply to positive reviews in ten lines:
107
+
108
+ ```python
109
+ import listingsapi
110
+
111
+ client = listingsapi.ListingsAPI()
112
+
113
+ results = client.workflows.auto_reply_to_reviews(
114
+ 16808,
115
+ template="Thanks for the {rating}-star review! We appreciate your support.",
116
+ min_rating=4,
117
+ )
118
+ print(f"Replied to {len(results)} reviews")
119
+ ```
120
+
121
+ Audit your listings health in three:
122
+
123
+ ```python
124
+ audit = client.workflows.listings_health_audit(16808)
125
+ print(f"Health score: {audit.health_score}%")
126
+ print(f"Synced: {audit.synced_count}, Issues: {audit.issue_count}")
127
+ ```
128
+
129
+ ## Pagination
130
+
131
+ List endpoints return a `SyncPage`. Iterate it directly, page manually, or let it walk every page for you:
132
+
133
+ ```python
134
+ page = client.locations.list(first=50)
135
+ for loc in page:
136
+ print(loc.name)
137
+
138
+ if page.has_more: # manual paging
139
+ next_page = page.next_page()
140
+
141
+ for loc in page.auto_paging_iter(): # every record, all pages
142
+ print(loc.name)
143
+ ```
144
+
145
+ Responses are `APIObject`s: dot access (`loc.name`), dict access (`loc["stateIso"]`), and `loc.to_dict()` all work.
146
+
147
+ ## Locations
148
+
149
+ 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:
150
+
151
+ ```python
152
+ result = client.locations.add(
153
+ name="Acme Dental",
154
+ description=(
155
+ "Acme Dental is a family-owned dental practice in downtown New York "
156
+ "offering preventive care, cosmetic dentistry, orthodontics, and "
157
+ "emergency appointments. Our board-certified team combines modern "
158
+ "equipment with a gentle, patient-first approach."
159
+ ),
160
+ sub_category_id=1432,
161
+ country_iso="US",
162
+ city="New York",
163
+ street="123 Jump Street",
164
+ postal_code="10013",
165
+ phone="6443859313",
166
+ )
167
+ print(result.location.id)
168
+ ```
169
+
170
+ 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()`.
171
+
172
+ Find and fetch:
173
+
174
+ ```python
175
+ location = client.locations.retrieve(16808)
176
+ page = client.locations.search("cafe", first=20)
177
+
178
+ locations = client.locations.list_by_ids([16808, 16749])
179
+ locations = client.locations.list_by_store_codes(["STORE01", "STORE02"])
180
+ ```
181
+
182
+ Update and lifecycle:
183
+
184
+ ```python
185
+ client.locations.update({"id": 16808, "phone": "5559876543"})
186
+ client.locations.archive([16808])
187
+ client.locations.cancel_archive([16808], "manual", "ops@example.com")
188
+ ```
189
+
190
+ `locations.create(input_dict)` is also available when you already have a raw camelCase payload.
191
+
192
+ ## Reviews
193
+
194
+ ```python
195
+ # List with filters, or walk everything
196
+ page = client.reviews.list(16808, first=20, rating_filters=[1, 2])
197
+ for review in client.reviews.list(16808, first=50).auto_paging_iter():
198
+ print(review.rating, review.content)
199
+
200
+ # Respond, edit, archive
201
+ client.reviews.respond(interaction_id="uuid-here", content="Thank you!")
202
+ client.reviews.edit_response(review_id="...", response_id="...", content="Updated reply")
203
+ client.reviews.archive_response(response_id="...")
204
+
205
+ # Analytics and phrases
206
+ overview = client.reviews.analytics.overview(16808, start_date="2024-01-01")
207
+ timeline = client.reviews.analytics.timeline(16808)
208
+ sites = client.reviews.analytics.sites_stats(16808)
209
+ phrases = client.reviews.phrases(["TG9jYXRpb246MTY4MDg="], start_date="2024-01-01")
210
+ ```
211
+
212
+ ## Posts
213
+
214
+ Publish announcements, events, and offers to Google and Facebook.
215
+
216
+ `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:
217
+
218
+ ```python
219
+ result = client.posts.bulk_publish(
220
+ name="Holiday hours",
221
+ location_ids=[16808, 16809, 16810],
222
+ message="Open until 10pm through the holidays!",
223
+ media_url="https://cdn.example.com/holiday.jpg",
224
+ cta_type="LEARN_MORE",
225
+ cta_url="https://example.com/holiday-hours",
226
+ )
227
+ print(result.socialPost.id, result.socialPost.status) # INPROGRESS, publishes async
228
+ ```
229
+
230
+ Pass a dict as `message` for per-site copy: `{"GOOGLE": "...", "FACEBOOK": "..."}`. `sites` defaults to both.
231
+
232
+ Typed creates for single campaigns:
233
+
234
+ ```python
235
+ # Announcement: plain message, optional CTA and image
236
+ result = client.posts.create_announcement(
237
+ name="Grand Opening",
238
+ location_ids=[16808],
239
+ message="We are now open!",
240
+ sites=["GOOGLE"],
241
+ cta_type="LEARN_MORE",
242
+ cta_url="https://example.com/opening",
243
+ )
244
+
245
+ # Event: title plus start/end window
246
+ result = client.posts.create_event(
247
+ name="Live music",
248
+ location_ids=[16808],
249
+ message="Join us Friday!",
250
+ title="Jazz Night",
251
+ start_day="2026-08-01",
252
+ end_day="2026-08-01",
253
+ start_time="7:00pm",
254
+ end_time="10:00pm",
255
+ )
256
+
257
+ # Offer: coupon, discount, terms
258
+ result = client.posts.create_offer(
259
+ name="Summer sale",
260
+ location_ids=[16808],
261
+ message="20% off all week!",
262
+ title="Summer Sale",
263
+ coupon_code="SUMMER20",
264
+ discount="20%",
265
+ redeem_url="https://example.com/sale",
266
+ start_day="2026-08-01",
267
+ end_day="2026-08-07",
268
+ )
269
+ ```
270
+
271
+ Read, monitor, and delete:
272
+
273
+ ```python
274
+ post = client.posts.retrieve("U29jaWFsUG9zdDo0NDEyMg==") # per-site publish status + analytics
275
+ page = client.posts.list_for_location(16808, page=1, per_page=10)
276
+ bulk = client.posts.bulk_retrieve("QnVsa1Bvc3Q6OTk=") # per-location publish status
277
+ campaigns = client.posts.bulk_list_for_location(16808)
278
+ client.posts.delete("U29jaWFsUG9zdDo0NDEyMg==")
279
+ ```
280
+
281
+ Publishing is asynchronous: creates return `status: INPROGRESS`; poll `retrieve()` until `SUCCESS` and check `publishDetails[].submissionError` for per-site failures.
282
+
283
+ ## Listings
284
+
285
+ ```python
286
+ premium = client.listings.premium(16808)
287
+ voice = client.listings.voice(16808)
288
+
289
+ # Duplicates
290
+ dupes = client.listings.duplicates(16808)
291
+ client.listings.mark_duplicate(16808, ["listing-item-id"])
292
+ client.listings.mark_not_duplicate(16808, ["listing-item-id"])
293
+
294
+ # Connect and disconnect
295
+ client.listings.connect(16808, "listing-id", "account-id")
296
+ client.listings.disconnect(16808, "GOOGLE")
297
+ ```
298
+
299
+ ## Analytics
300
+
301
+ ```python
302
+ google = client.analytics.google(16808, from_date="2024-01-01")
303
+ bing = client.analytics.bing(16808)
304
+ facebook = client.analytics.facebook(16808)
305
+ ```
306
+
307
+ ## Photos
308
+
309
+ ```python
310
+ photos = client.photos.list(16808)
311
+ client.photos.add(16808, [{"photo": "https://example.com/img.jpg", "type": "ADDITIONAL"}])
312
+ client.photos.remove(16808, ["photo-id"])
313
+ client.photos.star(16808, ["media-id"], starred=True)
314
+ status = client.photos.upload_status("request-id")
315
+ ```
316
+
317
+ ## Connected accounts
318
+
319
+ ```python
320
+ accounts = client.connected_accounts.list()
321
+ url = client.connected_accounts.connect_google("https://ok.com", "https://err.com")
322
+ client.connected_accounts.disconnect_google("account-id")
323
+
324
+ # OAuth for a single location
325
+ url = client.connected_accounts.oauth_url(16808, "GOOGLE", "https://ok.com", "https://err.com")
326
+ client.connected_accounts.oauth_disconnect(16808, "FACEBOOK")
327
+ ```
328
+
329
+ ## Supporting APIs
330
+
331
+ ```python
332
+ sites = client.plan_sites() # directories included in your plan
333
+ countries = client.countries() # supported countries and states
334
+ subcategories = client.subcategories() # business categories (IDs for locations.add)
335
+ subs = client.subscriptions() # active subscriptions
336
+ ```
337
+
338
+ ## Workflows: pre-built automations
339
+
340
+ High-level functions that combine multiple API calls into real product features.
341
+
342
+ ```python
343
+ # Auto-reply to positive reviews (dry_run previews without sending)
344
+ results = client.workflows.auto_reply_to_reviews(
345
+ 16808,
346
+ template="Thanks for the {rating}-star review!",
347
+ min_rating=4,
348
+ dry_run=True,
349
+ )
350
+
351
+ # Weekly reputation report
352
+ report = client.workflows.weekly_reputation_report(16808)
353
+ print(report.review_summary, report.analytics, report.listings_health)
354
+
355
+ # Listings health audit
356
+ audit = client.workflows.listings_health_audit(16808)
357
+ print(f"Score: {audit.health_score}%, Issues: {audit.issue_count}")
358
+ ```
359
+
360
+ ## Method reference
361
+
362
+ ### Supporting APIs (client-level)
363
+
364
+ | Method | Description |
365
+ |--------|-------------|
366
+ | `client.plan_sites()` | Directories included in your plan |
367
+ | `client.countries()` | Supported countries and states (ISO codes) |
368
+ | `client.subcategories()` | Business categories (IDs for `locations.add`) |
369
+ | `client.subscriptions()` | Active subscriptions for the account |
370
+
371
+ ### Locations
372
+
373
+ | Method | Description |
374
+ |--------|-------------|
375
+ | `locations.list(first, after, before, last)` | List locations with cursor pagination |
376
+ | `locations.retrieve(location_id)` | Fetch a single location by ID |
377
+ | `locations.list_by_ids(location_ids)` | Fetch specific locations by ID |
378
+ | `locations.list_by_store_codes(store_codes)` | Fetch locations by store code |
379
+ | `locations.search(query, fields, first, after)` | Search by name, address, or store ID |
380
+ | `locations.add(**fields)` | One-call create with keyword args and client-side validation |
381
+ | `locations.create(input_dict)` | Create from a raw camelCase payload |
382
+ | `locations.update(input_dict)` | Update fields on a location (pass `id` plus changes) |
383
+ | `locations.archive(location_ids)` | Archive locations |
384
+ | `locations.cancel_archive(location_ids, selection_type, changed_by)` | Cancel a pending archive |
385
+
386
+ ### Posts
387
+
388
+ | Method | Description |
389
+ |--------|-------------|
390
+ | `posts.bulk_publish(**fields)` | One post to Google and Facebook across many locations |
391
+ | `posts.create_announcement(**fields)` | Create an ANNOUNCEMENT post |
392
+ | `posts.create_event(**fields)` | Create an EVENT post with title and date window |
393
+ | `posts.create_offer(**fields)` | Create an OFFER post with coupon and terms |
394
+ | `posts.create(body)` | Create from a raw flat payload |
395
+ | `posts.retrieve(post_id)` | Get a post with per-site publish status and analytics |
396
+ | `posts.delete(post_id)` | Delete a post from every published site |
397
+ | `posts.list_for_location(location_id, tag, page, per_page)` | List post campaigns for a location |
398
+ | `posts.bulk_retrieve(bulk_post_id)` | Get a bulk campaign with per-location status |
399
+ | `posts.bulk_list_for_location(location_id, tag, page, per_page)` | List bulk campaigns touching a location |
400
+
401
+ ### Reviews
402
+
403
+ | Method | Description |
404
+ |--------|-------------|
405
+ | `reviews.list(location_id, first, rating_filters, ...)` | List reviews/interactions with filters |
406
+ | `reviews.details(interaction_ids)` | Detailed review data for specific interactions |
407
+ | `reviews.respond(interaction_id, content)` | Reply to a review |
408
+ | `reviews.edit_response(review_id, response_id, content)` | Edit a reply |
409
+ | `reviews.archive_response(response_id)` | Archive a reply |
410
+ | `reviews.settings(location_id)` | Review notification settings |
411
+ | `reviews.edit_settings(location_id, site_urls)` | Update notification settings |
412
+ | `reviews.site_config()` | Review site configuration for the account |
413
+ | `reviews.phrases(location_ids, start_date, ...)` | Frequently used phrases across reviews |
414
+ | `reviews.analytics.overview(location_id, start_date, end_date)` | Review analytics overview |
415
+ | `reviews.analytics.timeline(location_id, ...)` | Review volume/rating over time |
416
+ | `reviews.analytics.sites_stats(location_id, ...)` | Per-site review stats |
417
+
418
+ ### Listings
419
+
420
+ | Method | Description |
421
+ |--------|-------------|
422
+ | `listings.premium(location_id)` | Premium directory listings |
423
+ | `listings.voice(location_id)` | Voice assistant listings |
424
+ | `listings.duplicates(location_id)` | Duplicate listings for a location |
425
+ | `listings.all_duplicates(tag, page)` | Duplicates across all locations |
426
+ | `listings.mark_duplicate(location_id, listing_item_ids)` | Mark listings as duplicates |
427
+ | `listings.mark_not_duplicate(location_id, listing_item_ids)` | Unmark duplicates |
428
+ | `listings.connect(location_id, listing_id, account_id)` | Connect a listing to a profile |
429
+ | `listings.disconnect(location_id, site)` | Disconnect a listing |
430
+ | `listings.create_gmb(location_id, ...)` | Create a Google Business Profile listing |
431
+
432
+ ### Analytics
433
+
434
+ | Method | Description |
435
+ |--------|-------------|
436
+ | `analytics.google(location_id, from_date, to_date)` | Google Business Profile insights |
437
+ | `analytics.bing(location_id, from_date, to_date)` | Bing Places insights |
438
+ | `analytics.facebook(location_id, from_date, to_date)` | Facebook page insights |
439
+
440
+ ### Photos
441
+
442
+ | Method | Description |
443
+ |--------|-------------|
444
+ | `photos.list(location_id)` | List photos for a location |
445
+ | `photos.add(location_id, photos)` | Upload photos by URL |
446
+ | `photos.remove(location_id, photo_ids)` | Delete photos |
447
+ | `photos.star(location_id, media_ids, starred)` | Star or unstar photos |
448
+ | `photos.upload_status(request_id)` | Status of a bulk photo upload |
449
+
450
+ ### Connected accounts
451
+
452
+ | Method | Description |
453
+ |--------|-------------|
454
+ | `connected_accounts.list(...)` | List connected Google/Facebook accounts |
455
+ | `connected_accounts.details(connected_account_id)` | Account details |
456
+ | `connected_accounts.folders(connected_account_id, folder_name)` | Folders/location groups in the account |
457
+ | `connected_accounts.suggestions(...)` | Suggested listing-to-location matches |
458
+ | `connected_accounts.listings(...)` | Listings available in a connected account |
459
+ | `connected_accounts.connect_google(success_url, error_url)` | Start Google OAuth connect |
460
+ | `connected_accounts.connect_facebook(success_url, error_url)` | Start Facebook OAuth connect |
461
+ | `connected_accounts.disconnect_google(connected_account_id)` | Disconnect a Google account |
462
+ | `connected_accounts.disconnect_facebook(connected_account_id)` | Disconnect a Facebook account |
463
+ | `connected_accounts.trigger_matches(connected_account_ids)` | Trigger profile-to-location matching |
464
+ | `connected_accounts.confirm_matches(match_record_ids)` | Confirm suggested matches |
465
+ | `connected_accounts.oauth_url(location_id, site, success_url, error_url)` | OAuth connect URL for one location |
466
+ | `connected_accounts.oauth_disconnect(location_id, site)` | Disconnect one location's profile |
467
+
468
+ ### Workflows
469
+
470
+ | Method | Description |
471
+ |--------|-------------|
472
+ | `workflows.auto_reply_to_reviews(location_id, template, min_rating, dry_run)` | Auto-reply to unanswered positive reviews |
473
+ | `workflows.weekly_reputation_report(location_id)` | Reviews + analytics + listings health in one report |
474
+ | `workflows.listings_health_audit(location_id)` | Sync status and duplicate audit with health score |
475
+
476
+ ## Error handling
477
+
478
+ 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.
479
+
480
+ ```
481
+ ListingsAPIError base class for everything below
482
+ ├── APIError any failure reported by the API
483
+ │ ├── AuthenticationError 401 or invalid-token error payloads
484
+ │ ├── PermissionDeniedError 403 or permission error payloads
485
+ │ ├── NotFoundError 404
486
+ │ ├── ValidationError 400/422, or mutations with success=false / errors[]
487
+ │ ├── RateLimitError 429, carries retry_after
488
+ │ └── InternalServerError 5xx, safe to retry
489
+ └── APIConnectionError network failure, nothing reached the API
490
+ ```
491
+
492
+ | Attribute | Meaning |
493
+ |---|---|
494
+ | `status_code` | HTTP status of the response (can be 200 for payload-level errors) |
495
+ | `code` | First platform error code, e.g. `"SY10005"` |
496
+ | `errors` | Every error entry, normalized to `{"code", "message", "context"}` |
497
+ | `response_body` | Raw response text |
498
+ | `retry_after` | Seconds to wait (RateLimitError only) |
499
+
500
+ ```python
501
+ import listingsapi
502
+
503
+ client = listingsapi.ListingsAPI()
504
+
505
+ try:
506
+ result = client.locations.add(
507
+ name="Acme Dental",
508
+ description=DESCRIPTION, # min 200 characters
509
+ sub_category_id=1432,
510
+ country_iso="US",
511
+ city="New York",
512
+ )
513
+ except listingsapi.ValidationError as e:
514
+ # Raised client-side for missing/short mandatory fields, or by the API
515
+ # (e.g. SY10005 description too short, SY10072 duplicate storeId)
516
+ for err in e.errors:
517
+ print(err["code"], err["message"])
518
+ except listingsapi.AuthenticationError:
519
+ print("Invalid API key. Check LISTINGSAPI_KEY.")
520
+ except listingsapi.RateLimitError as e:
521
+ print(f"Rate limited. Retry after {e.retry_after}s.")
522
+ except listingsapi.APIConnectionError:
523
+ print("Network error. Could not reach the API.")
524
+ ```
525
+
526
+ 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:
527
+
528
+ ```python
529
+ import time
530
+ import listingsapi
531
+
532
+ def with_backoff(fn, attempts=3):
533
+ for attempt in range(attempts):
534
+ try:
535
+ return fn()
536
+ except listingsapi.RateLimitError as e:
537
+ if attempt == attempts - 1:
538
+ raise
539
+ time.sleep(e.retry_after or 2 ** attempt)
540
+
541
+ page = with_backoff(lambda: client.locations.list(first=50))
542
+ ```
543
+
544
+ See [examples/12_error_handling.py](examples/12_error_handling.py) for a runnable walkthrough.
545
+
546
+ ## Configuration
547
+
548
+ ```python
549
+ client = listingsapi.ListingsAPI(
550
+ api_key="your-api-key",
551
+ base_url="https://listingsapi.com", # default
552
+ timeout=300.0,
553
+ max_retries=3,
554
+ )
555
+ ```
556
+
557
+ | Option | Default | Description |
558
+ |---|---|---|
559
+ | `api_key` | `LISTINGSAPI_KEY` env var | Your API key |
560
+ | `base_url` | `https://listingsapi.com` | API host |
561
+ | `timeout` | `240.0` | Request timeout in seconds |
562
+ | `max_retries` | `2` | Automatic retries on 429 and 5xx |
563
+
564
+ ## Examples and development
565
+
566
+ Runnable scripts for every area live in [examples/](examples/), from a 10-line quickstart to a full FastAPI backend.
567
+
568
+ ```bash
569
+ git clone https://github.com/synup/listingsapi-python-sdk.git
570
+ cd listingsapi-python-sdk
571
+ pip install -e ".[dev]"
572
+ pytest
573
+ ```
574
+
575
+ ## License
576
+
577
+ [MIT](LICENSE)