skrap-ai 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
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .env
14
+ .env.*
15
+ .DS_Store
16
+ .tools/
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package uses [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [0.1.0] - Unreleased
6
+
7
+ ### Added
8
+
9
+ - `Skrap` (sync) and `AsyncSkrap` (async) clients: API key from `api_key` or `SKRAP_API_KEY`, `base_url`, `timeout`, `max_retries`, `max_retry_delay`, default headers and a custom `httpx` client.
10
+ - Verify and find: `verify_email`, `find_email`, `get_find_refinement`, `wait_for_find_refinement`.
11
+ - Enrichment: `enrich.reverse_email`, `enrich.person`, `enrich.company`, `enrich.company_contacts`.
12
+ - Bulk jobs:
13
+ - `bulk.verify` and `bulk.find`
14
+ - `tasks.get`, `tasks.results`, `tasks.iter_results` and `tasks.wait`
15
+ - `tasks.download` and `tasks.download_url`
16
+ - `tasks.pause`, `tasks.resume` and `tasks.cancel`
17
+ - Lists: `lists.list`, `lists.create`, `lists.add_lead`, `lists.transfer_leads`, `lists.export`.
18
+ - Account: `account.get`.
19
+ - Webhooks: `webhooks.create`, `webhooks.list`, `webhooks.delete`, `webhooks.sample`, plus `verify_webhook_signature` and `construct_webhook_event`.
20
+ - Automatic retries with exponential backoff and jitter that honour `Retry-After`. Calls that could run twice are not retried after an unknown outcome.
21
+ - Typed errors (`SkrapError` and its subclasses) with `status`, `code`, `request_id`, `retryable`, `retry_after` and `details`.
22
+ - `response_meta(result)` for status, request id, headers and rate-limit info.
23
+ - `TypedDict` response models and `py.typed`.
24
+
25
+ [0.1.0]: https://github.com/skrapai/skrap-python/releases/tag/v0.1.0
skrap_ai-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Infralyon Technology (Skrap.ai)
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,372 @@
1
+ Metadata-Version: 2.5
2
+ Name: skrap-ai
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Skrap.ai API: email verification, email finder, enrichment, bulk jobs, lists and webhooks.
5
+ Project-URL: Homepage, https://skrap.ai
6
+ Project-URL: Documentation, https://skrap.ai/docs/api
7
+ Project-URL: Repository, https://github.com/skrapai/skrap-python
8
+ Project-URL: Issues, https://github.com/skrapai/skrap-python/issues
9
+ Project-URL: Changelog, https://github.com/skrapai/skrap-python/blob/main/CHANGELOG.md
10
+ Author-email: "Infralyon Technology (Skrap.ai)" <contact@skrap.ai>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,b2b,email finder,email verification,email verifier,enrichment,sdk,skrap
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.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Communications :: Email
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Requires-Dist: httpx<1,>=0.23
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Skrap.ai Python SDK
32
+
33
+ The official Python library for the [Skrap.ai API](https://skrap.ai/docs/api). Use it to verify email addresses, find work emails, enrich people and companies, run bulk jobs, manage lists and receive webhooks.
34
+
35
+ - Sync (`Skrap`) and async (`AsyncSkrap`) clients
36
+ - One dependency: [`httpx`](https://www.python-httpx.org/)
37
+ - Type hints throughout, with `TypedDict` models for every response (`py.typed`)
38
+ - Automatic retries with exponential backoff that honour `Retry-After`
39
+ - Typed errors that carry the HTTP status, error code and retry hints
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install skrap-ai
45
+ ```
46
+
47
+ Requires Python 3.9 or later. The package installs as `skrap-ai` and you import it as `skrap`. You need an API key: create one in the app under **Settings → API keys & MCP** (paid plans).
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ from skrap import Skrap
53
+
54
+ client = Skrap(api_key="sk_...") # or set SKRAP_API_KEY and call Skrap()
55
+
56
+ # Verify an address
57
+ result = client.verify_email("jane@example.com")
58
+ print(result["status"]) # valid | risky | invalid | unknown
59
+
60
+ # Find a work email
61
+ found = client.find_email(first_name="Jane", last_name="Doe", domain="example.com")
62
+ print(found["emails"][0]["email"] if found["emails"] else "not found")
63
+ ```
64
+
65
+ Responses are plain `dict`s that keep the API's own keys (`riskReasons`, `requestId`, ...). Their types are described in `skrap.types`, so editors and type checkers know the fields. Keep API keys on the server.
66
+
67
+ ## Async
68
+
69
+ ```python
70
+ import asyncio
71
+ from skrap import AsyncSkrap
72
+
73
+
74
+ async def main() -> None:
75
+ async with AsyncSkrap() as client:
76
+ result = await client.verify_email("jane@example.com")
77
+ print(result["status"])
78
+
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ `AsyncSkrap` has the same methods as `Skrap`; you `await` each one, and `iter_results` becomes an `async for`. To cancel a call, cancel its task, for example with `asyncio.wait_for`.
84
+
85
+ ## Configuration
86
+
87
+ ```python
88
+ client = Skrap(
89
+ api_key="sk_...", # default: SKRAP_API_KEY
90
+ base_url="https://api.skrap.ai", # default: SKRAP_BASE_URL, then https://api.skrap.ai
91
+ timeout=120, # seconds per attempt (verify and find can take about a minute)
92
+ max_retries=2, # retries after the first attempt; 0 turns retries off
93
+ max_retry_delay=60, # longest Retry-After (seconds) the client will wait for
94
+ default_headers={"x-my-trace": "abc"},
95
+ http_client=None, # your own httpx.Client (proxies, transports); not closed by close()
96
+ )
97
+ ```
98
+
99
+ You can use the client as a context manager (`with Skrap() as client:`) or call `client.close()` when done. Every method also takes per-request overrides:
100
+
101
+ ```python
102
+ client.verify_email(
103
+ "jane@example.com", request_options={"timeout": 30, "max_retries": 0, "headers": {"x-my-trace": "abc"}}
104
+ )
105
+ ```
106
+
107
+ ## Credits
108
+
109
+ Lookups that come back invalid, unknown or not found are free. If you repeat a verify or find that you already paid for, it is also free while inside your plan's repeat window, and the answer then carries `repeat: {"free": True, "firstChargedAt": ...}`. See [pricing](https://skrap.ai/pricing) and the [credit table](https://skrap.ai/docs/api).
110
+
111
+ ## Methods
112
+
113
+ ### Verify an email
114
+
115
+ Costs 1 credit when the result is valid or risky.
116
+
117
+ ```python
118
+ r = client.verify_email("jane@example.com")
119
+ r["status"] # 'valid' | 'risky' | 'invalid' | 'unknown'
120
+ r["retryable"] # True: unknown for a temporary reason, try again later
121
+ r["score"] # 95 valid, 10-70 risky, 0 invalid, None unknown
122
+ r["checks"] # syntax, mxFound, smtp, catchAll, disposable, roleAccount, freeProvider
123
+ r["riskReasons"] # risky only: catch_all, catch_all_unconfirmed, role_account, disposable
124
+ r["didYouMean"] # typo hint or None
125
+ ```
126
+
127
+ ### Find a work email
128
+
129
+ Costs 1 credit when a valid or risky email is found. Give a domain when you have one (best results). Otherwise give a company name.
130
+
131
+ ```python
132
+ r = client.find_email(
133
+ first_name="Jane",
134
+ last_name="Doe", # or full_name="Jane Doe"
135
+ domain="example.com", # or company="Example Inc" (+ optional location="Austin")
136
+ )
137
+ r["emails"] # [{"email": ..., "status": ...}]
138
+ r["domainStatus"]
139
+ r["requestId"]
140
+ ```
141
+
142
+ Some mail servers answer slowly. In that case the first answer has `pendingRefinement: True` and is not charged yet. Wait for the final answer like this:
143
+
144
+ ```python
145
+ if r.get("pendingRefinement"):
146
+ final = client.wait_for_find_refinement(r["requestId"], interval=2, timeout=120)
147
+ final["email"], final["status"]
148
+ # or poll yourself:
149
+ state = client.get_find_refinement(r["requestId"]) # {"ready": bool, "email"?, "status"?, ...}
150
+ ```
151
+
152
+ Sometimes a company name matches several companies. Then the answer has `needsCompanyConfirmation: True` and `suggestions` (name, domain, employeeCount). Resend with the domain you mean.
153
+
154
+ ### Enrichment
155
+
156
+ You are charged only when something is found. Each account can spend up to 5,000 credits a day on enrichment, and the limit resets at midnight UTC. Over the limit, you get a `RateLimitError` with code `ENRICH_DAILY_LIMIT`.
157
+
158
+ ```python
159
+ # Who is behind a work email. 2 credits when found (+1 with include_company when the profile is found).
160
+ who = client.enrich.reverse_email("jane.doe@example.com", include_company=True)
161
+ who["person"]["fullName"]
162
+ who["company"] # the person's current company: name, domain, linkedinUrl
163
+ who["companyProfile"] # full company profile, only with include_company=True
164
+
165
+ # A LinkedIn profile, plus the work email at the current employer when on record. 1 credit (+1 with email).
166
+ p = client.enrich.person("https://www.linkedin.com/in/jane-doe", include_email=True)
167
+ p["person"], p["workEmail"] # workEmail: {"address", "status"} or None
168
+
169
+ # A company from its domain or LinkedIn page. 1 credit (+1 with include_breakdown).
170
+ c = client.enrich.company(domain="example.com", include_breakdown=True)
171
+ c["company"]["industry"], c["headcountBreakdown"]["departments"]
172
+
173
+ # People at a company. 1 credit per person returned (emails included).
174
+ team = client.enrich.company_contacts(
175
+ domain="example.com",
176
+ seniority=["vp", "director"],
177
+ department=["sales"],
178
+ country=["US"],
179
+ limit=10,
180
+ page=1,
181
+ )
182
+ for person in team["people"]:
183
+ print(person["fullName"], person["title"], (person["email"] or {}).get("address"))
184
+ ```
185
+
186
+ Every enrichment answer carries `creditsCharged` and `requestId`.
187
+
188
+ ### Bulk jobs
189
+
190
+ A bulk job takes up to 25,000 rows and runs in the background. Each verify row is charged once, when it settles as valid or risky.
191
+
192
+ ```python
193
+ job = client.bulk.verify(["jane@example.com", "john@example.org"], list_name="Q2")
194
+ job2 = client.bulk.find(
195
+ [
196
+ {"first_name": "Jane", "last_name": "Doe", "domain": "example.com"},
197
+ {"first_name": "Li", "company": "Acme"},
198
+ ]
199
+ )
200
+
201
+ task = client.tasks.get(job["taskId"]) # status + progress
202
+ # poll until completed, failed, cancelled or paused
203
+ done = client.tasks.wait(
204
+ job["taskId"],
205
+ interval=5,
206
+ on_progress=lambda t: print(t["progress"]["percentage"]),
207
+ )
208
+
209
+ page = client.tasks.results(job["taskId"], offset=0, limit=50) # one page
210
+ for row in client.tasks.iter_results(job["taskId"]): # every row
211
+ print(row["email"], row["status"])
212
+
213
+ csv_text = client.tasks.download(job["taskId"])["csv"] # results CSV
214
+ link = client.tasks.download_url(job["taskId"])["downloadUrl"] # signed link to the stored file
215
+
216
+ client.tasks.pause(job["taskId"])
217
+ client.tasks.resume(job["taskId"])
218
+ client.tasks.cancel(job["taskId"]) # rows already processed stay charged; nothing else is
219
+ ```
220
+
221
+ A job also pauses itself when credits run out (`metadata.pauseReason == "insufficient_credits"`). Resume it after a top-up. While a job has no results yet, `tasks.download()` raises a `SkrapError` with code `TASK_PROCESSING` (status 202). Results are kept for 30 days. After that, results and downloads fail with status 410 and code `RESULTS_EXPIRED`. You can also subscribe to the `bulk_job.completed` webhook instead of polling.
222
+
223
+ ### Lists
224
+
225
+ ```python
226
+ lists = client.lists.list(kind="prospect")["lists"] # id, name, kind, leadCount
227
+ new = client.lists.create("Q2 outbound", kind="prospect")["list"] # "prospect" or "company"
228
+
229
+ client.lists.add_lead(
230
+ new["id"],
231
+ {
232
+ "id": "lead-123", # your id, unique in the list
233
+ "name": "Jane Doe",
234
+ "company": "Example Inc",
235
+ "email": "jane@example.com", # required on prospect (people) lists
236
+ "email_status": "valid", # snake_case or camelCase keys
237
+ "job_role": "CTO",
238
+ "domain": "example.com",
239
+ },
240
+ )
241
+
242
+ client.lists.transfer_leads(new["id"], target_list_id="other-list-id", lead_ids=["lead-123"], mode="move")
243
+
244
+ export = client.lists.export(ids=[new["id"]], kind="prospect") # ids defaults to "all"
245
+ export["csv"], export["suppressed"] # do-not-contact leads are left out unless include_suppressed=True
246
+ ```
247
+
248
+ Adding a lead whose id or email is already in the list raises `ConflictError` (409).
249
+
250
+ ### Account
251
+
252
+ ```python
253
+ me = client.account.get() # id, email, plan, credits, team, apiKey
254
+ ```
255
+
256
+ This call is free, so it works well as a connection test.
257
+
258
+ ### Webhooks
259
+
260
+ ```python
261
+ hook = client.webhooks.create("https://example.com/skrap-webhook", ["bulk_job.completed", "credits.low"])
262
+ hook["secret"] # signing secret, returned only once: store it
263
+
264
+ client.webhooks.list()["webhooks"]
265
+ client.webhooks.delete(hook["id"])
266
+ client.webhooks.sample("bulk_job.completed") # example payload (list of one)
267
+ ```
268
+
269
+ The events are `bulk_job.completed`, `bulk_job.failed`, `credits.low` and `list.lead_added`. Webhooks are available on paid plans. On a team, only owners and admins can subscribe or remove them.
270
+
271
+ #### Verify webhook signatures
272
+
273
+ Pass the raw request body as bytes, exactly as received:
274
+
275
+ ```python
276
+ from skrap import WebhookSignatureError, construct_webhook_event
277
+
278
+
279
+ @app.post("/skrap-webhook") # Flask
280
+ def skrap_webhook():
281
+ try:
282
+ event = construct_webhook_event(request.get_data(), request.headers.get("Skrap-Signature"), SECRET)
283
+ except WebhookSignatureError:
284
+ return "bad signature", 400
285
+ # de-duplicate with the Skrap-Event-Id header, then handle event["type"] / event["data"]
286
+ return "ok", 200
287
+ ```
288
+
289
+ `verify_webhook_signature(raw_body, header, secret)` returns a bool instead of raising. Both functions reject signatures older than 5 minutes by default; change that with `tolerance=`.
290
+
291
+ ## Errors
292
+
293
+ Every failure raises a `SkrapError`. Specific cases use these subclasses:
294
+
295
+ | Class | HTTP | Typical `code` |
296
+ | --- | --- | --- |
297
+ | `ValidationError` | 400 | `VALIDATION_ERROR`, `UNSUPPORTED_EMAIL` |
298
+ | `AuthenticationError` | 401 | `INVALID_API_KEY`, `API_KEY_EXPIRED` |
299
+ | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS`, `MEMBER_CREDIT_LIMIT`, `OVERAGE_LIMIT`, `OVERAGE_BLOCKED` |
300
+ | `PermissionDeniedError` | 403 | `PLAN_REQUIRED`, `IP_NOT_ALLOWED`, `FEATURE_NOT_AVAILABLE`, `ACCESS_DENIED`, `AUTHORIZATION_ERROR` |
301
+ | `NotFoundError` | 404 | `NOT_FOUND`, `TASK_NOT_FOUND` |
302
+ | `ConflictError` | 409 | (duplicate list or lead) |
303
+ | `RateLimitError` | 429 | `RATE_LIMITED`, `RATE_LIMIT_EXCEEDED`, `ENRICH_DAILY_LIMIT`, `FREE_ENRICH_LIMIT` |
304
+ | `ServerError` | 5xx | `INTERNAL_ERROR`, `ENRICH_UNAVAILABLE`, `CREDIT_CONTENTION` |
305
+ | `APIConnectionError` | none | `CONNECTION_ERROR` |
306
+ | `APITimeoutError` | none | `TIMEOUT` |
307
+
308
+ Other statuses, such as 410 `RESULTS_EXPIRED`, raise the base `SkrapError`.
309
+
310
+ ```python
311
+ from skrap import InsufficientCreditsError, RateLimitError, SkrapError
312
+
313
+ try:
314
+ client.verify_email("jane@example.com")
315
+ except InsufficientCreditsError as e:
316
+ print(e.details) # {"required": ..., "available": ..., "shortfall": ...}
317
+ except RateLimitError as e:
318
+ print(f"retry in {e.retry_after}s", e.body) # plan, planName, limitPerMinute, upgrade
319
+ except SkrapError as e:
320
+ print(e.status, e.code, e.message, e.retryable, e.request_id)
321
+ ```
322
+
323
+ Every `SkrapError` has these fields:
324
+
325
+ - `status`: the HTTP status, or `None` when no response arrived.
326
+ - `code`: the API's error code. Some endpoints, such as list errors, send no code, so it can be `None`.
327
+ - `message`
328
+ - `retryable`: the API's `retryable` flag when it sends one. Otherwise it is true for 429, 5xx, connection errors and timeouts.
329
+ - `retry_after`: in seconds.
330
+ - `details`, `body` and `headers`
331
+ - `request_id`: currently `None` for most errors, because error responses do not carry a request id.
332
+
333
+ ## Retries
334
+
335
+ The client retries automatically (2 times by default):
336
+
337
+ - **429, 500 and 503:** always retried. The API did no work and charged nothing.
338
+ - **502, 504, network errors and timeouts:** retried only for calls that are safe to repeat. These are verify, find (a repeat is free) and all reads. Bulk job creation, enrichment, list writes, webhook changes and task controls are not retried after an unknown outcome, so they never run twice.
339
+ - **Waiting:** the client waits the `Retry-After` header (or `retryAfterSec`) when the API sends one. Otherwise it backs off exponentially with jitter, from 0.5 s up to 8 s.
340
+ - **Long waits:** if the API asks to wait longer than `max_retry_delay` (60 s by default), the client raises right away with `retry_after` set. This happens with the daily enrichment limit, for example.
341
+ - **No retry:** a response with `retryable: false` is never retried.
342
+
343
+ Plan rate limits are per key: Free 30, Professional 120 and Enterprise 600 requests a minute.
344
+
345
+ ## Response details
346
+
347
+ ```python
348
+ from skrap import response_meta
349
+
350
+ r = client.verify_email("jane@example.com")
351
+ meta = response_meta(r)
352
+ meta.status # 200
353
+ meta.request_id # x-request-id header, else the body's requestId (find and enrichment), else None
354
+ meta.rate_limit # RateLimitInfo(limit, remaining, reset) from the RateLimit headers, or None
355
+ meta.headers
356
+ ```
357
+
358
+ ## Examples
359
+
360
+ - [`examples/verify_then_find.py`](examples/verify_then_find.py): verify, then find, with error handling
361
+ - [`examples/bulk_verify_async.py`](examples/bulk_verify_async.py): async bulk job, wait, results, CSV
362
+ - [`examples/webhook_server.py`](examples/webhook_server.py): receive webhooks and check their signatures
363
+
364
+ ## Documentation
365
+
366
+ - API reference: https://skrap.ai/docs/api
367
+ - Webhooks: https://skrap.ai/docs/webhooks
368
+ - OpenAPI: https://skrap.ai/openapi.json
369
+
370
+ ## License
371
+
372
+ MIT. See [LICENSE](LICENSE).