---
title: "TypeScript and Python SDKs | Leadsets Docs"
description: "Install @leadsetsai/sdk from npm or leadsets from PyPI, create a client and call every Leadsets endpoint, with retries, idempotency keys and pagination."
canonical: https://www.leadsets.ai/docs/sdks
updated: 2026-11-11
---

More / SDKs

# SDKs.

Official TypeScript and Python clients for the REST API. Typed responses with the same field names as the API, and automatic retries on 429 and 5xx.

## Install

```ts
npm install @leadsetsai/sdk
```

```python
pip install leadsets
```

The TypeScript SDK has no runtime dependencies and runs on Node.js 18+, Bun, Deno and edge runtimes. The Python SDK has a sync `Leadsets` and an async `AsyncLeadsets` client.

## Create a client

Pass your API key, or leave it out and the client reads `LEADSETS_API_KEY` (or `LEADSETS_KEY`) from the environment. Never hard-code it. See [Authentication](https://www.leadsets.ai/docs/authentication).

```ts
import { Leadsets } from "@leadsetsai/sdk";

const ls = new Leadsets(process.env.LEADSETS_KEY);

const res = await ls.people.search({
 function: ["revenue"],
 seniority: ["vp", "head"],
 country: "US",
 limit: 10,
});
console.log(res.items[0].match_reasons, res.usage);
```

```python
import os
from leadsets import Leadsets

ls = Leadsets(os.environ["LEADSETS_KEY"])

res = ls.people.search(
   function=["revenue"],
   seniority=["vp", "head"],
   country="US",
   limit=10,
)
print(res.items[0].match_reasons, res.usage)
```

## Methods

One method per REST route. Arguments are the route's body parameters, with the same names in both languages.

| Method | REST route | MCP tool |
| --- | --- | --- |
| people.search | [POST /v1/people/search](https://www.leadsets.ai/docs/api/people-search). Also `searchAll` / `search_all` and `searchPages` / `search_pages`. | `search_people` |
| companies.search | [POST /v1/companies/search](https://www.leadsets.ai/docs/api/companies-search). Also `searchAll` / `search_all` and `searchPages` / `search_pages`. | `search_companies` |
| people.enrich | [POST /v1/people/enrich](https://www.leadsets.ai/docs/api/people-enrich) | `enrich_person` |
| companies.enrich | [POST /v1/companies/enrich](https://www.leadsets.ai/docs/api/companies-enrich) | `enrich_company` |
| emails.verify | [POST /v1/emails/verify](https://www.leadsets.ai/docs/api/emails-verify). Takes one email, a list, or `{ emails }`. | `verify_email` |
| scrape.company | [POST /v1/scrape/company](https://www.leadsets.ai/docs/api/scrape-company) | `scrape_company` |
| scrape.jobs | [POST /v1/scrape/jobs](https://www.leadsets.ai/docs/api/scrape-jobs) | `scrape_jobs` |
| jobs.get | [GET /v1/jobs/{id}](https://www.leadsets.ai/docs/api/jobs). Throws `NotFoundError` until async jobs ship. | Not available yet |
| usage.get | [GET /v1/usage](https://www.leadsets.ai/docs/api/usage) | `get_usage` |

## Errors and retries

Every API error is thrown as a typed error with the API's `status`, `type`, `message` and `request_id`. Branch on the class:

| Class | Status | type |
| --- | --- | --- |
| InvalidRequestError | 400 | `invalid_request` (`param` names the field) |
| AuthenticationError | 401 | `unauthorized` |
| FreeRecordsExhaustedError | 402 | `free_records_exhausted` |
| ScrapeUnitsExhaustedError | 402 | `scrape_units_exhausted` |
| PermissionDeniedError | 403 | `forbidden`, with the upgrade URL when a plan change would allow the call |
| NotFoundError | 404 | `not_found` |
| IdempotencyConflictError | 409 | `idempotency_conflict` |
| RateLimitError | 429 | `window_full`, `weekly_limit`, `too_many_concurrent` |
| InternalServerError | 5xx | `internal_error` |
| ServiceUnavailableError | 503 | `unavailable` |
| APIConnectionError | none | No answer from the API. |
| APITimeoutError | none | The request timed out. |

Both payment errors extend `PaymentRequiredError`, every HTTP error extends `APIError`, and everything extends `LeadsetsError`.

```ts
import { APIError, FreeRecordsExhaustedError, NotFoundError, RateLimitError } from "@leadsetsai/sdk";

try {
 await ls.people.enrich({ email: "maya@northwind.io" });
} catch (err) {
 if (err instanceof NotFoundError) {
   // nothing to enrich; nothing was counted
 } else if (err instanceof FreeRecordsExhaustedError) {
   askToUpgrade();
 } else if (err instanceof RateLimitError) {
   console.log(err.type, err.meter, err.resetsAt, err.retryAfter);
 } else if (err instanceof APIError) {
   console.log(err.status, err.type, err.message, err.requestId);
 } else {
   throw err;
 }
}
```

```python
from leadsets import APIError, FreeRecordsExhaustedError, NotFoundError, RateLimitError

try:
   ls.people.enrich(email="maya@northwind.io")
except NotFoundError:
   pass  # nothing to enrich; nothing was counted
except FreeRecordsExhaustedError:
   ask_to_upgrade()
except RateLimitError as e:
   print(e.type, e.meter, e.resets_at, e.retry_after)
except APIError as e:
   print(e.status_code, e.type, e.message, e.request_id)
```

**Retries.** The SDKs retry 429, 5xx, timeouts and network errors up to `maxRetries` / `max_retries` times (default 2). They wait for `Retry-After` when the API sends one, and otherwise back off exponentially with jitter. A `Retry-After` longer than `maxRetryDelay` / `max_retry_delay` (default 60 seconds), such as a full weekly window, is thrown at once so a run doesn't hang. 400, 401, 402, 403, 404 and 409 are never retried. Every error type is in [Errors and retries](https://www.leadsets.ai/docs/errors).

## Idempotency keys

The SDKs send an `Idempotency-Key` with every paid call (every POST) and reuse it across their 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. See [Idempotency](https://www.leadsets.ai/docs/idempotency).

```ts
await ls.people.enrich({ email: row.email }, { idempotencyKey: `crm-${row.id}` });
```

```python
ls.people.enrich(email=row["email"], idempotency_key=f"crm-{row['id']}")
```

## Pagination

`searchAll` / `search_all` follows `next_cursor` for you and yields one record at a time. Each record returned counts, so cap a run with `maxItems` / `max_items`; the last page asks only for what is left. `searchPages` / `search_pages` yields whole pages when you want to read `usage` as you go. See [Pagination](https://www.leadsets.ai/docs/pagination).

```ts
for await (const company of ls.companies.searchAll({ industry: "saas" }, { maxItems: 250 })) {
 save(company);
}
```

```python
for company in ls.companies.search_all(industry="saas", max_items=250):
   save(company)
```

## Without an SDK

Every SDK method is one HTTPS request with a JSON body, so any HTTP client works: send `Authorization: Bearer` and `Content-Type: application/json`. Each reference page has a cURL example, starting with [POST /v1/people/search](https://www.leadsets.ai/docs/api/people-search). For agents, use [MCP](https://www.leadsets.ai/docs/mcp) instead: no code at all.

[Previous
**← get_usage**](https://www.leadsets.ai/docs/api/usage) [Next
**Pagination →**](https://www.leadsets.ai/docs/pagination)

Was this page useful? [Tell us](mailto:support@leadsets.ai?subject=Docs%20feedback%3A%20%2Fdocs%2Fsdks)

---

Leadsets by Mobylogix LLC, 10160 hwy 242, Conroe, TX, 77385, US. Facts for AI assistants: https://www.leadsets.ai/llm · Site index: https://www.leadsets.ai/llms.txt · Every page: https://www.leadsets.ai/llms-full.txt
