leadsets 0.1.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,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .DS_Store
14
+ .env
15
+ .env.*
16
+ !.env.example
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-11-11
8
+
9
+ ### Added
10
+
11
+ - First release of `leadsets`.
12
+ - Sync `Leadsets` and async `AsyncLeadsets` clients on httpx.
13
+ - `people.search`, `people.enrich`, `companies.search`, `companies.enrich`, `emails.verify`,
14
+ `scrape.company`, `scrape.jobs`, `jobs.get` and `usage.get`.
15
+ - Pagination with `search_all` (records) and `search_pages` (pages), sync and async, with
16
+ `max_items` and `max_pages`.
17
+ - Typed exceptions mapped from `error.type` and status, each with `request_id`.
18
+ - Automatic retries with exponential backoff and jitter on 429 (honouring `Retry-After`), 5xx,
19
+ timeouts and network errors.
20
+ - An auto-generated `Idempotency-Key` on every paid call, reused across retries.
21
+ - Typed parameters (`TypedDict`) and responses (dataclasses), with a `py.typed` marker.
22
+ - Parsed rate-limit headers on `last_response.rate_limit`.
23
+
24
+ [0.1.0]: https://github.com/leadsets/sdk-python/releases/tag/v0.1.0
leadsets-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mobylogix LLC
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,419 @@
1
+ Metadata-Version: 2.5
2
+ Name: leadsets
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Leadsets B2B data API: people and company search, enrichment, email verification and web scraping.
5
+ Project-URL: Homepage, https://www.leadsets.ai/
6
+ Project-URL: Documentation, https://www.leadsets.ai/docs
7
+ Project-URL: Repository, https://github.com/leadsets/sdk-python
8
+ Project-URL: Issues, https://github.com/leadsets/sdk-python/issues
9
+ Project-URL: Changelog, https://github.com/leadsets/sdk-python/blob/main/CHANGELOG.md
10
+ Author: Leadsets
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,b2b-data,company-search,email-verification,enrichment,lead-generation,leadsets,people-search,sales,sdk,web-scraping
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: httpx<1,>=0.27
29
+ Requires-Dist: typing-extensions>=4.7; python_version < '3.12'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Leadsets Python SDK
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/leadsets.svg)](https://pypi.org/project/leadsets/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/leadsets.svg)](https://pypi.org/project/leadsets/)
36
+ [![CI](https://github.com/leadsets/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/leadsets/sdk-python/actions/workflows/ci.yml)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
38
+
39
+ The official Python client for the [Leadsets](https://www.leadsets.ai/?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=intro) API.
40
+
41
+ Leadsets is B2B data for AI agents and the developers who build them. Search people and companies, enrich a person or a company, verify emails and scrape company websites and careers pages, over a REST API or an MCP server. Every record says why it matched, every field carries when it was last checked and a confidence score, and emails are only labelled verified when they are.
42
+
43
+ - Sync (`Leadsets`) and async (`AsyncLeadsets`) clients on [httpx](https://www.python-httpx.org/)
44
+ - Typed responses (dataclasses) and typed keyword arguments, with a `py.typed` marker
45
+ - Automatic retries with backoff on 429 (honouring `Retry-After`), 5xx and network errors
46
+ - An Idempotency-Key on every paid call, so a retry never counts records twice
47
+ - Cursor pagination with generators and async generators
48
+ - Python 3.10+
49
+
50
+ ## Contents
51
+
52
+ - [Install](#install)
53
+ - [Quickstart](#quickstart)
54
+ - [Authentication](#authentication)
55
+ - [Resources](#resources)
56
+ - [Pagination](#pagination)
57
+ - [Errors and retries](#errors-and-retries)
58
+ - [Limits and usage](#limits-and-usage)
59
+ - [Idempotency](#idempotency)
60
+ - [Async usage](#async-usage)
61
+ - [Configuration](#configuration)
62
+ - [Typing](#typing)
63
+ - [MCP](#mcp)
64
+ - [Support](#support)
65
+ - [License](#license)
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ pip install leadsets
71
+ # or
72
+ uv add leadsets
73
+ poetry add leadsets
74
+ ```
75
+
76
+ ## Quickstart
77
+
78
+ [Create a free account](https://www.leadsets.ai/signup?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=quickstart) and copy your API key. Keys start with `ls_live_`.
79
+
80
+ ```bash
81
+ export LEADSETS_API_KEY="ls_live_…"
82
+ ```
83
+
84
+ Ten revenue leaders at US SaaS companies with 20–200 people:
85
+
86
+ ```python
87
+ import os
88
+ from leadsets import Leadsets
89
+
90
+ ls = Leadsets(os.environ["LEADSETS_API_KEY"])
91
+
92
+ res = ls.people.search(
93
+ function=["revenue"],
94
+ seniority=["vp", "head"],
95
+ employees="20-200",
96
+ country="US",
97
+ industry="saas",
98
+ limit=10,
99
+ )
100
+
101
+ print(res.items[0].match_reasons, res.usage)
102
+ ```
103
+
104
+ Every response carries what the call counted and what is left (`usage`), plus a `request_id`. The [quickstart guide](https://www.leadsets.ai/docs/quickstart?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=quickstart) walks through the same call with cURL.
105
+
106
+ ## Authentication
107
+
108
+ The API uses bearer keys. Pass the key to the constructor, or leave it out and the SDK reads the `LEADSETS_API_KEY` environment variable (or `LEADSETS_KEY`).
109
+
110
+ ```python
111
+ ls = Leadsets("ls_live_…") # explicit
112
+ ls = Leadsets() # from LEADSETS_API_KEY / LEADSETS_KEY
113
+ ```
114
+
115
+ Keep the key out of source control. A free key is available at [leadsets.ai/signup](https://www.leadsets.ai/signup?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=authentication).
116
+
117
+ ## Resources
118
+
119
+ | Method | Endpoint | Uses |
120
+ | --- | --- | --- |
121
+ | `ls.people.search(**params)` | `POST /v1/people/search` | 1 search record per record returned |
122
+ | `ls.people.enrich(**params)` | `POST /v1/people/enrich` | 1 enrichment record when found (+1 for a personal email) |
123
+ | `ls.companies.search(**params)` | `POST /v1/companies/search` | 1 search record per record returned |
124
+ | `ls.companies.enrich(**params)` | `POST /v1/companies/enrich` | 1 enrichment record when found |
125
+ | `ls.emails.verify(email_or_emails)` | `POST /v1/emails/verify` | 1 enrichment record per email checked |
126
+ | `ls.scrape.company(**params)` | `POST /v1/scrape/company` | 1 scrape unit per page read |
127
+ | `ls.scrape.jobs(**params)` | `POST /v1/scrape/jobs` | 1 scrape unit per page read |
128
+ | `ls.jobs.get(job_id)` | `GET /v1/jobs/{id}` | nothing |
129
+ | `ls.usage.get()` | `GET /v1/usage` | nothing |
130
+
131
+ Every method also takes [request options](#per-request-options) as keyword arguments.
132
+
133
+ Responses are dataclasses: `res.items[0].email.status`. `obj.to_dict()` returns the raw JSON, and `obj["field"]` reads any field, including ones newer than your SDK version.
134
+
135
+ ### People search
136
+
137
+ Filters: `title`, `seniority` (`c_level`, `vp`, `head`, `director`, `manager`, `ic`), `function` (`revenue`, `marketing`, `engineering`, `product`, `ops`, `finance`, `hr`), `company_domain`, `employees` (`"50-200"`, `"1000+"`, `"<50"`), `industry` (slugs such as `"saas"`), `country` (ISO 3166-1 alpha-2), `has_verified_email`, `max_age_days`, `exclude_stale`, `limit` (1–100, default 25) and `cursor`. A string or a list works wherever a list is accepted.
138
+
139
+ ```python
140
+ res = ls.people.search(
141
+ title=["VP Sales", "Head of Sales"], # fuzzy: "VP Sales" also finds "Vice President, Sales"
142
+ country=["US", "CA"],
143
+ employees="50-200",
144
+ has_verified_email=True,
145
+ limit=25,
146
+ )
147
+
148
+ for p in res.items:
149
+ print(p.full_name, p.title, p.company.domain if p.company else None)
150
+ if p.email:
151
+ print(p.email.address, p.email.status, p.email.confidence, p.email.checked_at)
152
+ print(p.match_reasons) # ["title", "size", "country", "verified_email"]
153
+ print(p.sources["title"]) # FieldSource(seen_at=…, confidence=…)
154
+ ```
155
+
156
+ Each `Person` has `person_id`, names, `title`, `seniority` and `function` (with finer `seniority_detail` and `function_detail`), `linkedin_url`, `location`, `company`, `email`, `refreshed_at`, `stale` and `sources`.
157
+
158
+ ### People enrich
159
+
160
+ Look a person up by LinkedIn URL, by work email, or by name plus company domain.
161
+
162
+ ```python
163
+ res = ls.people.enrich(linkedin_url="https://www.linkedin.com/in/example")
164
+ ls.people.enrich(email="jane@example.com")
165
+ ls.people.enrich(full_name="Jane Doe", company_domain="example.com")
166
+ ls.people.enrich(first_name="Jane", last_name="Doe", company_domain="example.com", include_personal_email=True)
167
+
168
+ print(res.person.title, res.matched_by)
169
+ ```
170
+
171
+ No match raises `NotFoundError` and costs nothing.
172
+
173
+ ### Companies search
174
+
175
+ ```python
176
+ res = ls.companies.search(
177
+ industry="saas",
178
+ employees="50-200",
179
+ country="US",
180
+ technology=["hubspot", "salesforce"], # matches any
181
+ limit=50,
182
+ )
183
+ print([(c.name, c.domain, c.employees) for c in res.items])
184
+ ```
185
+
186
+ ### Companies enrich
187
+
188
+ ```python
189
+ company = ls.companies.enrich(domain="example.com").company # a URL works too
190
+ ls.companies.enrich(linkedin_url="https://www.linkedin.com/company/example")
191
+
192
+ print(company.industry, company.naics, company.employee_range, company.revenue_range, company.technologies)
193
+ ```
194
+
195
+ ### Email verification
196
+
197
+ One email or up to 100 per call. Each result has a `status` (`verified`, `catch_all`, `unknown`, `invalid`), a `confidence` and `checked_at`.
198
+
199
+ ```python
200
+ one = ls.emails.verify("jane@example.com")
201
+ many = ls.emails.verify(["jane@example.com", "sam@example.org"])
202
+ # or: ls.emails.verify(emails=[...])
203
+
204
+ print(many.summary) # verified, catch_all, unknown, invalid, not_checked
205
+ for r in many.results:
206
+ if r.status is None:
207
+ print(r.email, r.reason) # not checked, not counted
208
+ ```
209
+
210
+ ### Scrape a company website
211
+
212
+ A website as structured data: name, description, headings, key links (pricing, customers, careers, docs and more), social profiles and page text. `max_pages` (1–10, default 1) adds the about, pricing, customers, product and careers pages. Scraping is available on paid plans with scrape units.
213
+
214
+ ```python
215
+ site = ls.scrape.company(domain="example.com", max_pages=3)
216
+ print(site.name, site.key_links.get("pricing"), site.social.get("linkedin"))
217
+ print([p.url for p in site.pages], site.usage.scrape_units_left)
218
+ ```
219
+
220
+ ### Scrape open roles
221
+
222
+ Open roles from a company's careers page or job board: title, link, team, location and posted date when shown. `max_pages` defaults to 3.
223
+
224
+ ```python
225
+ res = ls.scrape.jobs(domain="example.com")
226
+ print(res.count, res.careers_url)
227
+ for job in res.jobs:
228
+ print(job.title, job.team, job.location, job.url)
229
+ ```
230
+
231
+ ### Async jobs
232
+
233
+ ```python
234
+ job = ls.jobs.get("job_123")
235
+ ```
236
+
237
+ Async jobs are not generally available yet. Until they are, `jobs.get` raises `NotFoundError`. Split large runs across windows instead.
238
+
239
+ ### Usage
240
+
241
+ What's left in every meter and when each window resets. Free to call, so call it before a big run.
242
+
243
+ ```python
244
+ from leadsets.types import WindowMeters
245
+
246
+ u = ls.usage.get()
247
+ print(u.plan)
248
+ if isinstance(u.search, WindowMeters):
249
+ used, limit = u.search.records_5h
250
+ print(f"{limit - used} search records left; resets at {u.search.resets_at}")
251
+ print(u.concurrent_searches, u.scrape_units)
252
+ ```
253
+
254
+ On the Free plan, `u.free` holds the one-time balances instead.
255
+
256
+ ## Pagination
257
+
258
+ Searches return `next_cursor`. `search_all` follows it for you and yields one record at a time:
259
+
260
+ ```python
261
+ for person in ls.people.search_all(title=["VP Sales"], country="US", limit=100):
262
+ print(person.full_name)
263
+ ```
264
+
265
+ Each record returned counts, so cap a run with `max_items`. The last page asks only for what's left, so you never pay for more than you take. `max_pages` caps the number of calls.
266
+
267
+ ```python
268
+ for company in ls.companies.search_all(industry="saas", max_items=250):
269
+ ...
270
+ ```
271
+
272
+ To read `usage` as you go, iterate pages instead:
273
+
274
+ ```python
275
+ for page in ls.people.search_pages(country="US", limit=100, max_pages=5):
276
+ print(len(page.items), page.usage.search_records_left_5h)
277
+ ```
278
+
279
+ Breaking out of a loop stops fetching. To page by hand, pass `cursor=res.next_cursor` to the next `search` call.
280
+
281
+ ## Errors and retries
282
+
283
+ Every API error has a status, a machine-readable `type`, a message and a `request_id`. The SDK raises a typed exception for each:
284
+
285
+ | Exception | Status | `type` |
286
+ | --- | --- | --- |
287
+ | `InvalidRequestError` | 400 | `invalid_request` (`param` names the field) |
288
+ | `AuthenticationError` | 401 | `unauthorized` |
289
+ | `FreeRecordsExhaustedError` | 402 | `free_records_exhausted` |
290
+ | `ScrapeUnitsExhaustedError` | 402 | `scrape_units_exhausted` |
291
+ | `PermissionDeniedError` | 403 | `forbidden` (`upgrade_url` when an upgrade would allow the call) |
292
+ | `NotFoundError` | 404 | `not_found` |
293
+ | `IdempotencyConflictError` | 409 | `idempotency_conflict` |
294
+ | `RateLimitError` | 429 | `window_full`, `weekly_limit`, `too_many_concurrent` |
295
+ | `InternalServerError` | 5xx | `internal_error` |
296
+ | `ServiceUnavailableError` | 503 | `unavailable` |
297
+ | `APIConnectionError` | none | the request got no answer |
298
+ | `APITimeoutError` | none | the request timed out |
299
+
300
+ `FreeRecordsExhaustedError` and `ScrapeUnitsExhaustedError` subclass `PaymentRequiredError`. Every HTTP error subclasses `APIError`, and everything subclasses `LeadsetsError`. `leadsets.ConnectionError` and `leadsets.TimeoutError` are aliases of the last two.
301
+
302
+ ```python
303
+ from leadsets import APIError, NotFoundError, RateLimitError
304
+
305
+ try:
306
+ ls.people.enrich(email="jane@example.com")
307
+ except NotFoundError:
308
+ pass # nothing to enrich; nothing was counted
309
+ except RateLimitError as e:
310
+ print(e.type, e.meter, e.resets_at, e.retry_after)
311
+ except APIError as e:
312
+ print(e.status_code, e.type, e.message, e.request_id)
313
+ ```
314
+
315
+ **Retries.** The SDK retries 429, 5xx, timeouts and network errors up to `max_retries` times (default 2). It waits for `Retry-After` when the API sends one, and otherwise backs off exponentially with jitter (about 0.5 s, 1 s, 2 s, up to 8 s). A `Retry-After` longer than `max_retry_delay` (default 60 s), such as a weekly limit, is raised at once so a run doesn't hang. 400, 401, 402, 403, 404 and 409 are never retried.
316
+
317
+ ## Limits and usage
318
+
319
+ - **Records count only when data comes back.** Empty results, misses and errors use no records. Every call counts one request toward its window, hits and misses alike.
320
+ - **Paid plans have rolling five-hour windows** for records and for requests, plus a **weekly** records window, kept separately for search and for enrichment. Scraping uses scrape units.
321
+ - **Free** has 1,000 enrichment records and 100 search records, one time, with no card needed. Paid plans start at $29/month. See [pricing](https://www.leadsets.ai/pricing?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=limits).
322
+
323
+ Every response has a `usage` object with what the call counted and what's left, and the SDK parses the rate-limit headers into `res.last_response.rate_limit`:
324
+
325
+ ```python
326
+ res = ls.people.search(country="US")
327
+ res.usage # search_records_counted, search_records_left_5h, …
328
+ res.last_response.rate_limit # records_remaining, requests_remaining, weekly_remaining, free_remaining, retry_after
329
+ res.last_response.request_id # also res.request_id
330
+ res.last_response.headers # the raw httpx.Headers
331
+ ```
332
+
333
+ Read more in the [docs](https://www.leadsets.ai/docs?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=limits).
334
+
335
+ ## Idempotency
336
+
337
+ The SDK sends an `Idempotency-Key` with every paid call (all POSTs) and reuses it across its own retries, so a retry never counts records twice. Pass your own key to make a call safe to repeat across processes, for example one key per CRM row:
338
+
339
+ ```python
340
+ ls.people.enrich(email=row["email"], idempotency_key=f"crm-{row['id']}")
341
+ ```
342
+
343
+ A repeat with the same key and body returns the first result (`res.last_response.idempotent_replayed` is `True`) and counts nothing. The same key with a different body raises `IdempotencyConflictError`. Keys last 24 hours. With `search_all`/`search_pages`, a key you pass is suffixed per page (`key`, `key-p2`, `key-p3` …).
344
+
345
+ ## Async usage
346
+
347
+ `AsyncLeadsets` has the same resources and methods, awaited:
348
+
349
+ ```python
350
+ import asyncio
351
+ from leadsets import AsyncLeadsets
352
+
353
+ async def main() -> None:
354
+ async with AsyncLeadsets() as ls:
355
+ sales, marketing = await asyncio.gather(
356
+ ls.people.search(function="revenue", seniority="vp", country="US", limit=10),
357
+ ls.people.search(function="marketing", seniority="head", country="US", limit=10),
358
+ )
359
+ async for company in ls.companies.search_all(industry="saas", max_items=50):
360
+ print(company.name)
361
+
362
+ asyncio.run(main())
363
+ ```
364
+
365
+ Use the clients as context managers (or call `close()` / `await aclose()`) to release connections. One client can be shared across threads or tasks.
366
+
367
+ ## Configuration
368
+
369
+ ```python
370
+ ls = Leadsets(
371
+ "ls_live_…", # default: LEADSETS_API_KEY or LEADSETS_KEY
372
+ base_url="https://api.leadsets.ai", # default; LEADSETS_BASE_URL also works
373
+ max_retries=2, # retries on 429, 5xx, timeouts and network errors
374
+ timeout=120.0, # seconds per attempt (float or httpx.Timeout)
375
+ max_retry_delay=60.0, # longest Retry-After to wait for, in seconds
376
+ default_headers={"X-My-App": "crm-sync"},
377
+ http_client=httpx.Client(proxy="http://proxy:8080"), # your own httpx client
378
+ )
379
+ ```
380
+
381
+ ### Per-request options
382
+
383
+ ```python
384
+ ls.people.search(
385
+ country="US",
386
+ idempotency_key="run-42",
387
+ max_retries=5,
388
+ timeout=30.0,
389
+ extra_headers={"X-Trace-Id": "abc"},
390
+ )
391
+ ```
392
+
393
+ ## Typing
394
+
395
+ The package ships a `py.typed` marker. Search and enrich parameters are `TypedDict`s (`PeopleSearchParams`, `CompaniesSearchParams`, …) used as keyword arguments, so mypy and Pyright check filter names and values such as `seniority="vp"`. Responses are dataclasses in `leadsets.types`: `Person`, `Company`, `Email`, `EmailVerification`, `ScrapeCompanyResponse`, `Job`, `UsageResponse`, `Usage` and more. Fields the API may leave empty are `Optional`.
396
+
397
+ ```python
398
+ from leadsets.types import PeopleSearchParams, Person
399
+
400
+ query: PeopleSearchParams = {"seniority": ["vp"], "function": ["marketing"]}
401
+ people: list[Person] = ls.people.search(**query).items
402
+ ```
403
+
404
+ ## MCP
405
+
406
+ Using an AI agent? Leadsets also runs a hosted MCP server at `https://mcp.leadsets.ai` with the same tools, so Claude, Cursor, ChatGPT and other MCP clients can call it with no code. See [Connect via MCP](https://www.leadsets.ai/docs/mcp?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=mcp).
407
+
408
+ ## Support
409
+
410
+ - [Docs](https://www.leadsets.ai/docs?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=support) and the [quickstart](https://www.leadsets.ai/docs/quickstart?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=support)
411
+ - [Support](https://www.leadsets.ai/support?utm_source=github&utm_medium=readme&utm_campaign=sdk-python&utm_content=support): include the `request_id` from the response or error and we can see exactly what happened
412
+ - Bugs and feature requests: [GitHub issues](https://github.com/leadsets/sdk-python/issues)
413
+ - Security: see [SECURITY.md](SECURITY.md)
414
+
415
+ Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
416
+
417
+ ## License
418
+
419
+ [MIT](LICENSE)