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.
- skrap_ai-0.1.0/.gitignore +16 -0
- skrap_ai-0.1.0/CHANGELOG.md +25 -0
- skrap_ai-0.1.0/LICENSE +21 -0
- skrap_ai-0.1.0/PKG-INFO +372 -0
- skrap_ai-0.1.0/README.md +342 -0
- skrap_ai-0.1.0/examples/bulk_verify_async.py +45 -0
- skrap_ai-0.1.0/examples/verify_then_find.py +54 -0
- skrap_ai-0.1.0/examples/webhook_server.py +39 -0
- skrap_ai-0.1.0/pyproject.toml +81 -0
- skrap_ai-0.1.0/src/skrap/__init__.py +59 -0
- skrap_ai-0.1.0/src/skrap/_async_client.py +440 -0
- skrap_ai-0.1.0/src/skrap/_base.py +475 -0
- skrap_ai-0.1.0/src/skrap/_client.py +490 -0
- skrap_ai-0.1.0/src/skrap/_errors.py +129 -0
- skrap_ai-0.1.0/src/skrap/_version.py +1 -0
- skrap_ai-0.1.0/src/skrap/_webhooks.py +88 -0
- skrap_ai-0.1.0/src/skrap/py.typed +0 -0
- skrap_ai-0.1.0/src/skrap/types.py +503 -0
- skrap_ai-0.1.0/tests/__init__.py +0 -0
- skrap_ai-0.1.0/tests/conftest.py +72 -0
- skrap_ai-0.1.0/tests/test_async.py +186 -0
- skrap_ai-0.1.0/tests/test_client.py +113 -0
- skrap_ai-0.1.0/tests/test_errors.py +129 -0
- skrap_ai-0.1.0/tests/test_live_smoke.py +52 -0
- skrap_ai-0.1.0/tests/test_methods.py +356 -0
- skrap_ai-0.1.0/tests/test_polling.py +85 -0
- skrap_ai-0.1.0/tests/test_retries.py +194 -0
- skrap_ai-0.1.0/tests/test_webhooks.py +64 -0
|
@@ -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.
|
skrap_ai-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|