verifly-email 1.0.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.
- verifly_email-1.0.0/PKG-INFO +145 -0
- verifly_email-1.0.0/README.md +130 -0
- verifly_email-1.0.0/pyproject.toml +28 -0
- verifly_email-1.0.0/setup.cfg +4 -0
- verifly_email-1.0.0/verifly_email/__init__.py +16 -0
- verifly_email-1.0.0/verifly_email/client.py +465 -0
- verifly_email-1.0.0/verifly_email/py.typed +0 -0
- verifly_email-1.0.0/verifly_email.egg-info/PKG-INFO +145 -0
- verifly_email-1.0.0/verifly_email.egg-info/SOURCES.txt +10 -0
- verifly_email-1.0.0/verifly_email.egg-info/dependency_links.txt +1 -0
- verifly_email-1.0.0/verifly_email.egg-info/requires.txt +3 -0
- verifly_email-1.0.0/verifly_email.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verifly-email
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the Verifly email-verification API (verifly.email)
|
|
5
|
+
Author: Verifly
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://verifly.email
|
|
8
|
+
Project-URL: Documentation, https://verifly.email/docs/api
|
|
9
|
+
Project-URL: API Spec, https://verifly.email/openapi.json
|
|
10
|
+
Keywords: email,verification,validation,verifly,deliverability
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# verifly-email (Python)
|
|
17
|
+
|
|
18
|
+
Official Python SDK for the [Verifly](https://verifly.email) email-verification API.
|
|
19
|
+
|
|
20
|
+
> **Package naming.** Install **`verifly-email`** and import **`verifly_email`**.
|
|
21
|
+
> The plain name `verifly` on PyPI belongs to an **unrelated 2FA company** — it
|
|
22
|
+
> is *not* this project. Always use `verifly-email`.
|
|
23
|
+
|
|
24
|
+
- Zero dependencies (pure Python standard library).
|
|
25
|
+
- Fully typed, with docstrings on every method.
|
|
26
|
+
- Built-in retry with backoff on `429` / `5xx` (honors `Retry-After`).
|
|
27
|
+
- Automatic `Idempotency-Key` for `buy_credits` and `submit_bulk`.
|
|
28
|
+
- Typed `VeriflyError(code, message, request_id)` on API error envelopes.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install verifly-email # once published
|
|
34
|
+
# or, from this repo:
|
|
35
|
+
pip install /path/to/sdks/python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from verifly_email import VeriflyClient, VeriflyError
|
|
42
|
+
|
|
43
|
+
client = VeriflyClient("vf_your_api_key") # base_url defaults to https://verifly.email
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
r = client.verify("bill.gates@microsoft.com")
|
|
47
|
+
print(r["result"]) # deliverable | undeliverable | risky | unknown
|
|
48
|
+
print(r["recommendation"]) # safe_to_send | risky | do_not_send
|
|
49
|
+
print(r["credits"]) # {"used": 1, "remaining": 99}
|
|
50
|
+
except VeriflyError as e:
|
|
51
|
+
print(e.code, e.message, e.request_id)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Every call (except `register`) authenticates with your `vf_` key, sent as
|
|
57
|
+
`Authorization: Bearer <api_key>`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = VeriflyClient(api_key="vf_...", base_url="https://verifly.email")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Create an account programmatically
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
res = VeriflyClient.register("you@example.com", "a-strong-password")
|
|
67
|
+
api_key = res["api_key"]["key"] # shown ONCE — store it now
|
|
68
|
+
client = VeriflyClient(api_key)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Methods
|
|
72
|
+
|
|
73
|
+
| Method | Description |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `verify(email)` | Verify a single address. |
|
|
76
|
+
| `verify_batch(emails, deduplicate=True, ...)` | Verify up to 100 addresses synchronously. |
|
|
77
|
+
| `clean(emails, options=None)` | Clean/filter a list (no verification, no credits). |
|
|
78
|
+
| `extract(text, deduplicate=True, lowercase=True)` | Pull email addresses out of text/CSV. |
|
|
79
|
+
| `submit_bulk(emails=None, text=None, webhook_url=None, ...)` | Create an async bulk job (up to 1M). |
|
|
80
|
+
| `jobs(status=None, limit=None, offset=None)` | List bulk jobs. |
|
|
81
|
+
| `job(job_id)` | Get a bulk job's status. |
|
|
82
|
+
| `job_results(job_id)` | Get a completed job's per-email results. |
|
|
83
|
+
| `account()` | Account profile + credit summary. |
|
|
84
|
+
| `credits()` | Current credit balance. |
|
|
85
|
+
| `usage(period=None, limit=None)` | API usage summary (day/week/month). |
|
|
86
|
+
| `packages()` | List credit packages and prices. |
|
|
87
|
+
| `payment_history()` | List payment history. |
|
|
88
|
+
| `buy_credits(package_id, method="stripe", currency=None)` | Create a Stripe/crypto checkout. |
|
|
89
|
+
| `VeriflyClient.register(email, password)` *(classmethod)* | Self-register, returns account + API key. |
|
|
90
|
+
|
|
91
|
+
### Examples
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Batch (<=100), synchronous
|
|
95
|
+
batch = client.verify_batch(
|
|
96
|
+
["a@example.com", "b@example.com"],
|
|
97
|
+
exclude_role_accounts=True,
|
|
98
|
+
)
|
|
99
|
+
for item in batch["results"]:
|
|
100
|
+
print(item["email"], item["result"])
|
|
101
|
+
|
|
102
|
+
# List hygiene without spending credits
|
|
103
|
+
print(client.clean(["A@Example.com ", "a@example.com", "bad"])["..."])
|
|
104
|
+
print(client.extract("contact us at sales@acme.io or ceo@acme.io"))
|
|
105
|
+
|
|
106
|
+
# Async bulk + polling
|
|
107
|
+
job = client.submit_bulk(emails=[...], webhook_url="https://you/webhook")
|
|
108
|
+
status = client.job(job["job_id"] if "job_id" in job else job["job"]["id"])
|
|
109
|
+
results = client.job_results(job_id)
|
|
110
|
+
|
|
111
|
+
# Account / billing
|
|
112
|
+
print(client.credits())
|
|
113
|
+
print(client.packages())
|
|
114
|
+
checkout = client.buy_credits("pro", method="stripe")
|
|
115
|
+
checkout = client.buy_credits("pro", method="crypto", currency="USDT")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
API error envelopes (`{"success": false, "error": {...}}`) and non-2xx
|
|
121
|
+
responses raise `VeriflyError`:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
try:
|
|
125
|
+
client.verify("nope")
|
|
126
|
+
except VeriflyError as e:
|
|
127
|
+
e.code # e.g. "invalid_email", "insufficient_credits", "rate_limit_exceeded"
|
|
128
|
+
e.message
|
|
129
|
+
e.request_id # from the x-request-id response header
|
|
130
|
+
e.status # HTTP status
|
|
131
|
+
e.suggestion # optional remediation hint
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Retries & idempotency
|
|
135
|
+
|
|
136
|
+
- `429` and `5xx` responses are retried (default 3 times) with exponential
|
|
137
|
+
backoff, honoring the `Retry-After` header. Configure via
|
|
138
|
+
`VeriflyClient(..., max_retries=N, timeout=seconds)`.
|
|
139
|
+
- `buy_credits` and `submit_bulk` send an `Idempotency-Key` header. One is
|
|
140
|
+
auto-generated per call; pass your own with `idempotency_key=...` to make a
|
|
141
|
+
specific retry safe end-to-end.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# verifly-email (Python)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Verifly](https://verifly.email) email-verification API.
|
|
4
|
+
|
|
5
|
+
> **Package naming.** Install **`verifly-email`** and import **`verifly_email`**.
|
|
6
|
+
> The plain name `verifly` on PyPI belongs to an **unrelated 2FA company** — it
|
|
7
|
+
> is *not* this project. Always use `verifly-email`.
|
|
8
|
+
|
|
9
|
+
- Zero dependencies (pure Python standard library).
|
|
10
|
+
- Fully typed, with docstrings on every method.
|
|
11
|
+
- Built-in retry with backoff on `429` / `5xx` (honors `Retry-After`).
|
|
12
|
+
- Automatic `Idempotency-Key` for `buy_credits` and `submit_bulk`.
|
|
13
|
+
- Typed `VeriflyError(code, message, request_id)` on API error envelopes.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install verifly-email # once published
|
|
19
|
+
# or, from this repo:
|
|
20
|
+
pip install /path/to/sdks/python
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from verifly_email import VeriflyClient, VeriflyError
|
|
27
|
+
|
|
28
|
+
client = VeriflyClient("vf_your_api_key") # base_url defaults to https://verifly.email
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
r = client.verify("bill.gates@microsoft.com")
|
|
32
|
+
print(r["result"]) # deliverable | undeliverable | risky | unknown
|
|
33
|
+
print(r["recommendation"]) # safe_to_send | risky | do_not_send
|
|
34
|
+
print(r["credits"]) # {"used": 1, "remaining": 99}
|
|
35
|
+
except VeriflyError as e:
|
|
36
|
+
print(e.code, e.message, e.request_id)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
Every call (except `register`) authenticates with your `vf_` key, sent as
|
|
42
|
+
`Authorization: Bearer <api_key>`.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
client = VeriflyClient(api_key="vf_...", base_url="https://verifly.email")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Create an account programmatically
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
res = VeriflyClient.register("you@example.com", "a-strong-password")
|
|
52
|
+
api_key = res["api_key"]["key"] # shown ONCE — store it now
|
|
53
|
+
client = VeriflyClient(api_key)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Methods
|
|
57
|
+
|
|
58
|
+
| Method | Description |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `verify(email)` | Verify a single address. |
|
|
61
|
+
| `verify_batch(emails, deduplicate=True, ...)` | Verify up to 100 addresses synchronously. |
|
|
62
|
+
| `clean(emails, options=None)` | Clean/filter a list (no verification, no credits). |
|
|
63
|
+
| `extract(text, deduplicate=True, lowercase=True)` | Pull email addresses out of text/CSV. |
|
|
64
|
+
| `submit_bulk(emails=None, text=None, webhook_url=None, ...)` | Create an async bulk job (up to 1M). |
|
|
65
|
+
| `jobs(status=None, limit=None, offset=None)` | List bulk jobs. |
|
|
66
|
+
| `job(job_id)` | Get a bulk job's status. |
|
|
67
|
+
| `job_results(job_id)` | Get a completed job's per-email results. |
|
|
68
|
+
| `account()` | Account profile + credit summary. |
|
|
69
|
+
| `credits()` | Current credit balance. |
|
|
70
|
+
| `usage(period=None, limit=None)` | API usage summary (day/week/month). |
|
|
71
|
+
| `packages()` | List credit packages and prices. |
|
|
72
|
+
| `payment_history()` | List payment history. |
|
|
73
|
+
| `buy_credits(package_id, method="stripe", currency=None)` | Create a Stripe/crypto checkout. |
|
|
74
|
+
| `VeriflyClient.register(email, password)` *(classmethod)* | Self-register, returns account + API key. |
|
|
75
|
+
|
|
76
|
+
### Examples
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Batch (<=100), synchronous
|
|
80
|
+
batch = client.verify_batch(
|
|
81
|
+
["a@example.com", "b@example.com"],
|
|
82
|
+
exclude_role_accounts=True,
|
|
83
|
+
)
|
|
84
|
+
for item in batch["results"]:
|
|
85
|
+
print(item["email"], item["result"])
|
|
86
|
+
|
|
87
|
+
# List hygiene without spending credits
|
|
88
|
+
print(client.clean(["A@Example.com ", "a@example.com", "bad"])["..."])
|
|
89
|
+
print(client.extract("contact us at sales@acme.io or ceo@acme.io"))
|
|
90
|
+
|
|
91
|
+
# Async bulk + polling
|
|
92
|
+
job = client.submit_bulk(emails=[...], webhook_url="https://you/webhook")
|
|
93
|
+
status = client.job(job["job_id"] if "job_id" in job else job["job"]["id"])
|
|
94
|
+
results = client.job_results(job_id)
|
|
95
|
+
|
|
96
|
+
# Account / billing
|
|
97
|
+
print(client.credits())
|
|
98
|
+
print(client.packages())
|
|
99
|
+
checkout = client.buy_credits("pro", method="stripe")
|
|
100
|
+
checkout = client.buy_credits("pro", method="crypto", currency="USDT")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Errors
|
|
104
|
+
|
|
105
|
+
API error envelopes (`{"success": false, "error": {...}}`) and non-2xx
|
|
106
|
+
responses raise `VeriflyError`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
try:
|
|
110
|
+
client.verify("nope")
|
|
111
|
+
except VeriflyError as e:
|
|
112
|
+
e.code # e.g. "invalid_email", "insufficient_credits", "rate_limit_exceeded"
|
|
113
|
+
e.message
|
|
114
|
+
e.request_id # from the x-request-id response header
|
|
115
|
+
e.status # HTTP status
|
|
116
|
+
e.suggestion # optional remediation hint
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Retries & idempotency
|
|
120
|
+
|
|
121
|
+
- `429` and `5xx` responses are retried (default 3 times) with exponential
|
|
122
|
+
backoff, honoring the `Retry-After` header. Configure via
|
|
123
|
+
`VeriflyClient(..., max_retries=N, timeout=seconds)`.
|
|
124
|
+
- `buy_credits` and `submit_bulk` send an `Idempotency-Key` header. One is
|
|
125
|
+
auto-generated per call; pass your own with `idempotency_key=...` to make a
|
|
126
|
+
specific retry safe end-to-end.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verifly-email"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the Verifly email-verification API (verifly.email)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Verifly" }]
|
|
13
|
+
keywords = ["email", "verification", "validation", "verifly", "deliverability"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://verifly.email"
|
|
18
|
+
Documentation = "https://verifly.email/docs/api"
|
|
19
|
+
"API Spec" = "https://verifly.email/openapi.json"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=7"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ["verifly_email"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
verifly_email = ["py.typed"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Official Python SDK for the Verifly email-verification API (https://verifly.email).
|
|
2
|
+
|
|
3
|
+
PyPI package name: ``verifly-email`` (import as ``verifly_email``).
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
from verifly_email import VeriflyClient
|
|
8
|
+
|
|
9
|
+
client = VeriflyClient("vf_your_api_key")
|
|
10
|
+
result = client.verify("bill.gates@microsoft.com")
|
|
11
|
+
print(result["result"], result["recommendation"])
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import VeriflyClient, VeriflyError, __version__
|
|
15
|
+
|
|
16
|
+
__all__ = ["VeriflyClient", "VeriflyError", "__version__"]
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Verifly API client.
|
|
2
|
+
|
|
3
|
+
Zero third-party dependencies: built on the Python standard library only
|
|
4
|
+
(``urllib``), so it installs and runs anywhere with no extra packages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
__version__ = "1.0.0"
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://verifly.email"
|
|
20
|
+
DEFAULT_TIMEOUT = 30.0
|
|
21
|
+
DEFAULT_MAX_RETRIES = 3
|
|
22
|
+
_USER_AGENT = f"verifly-email-python/{__version__}"
|
|
23
|
+
|
|
24
|
+
JSON = Dict[str, Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
28
|
+
"""Redirect handler that never leaks credentials across origins.
|
|
29
|
+
|
|
30
|
+
``urllib`` follows 3xx redirects automatically and, by default, forwards
|
|
31
|
+
every request header -- including ``Authorization`` -- to the redirect
|
|
32
|
+
target even when it points at a different scheme/host. That would leak the
|
|
33
|
+
``vf_`` API key to an unrelated server. This strips the ``Authorization``
|
|
34
|
+
header whenever a redirect crosses to a different scheme, host, or port.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
38
|
+
new_req = super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
39
|
+
if new_req is not None:
|
|
40
|
+
old = urllib.parse.urlsplit(req.full_url)
|
|
41
|
+
new = urllib.parse.urlsplit(newurl)
|
|
42
|
+
if (old.scheme, old.hostname, old.port) != (
|
|
43
|
+
new.scheme,
|
|
44
|
+
new.hostname,
|
|
45
|
+
new.port,
|
|
46
|
+
):
|
|
47
|
+
new_req.headers.pop("Authorization", None)
|
|
48
|
+
new_req.unredirected_hdrs.pop("Authorization", None)
|
|
49
|
+
return new_req
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VeriflyError(Exception):
|
|
53
|
+
"""Raised when the Verifly API returns an error envelope or the request fails.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
code: Machine-readable error code (e.g. ``invalid_api_key``,
|
|
57
|
+
``insufficient_credits``, ``rate_limit_exceeded``). ``http_error``
|
|
58
|
+
for transport-level failures.
|
|
59
|
+
message: Human-readable explanation.
|
|
60
|
+
request_id: Server request id, when provided (from the
|
|
61
|
+
``x-request-id`` response header).
|
|
62
|
+
status: HTTP status code, when available.
|
|
63
|
+
suggestion: Optional remediation hint from the API.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
code: str,
|
|
69
|
+
message: str,
|
|
70
|
+
request_id: Optional[str] = None,
|
|
71
|
+
status: Optional[int] = None,
|
|
72
|
+
suggestion: Optional[str] = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(f"[{code}] {message}")
|
|
75
|
+
self.code = code
|
|
76
|
+
self.message = message
|
|
77
|
+
self.request_id = request_id
|
|
78
|
+
self.status = status
|
|
79
|
+
self.suggestion = suggestion
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class VeriflyClient:
|
|
83
|
+
"""Typed client for the Verifly email-verification API.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
api_key: Your ``vf_`` API key. Sent as ``Authorization: Bearer <key>``.
|
|
87
|
+
Not required for :meth:`register`.
|
|
88
|
+
base_url: API base URL. Defaults to ``https://verifly.email``.
|
|
89
|
+
timeout: Per-request timeout in seconds.
|
|
90
|
+
max_retries: Number of retries on 429 / 5xx responses (with backoff,
|
|
91
|
+
honoring ``Retry-After``).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
api_key: Optional[str] = None,
|
|
97
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
98
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
99
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
100
|
+
) -> None:
|
|
101
|
+
self.api_key = api_key
|
|
102
|
+
self.base_url = base_url.rstrip("/")
|
|
103
|
+
self.timeout = timeout
|
|
104
|
+
self.max_retries = max_retries
|
|
105
|
+
# Open requests through a handler that strips Authorization on
|
|
106
|
+
# cross-origin redirects so the API key can never leak to another host.
|
|
107
|
+
self._opener = urllib.request.build_opener(_SafeRedirectHandler())
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------ #
|
|
110
|
+
# Account lifecycle
|
|
111
|
+
# ------------------------------------------------------------------ #
|
|
112
|
+
@classmethod
|
|
113
|
+
def register(
|
|
114
|
+
cls,
|
|
115
|
+
email: str,
|
|
116
|
+
password: str,
|
|
117
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
118
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
119
|
+
) -> JSON:
|
|
120
|
+
"""Self-register a new account and receive an API key (100 free credits).
|
|
121
|
+
|
|
122
|
+
The full API key is shown exactly once, under ``result["api_key"]["key"]``.
|
|
123
|
+
|
|
124
|
+
Returns the ``RegisterResult`` envelope: ``{success, message, account,
|
|
125
|
+
api_key}``.
|
|
126
|
+
"""
|
|
127
|
+
client = cls(api_key=None, base_url=base_url, timeout=timeout)
|
|
128
|
+
return client._request(
|
|
129
|
+
"POST",
|
|
130
|
+
"/api/v1/autonomous/register",
|
|
131
|
+
body={"email": email, "password": password},
|
|
132
|
+
auth=False,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
# Verification
|
|
137
|
+
# ------------------------------------------------------------------ #
|
|
138
|
+
def verify(self, email: str) -> JSON:
|
|
139
|
+
"""Verify a single email address.
|
|
140
|
+
|
|
141
|
+
Returns a ``VerificationResult``: ``{success, email, is_valid, result
|
|
142
|
+
(deliverable|undeliverable|risky|unknown), reason, details{...},
|
|
143
|
+
recommendation (safe_to_send|risky|do_not_send), credits_charged,
|
|
144
|
+
credits{used,remaining}}``.
|
|
145
|
+
"""
|
|
146
|
+
return self._request("GET", "/api/v1/verify", query={"email": email})
|
|
147
|
+
|
|
148
|
+
def verify_batch(
|
|
149
|
+
self,
|
|
150
|
+
emails: Sequence[str],
|
|
151
|
+
deduplicate: bool = True,
|
|
152
|
+
exclude_public_domains: bool = False,
|
|
153
|
+
exclude_role_accounts: bool = False,
|
|
154
|
+
domain_blacklist: Optional[Sequence[str]] = None,
|
|
155
|
+
pattern_blacklist: Optional[Sequence[str]] = None,
|
|
156
|
+
) -> JSON:
|
|
157
|
+
"""Verify up to 100 emails synchronously.
|
|
158
|
+
|
|
159
|
+
Returns a ``BatchVerificationResult`` with a ``results`` array.
|
|
160
|
+
"""
|
|
161
|
+
options: JSON = {
|
|
162
|
+
"deduplicate": deduplicate,
|
|
163
|
+
"exclude_public_domains": exclude_public_domains,
|
|
164
|
+
"exclude_role_accounts": exclude_role_accounts,
|
|
165
|
+
}
|
|
166
|
+
if domain_blacklist is not None:
|
|
167
|
+
options["domain_blacklist"] = list(domain_blacklist)
|
|
168
|
+
if pattern_blacklist is not None:
|
|
169
|
+
options["pattern_blacklist"] = list(pattern_blacklist)
|
|
170
|
+
return self._request(
|
|
171
|
+
"POST",
|
|
172
|
+
"/api/v1/verify/batch",
|
|
173
|
+
body={"emails": list(emails), "options": options},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def submit_bulk(
|
|
177
|
+
self,
|
|
178
|
+
emails: Optional[Sequence[str]] = None,
|
|
179
|
+
text: Optional[str] = None,
|
|
180
|
+
filename: Optional[str] = None,
|
|
181
|
+
webhook_url: Optional[str] = None,
|
|
182
|
+
idempotency_key: Optional[str] = None,
|
|
183
|
+
) -> JSON:
|
|
184
|
+
"""Create an asynchronous bulk verification job.
|
|
185
|
+
|
|
186
|
+
Provide either ``emails`` (up to 1,000,000) or raw ``text``/CSV to
|
|
187
|
+
extract addresses from. ``webhook_url`` is called when the job
|
|
188
|
+
completes. Returns a ``BulkJobResult`` with the job id; poll with
|
|
189
|
+
:meth:`job` and fetch output with :meth:`job_results`.
|
|
190
|
+
|
|
191
|
+
``idempotency_key`` is passed through as the ``Idempotency-Key`` header;
|
|
192
|
+
if omitted, one is generated automatically so retries are safe.
|
|
193
|
+
"""
|
|
194
|
+
if emails is None and text is None:
|
|
195
|
+
raise ValueError("submit_bulk requires either 'emails' or 'text'")
|
|
196
|
+
body: JSON = {}
|
|
197
|
+
if emails is not None:
|
|
198
|
+
body["emails"] = list(emails)
|
|
199
|
+
if text is not None:
|
|
200
|
+
body["text"] = text
|
|
201
|
+
if filename is not None:
|
|
202
|
+
body["filename"] = filename
|
|
203
|
+
if webhook_url is not None:
|
|
204
|
+
body["webhook_url"] = webhook_url
|
|
205
|
+
return self._request(
|
|
206
|
+
"POST",
|
|
207
|
+
"/api/v1/verify/bulk",
|
|
208
|
+
body=body,
|
|
209
|
+
idempotency_key=idempotency_key or str(uuid.uuid4()),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# ------------------------------------------------------------------ #
|
|
213
|
+
# List hygiene
|
|
214
|
+
# ------------------------------------------------------------------ #
|
|
215
|
+
def clean(
|
|
216
|
+
self,
|
|
217
|
+
emails: Sequence[str],
|
|
218
|
+
options: Optional[JSON] = None,
|
|
219
|
+
) -> JSON:
|
|
220
|
+
"""Clean and filter an email list (dedupe, syntax, role/disposable, etc).
|
|
221
|
+
|
|
222
|
+
Does not verify deliverability and does not consume credits. Returns a
|
|
223
|
+
``CleanResult``. Pass ``options`` to control the cleaning behavior.
|
|
224
|
+
"""
|
|
225
|
+
body: JSON = {"emails": list(emails)}
|
|
226
|
+
if options is not None:
|
|
227
|
+
body["options"] = options
|
|
228
|
+
return self._request("POST", "/api/v1/clean", body=body)
|
|
229
|
+
|
|
230
|
+
def extract(self, text: str, deduplicate: bool = True, lowercase: bool = True) -> JSON:
|
|
231
|
+
"""Extract email addresses from arbitrary text or CSV content.
|
|
232
|
+
|
|
233
|
+
Returns an ``ExtractResult`` with the found addresses.
|
|
234
|
+
"""
|
|
235
|
+
return self._request(
|
|
236
|
+
"POST",
|
|
237
|
+
"/api/v1/extract",
|
|
238
|
+
body={
|
|
239
|
+
"text": text,
|
|
240
|
+
"options": {"deduplicate": deduplicate, "lowercase": lowercase},
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------ #
|
|
245
|
+
# Jobs
|
|
246
|
+
# ------------------------------------------------------------------ #
|
|
247
|
+
def jobs(
|
|
248
|
+
self,
|
|
249
|
+
status: Optional[str] = None,
|
|
250
|
+
limit: Optional[int] = None,
|
|
251
|
+
offset: Optional[int] = None,
|
|
252
|
+
) -> JSON:
|
|
253
|
+
"""List bulk verification jobs (optionally filtered by status)."""
|
|
254
|
+
query: JSON = {}
|
|
255
|
+
if status is not None:
|
|
256
|
+
query["status"] = status
|
|
257
|
+
if limit is not None:
|
|
258
|
+
query["limit"] = limit
|
|
259
|
+
if offset is not None:
|
|
260
|
+
query["offset"] = offset
|
|
261
|
+
return self._request("GET", "/api/v1/jobs", query=query or None)
|
|
262
|
+
|
|
263
|
+
def job(self, job_id: str) -> JSON:
|
|
264
|
+
"""Get the status of a single bulk job."""
|
|
265
|
+
return self._request("GET", f"/api/v1/jobs/{urllib.parse.quote(job_id)}")
|
|
266
|
+
|
|
267
|
+
def job_results(self, job_id: str) -> JSON:
|
|
268
|
+
"""Get the per-email results of a completed bulk job as JSON."""
|
|
269
|
+
return self._request(
|
|
270
|
+
"GET", f"/api/v1/jobs/{urllib.parse.quote(job_id)}/results"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------ #
|
|
274
|
+
# Account, credits, usage
|
|
275
|
+
# ------------------------------------------------------------------ #
|
|
276
|
+
def account(self) -> JSON:
|
|
277
|
+
"""Get the account profile and credit summary."""
|
|
278
|
+
return self._request("GET", "/api/v1/account")
|
|
279
|
+
|
|
280
|
+
def credits(self) -> JSON:
|
|
281
|
+
"""Get the current credit balance."""
|
|
282
|
+
return self._request("GET", "/api/v1/credits")
|
|
283
|
+
|
|
284
|
+
def usage(self, period: Optional[str] = None, limit: Optional[int] = None) -> JSON:
|
|
285
|
+
"""Get an API usage summary. ``period`` is one of day|week|month."""
|
|
286
|
+
query: JSON = {}
|
|
287
|
+
if period is not None:
|
|
288
|
+
query["period"] = period
|
|
289
|
+
if limit is not None:
|
|
290
|
+
query["limit"] = limit
|
|
291
|
+
return self._request("GET", "/api/v1/usage", query=query or None)
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------ #
|
|
294
|
+
# Billing
|
|
295
|
+
# ------------------------------------------------------------------ #
|
|
296
|
+
def packages(self) -> JSON:
|
|
297
|
+
"""List the available credit packages and their prices."""
|
|
298
|
+
return self._request(
|
|
299
|
+
"GET", "/api/v1/billing", query={"action": "packages"}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def payment_history(self) -> JSON:
|
|
303
|
+
"""List the account's payment history."""
|
|
304
|
+
return self._request(
|
|
305
|
+
"GET", "/api/v1/billing", query={"action": "history"}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def buy_credits(
|
|
309
|
+
self,
|
|
310
|
+
package_id: str,
|
|
311
|
+
method: str = "stripe",
|
|
312
|
+
currency: Optional[str] = None,
|
|
313
|
+
idempotency_key: Optional[str] = None,
|
|
314
|
+
) -> JSON:
|
|
315
|
+
"""Create a checkout to buy a credit package.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
package_id: One of starter|basic|pro|business|enterprise.
|
|
319
|
+
method: ``stripe`` (default) or ``crypto``.
|
|
320
|
+
currency: Only for ``method="crypto"`` -- one of BTC|ETH|LTC|USDT|USDC.
|
|
321
|
+
When set, returns a raw wallet address + amount + qr_code.
|
|
322
|
+
idempotency_key: Passed through as the ``Idempotency-Key`` header.
|
|
323
|
+
Auto-generated if omitted so retries never double-charge.
|
|
324
|
+
|
|
325
|
+
Returns a Stripe or crypto checkout result.
|
|
326
|
+
"""
|
|
327
|
+
body: JSON = {"package_id": package_id, "method": method}
|
|
328
|
+
if currency is not None:
|
|
329
|
+
body["currency"] = currency
|
|
330
|
+
return self._request(
|
|
331
|
+
"POST",
|
|
332
|
+
"/api/v1/billing",
|
|
333
|
+
body=body,
|
|
334
|
+
idempotency_key=idempotency_key or str(uuid.uuid4()),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------------ #
|
|
338
|
+
# HTTP plumbing
|
|
339
|
+
# ------------------------------------------------------------------ #
|
|
340
|
+
def _request(
|
|
341
|
+
self,
|
|
342
|
+
method: str,
|
|
343
|
+
path: str,
|
|
344
|
+
query: Optional[JSON] = None,
|
|
345
|
+
body: Optional[JSON] = None,
|
|
346
|
+
auth: bool = True,
|
|
347
|
+
idempotency_key: Optional[str] = None,
|
|
348
|
+
) -> JSON:
|
|
349
|
+
url = self.base_url + path
|
|
350
|
+
if query:
|
|
351
|
+
url += "?" + urllib.parse.urlencode(query)
|
|
352
|
+
|
|
353
|
+
headers = {
|
|
354
|
+
"Accept": "application/json",
|
|
355
|
+
"User-Agent": _USER_AGENT,
|
|
356
|
+
}
|
|
357
|
+
if auth:
|
|
358
|
+
if not self.api_key:
|
|
359
|
+
raise VeriflyError(
|
|
360
|
+
"missing_api_key",
|
|
361
|
+
"An API key is required for this call. "
|
|
362
|
+
"Pass api_key to VeriflyClient(...).",
|
|
363
|
+
)
|
|
364
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
365
|
+
if idempotency_key:
|
|
366
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
367
|
+
|
|
368
|
+
data: Optional[bytes] = None
|
|
369
|
+
if body is not None:
|
|
370
|
+
data = json.dumps(body).encode("utf-8")
|
|
371
|
+
headers["Content-Type"] = "application/json"
|
|
372
|
+
|
|
373
|
+
# Only auto-retry requests that are safe to resend. GET/HEAD are
|
|
374
|
+
# idempotent; a POST is safe only when it carries an Idempotency-Key
|
|
375
|
+
# (the server dedupes those). Non-idempotent POSTs such as verify_batch
|
|
376
|
+
# have no server-side dedupe, so a timed-out request must never be
|
|
377
|
+
# re-sent or it could be charged 2-4x.
|
|
378
|
+
retryable = method in ("GET", "HEAD") or idempotency_key is not None
|
|
379
|
+
|
|
380
|
+
attempt = 0
|
|
381
|
+
while True:
|
|
382
|
+
attempt += 1
|
|
383
|
+
try:
|
|
384
|
+
payload, status, resp_headers = self._send(method, url, headers, data)
|
|
385
|
+
except urllib.error.HTTPError as exc:
|
|
386
|
+
payload, status, resp_headers = self._read_http_error(exc)
|
|
387
|
+
except urllib.error.URLError as exc:
|
|
388
|
+
if retryable and attempt <= self.max_retries:
|
|
389
|
+
time.sleep(self._backoff(attempt))
|
|
390
|
+
continue
|
|
391
|
+
raise VeriflyError(
|
|
392
|
+
"http_error", f"Network error contacting Verifly: {exc.reason}"
|
|
393
|
+
) from exc
|
|
394
|
+
|
|
395
|
+
request_id = resp_headers.get("x-request-id") or resp_headers.get(
|
|
396
|
+
"X-Request-Id"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if status == 429 or status >= 500:
|
|
400
|
+
if retryable and attempt <= self.max_retries:
|
|
401
|
+
retry_after = self._parse_retry_after(resp_headers)
|
|
402
|
+
time.sleep(
|
|
403
|
+
retry_after if retry_after is not None else self._backoff(attempt)
|
|
404
|
+
)
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
if status >= 400 or (isinstance(payload, dict) and payload.get("success") is False):
|
|
408
|
+
self._raise_for_envelope(payload, status, request_id)
|
|
409
|
+
|
|
410
|
+
if not isinstance(payload, dict):
|
|
411
|
+
raise VeriflyError(
|
|
412
|
+
"invalid_response",
|
|
413
|
+
"Expected a JSON object response from Verifly.",
|
|
414
|
+
request_id,
|
|
415
|
+
status,
|
|
416
|
+
)
|
|
417
|
+
return payload
|
|
418
|
+
|
|
419
|
+
def _send(self, method: str, url: str, headers: JSON, data: Optional[bytes]):
|
|
420
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
421
|
+
with self._opener.open(req, timeout=self.timeout) as resp:
|
|
422
|
+
raw = resp.read().decode("utf-8")
|
|
423
|
+
parsed = json.loads(raw) if raw else {}
|
|
424
|
+
return parsed, resp.status, dict(resp.headers)
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def _read_http_error(exc: urllib.error.HTTPError):
|
|
428
|
+
raw = exc.read().decode("utf-8") if exc.fp else ""
|
|
429
|
+
try:
|
|
430
|
+
parsed = json.loads(raw) if raw else {}
|
|
431
|
+
except json.JSONDecodeError:
|
|
432
|
+
parsed = {"success": False, "error": {"code": "http_error", "message": raw or exc.reason}}
|
|
433
|
+
return parsed, exc.code, dict(exc.headers or {})
|
|
434
|
+
|
|
435
|
+
@staticmethod
|
|
436
|
+
def _raise_for_envelope(payload: Any, status: int, request_id: Optional[str]) -> None:
|
|
437
|
+
if isinstance(payload, dict) and isinstance(payload.get("error"), dict):
|
|
438
|
+
err = payload["error"]
|
|
439
|
+
raise VeriflyError(
|
|
440
|
+
err.get("code", "unknown_error"),
|
|
441
|
+
err.get("message", "Unknown error"),
|
|
442
|
+
request_id,
|
|
443
|
+
status,
|
|
444
|
+
err.get("suggestion"),
|
|
445
|
+
)
|
|
446
|
+
raise VeriflyError(
|
|
447
|
+
"http_error",
|
|
448
|
+
f"Request failed with HTTP {status}",
|
|
449
|
+
request_id,
|
|
450
|
+
status,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _backoff(attempt: int) -> float:
|
|
455
|
+
return min(2.0 ** (attempt - 1), 30.0)
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _parse_retry_after(headers: JSON) -> Optional[float]:
|
|
459
|
+
value = headers.get("Retry-After") or headers.get("retry-after")
|
|
460
|
+
if value is None:
|
|
461
|
+
return None
|
|
462
|
+
try:
|
|
463
|
+
return float(value)
|
|
464
|
+
except (TypeError, ValueError):
|
|
465
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verifly-email
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the Verifly email-verification API (verifly.email)
|
|
5
|
+
Author: Verifly
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://verifly.email
|
|
8
|
+
Project-URL: Documentation, https://verifly.email/docs/api
|
|
9
|
+
Project-URL: API Spec, https://verifly.email/openapi.json
|
|
10
|
+
Keywords: email,verification,validation,verifly,deliverability
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# verifly-email (Python)
|
|
17
|
+
|
|
18
|
+
Official Python SDK for the [Verifly](https://verifly.email) email-verification API.
|
|
19
|
+
|
|
20
|
+
> **Package naming.** Install **`verifly-email`** and import **`verifly_email`**.
|
|
21
|
+
> The plain name `verifly` on PyPI belongs to an **unrelated 2FA company** — it
|
|
22
|
+
> is *not* this project. Always use `verifly-email`.
|
|
23
|
+
|
|
24
|
+
- Zero dependencies (pure Python standard library).
|
|
25
|
+
- Fully typed, with docstrings on every method.
|
|
26
|
+
- Built-in retry with backoff on `429` / `5xx` (honors `Retry-After`).
|
|
27
|
+
- Automatic `Idempotency-Key` for `buy_credits` and `submit_bulk`.
|
|
28
|
+
- Typed `VeriflyError(code, message, request_id)` on API error envelopes.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install verifly-email # once published
|
|
34
|
+
# or, from this repo:
|
|
35
|
+
pip install /path/to/sdks/python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from verifly_email import VeriflyClient, VeriflyError
|
|
42
|
+
|
|
43
|
+
client = VeriflyClient("vf_your_api_key") # base_url defaults to https://verifly.email
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
r = client.verify("bill.gates@microsoft.com")
|
|
47
|
+
print(r["result"]) # deliverable | undeliverable | risky | unknown
|
|
48
|
+
print(r["recommendation"]) # safe_to_send | risky | do_not_send
|
|
49
|
+
print(r["credits"]) # {"used": 1, "remaining": 99}
|
|
50
|
+
except VeriflyError as e:
|
|
51
|
+
print(e.code, e.message, e.request_id)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Every call (except `register`) authenticates with your `vf_` key, sent as
|
|
57
|
+
`Authorization: Bearer <api_key>`.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client = VeriflyClient(api_key="vf_...", base_url="https://verifly.email")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Create an account programmatically
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
res = VeriflyClient.register("you@example.com", "a-strong-password")
|
|
67
|
+
api_key = res["api_key"]["key"] # shown ONCE — store it now
|
|
68
|
+
client = VeriflyClient(api_key)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Methods
|
|
72
|
+
|
|
73
|
+
| Method | Description |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `verify(email)` | Verify a single address. |
|
|
76
|
+
| `verify_batch(emails, deduplicate=True, ...)` | Verify up to 100 addresses synchronously. |
|
|
77
|
+
| `clean(emails, options=None)` | Clean/filter a list (no verification, no credits). |
|
|
78
|
+
| `extract(text, deduplicate=True, lowercase=True)` | Pull email addresses out of text/CSV. |
|
|
79
|
+
| `submit_bulk(emails=None, text=None, webhook_url=None, ...)` | Create an async bulk job (up to 1M). |
|
|
80
|
+
| `jobs(status=None, limit=None, offset=None)` | List bulk jobs. |
|
|
81
|
+
| `job(job_id)` | Get a bulk job's status. |
|
|
82
|
+
| `job_results(job_id)` | Get a completed job's per-email results. |
|
|
83
|
+
| `account()` | Account profile + credit summary. |
|
|
84
|
+
| `credits()` | Current credit balance. |
|
|
85
|
+
| `usage(period=None, limit=None)` | API usage summary (day/week/month). |
|
|
86
|
+
| `packages()` | List credit packages and prices. |
|
|
87
|
+
| `payment_history()` | List payment history. |
|
|
88
|
+
| `buy_credits(package_id, method="stripe", currency=None)` | Create a Stripe/crypto checkout. |
|
|
89
|
+
| `VeriflyClient.register(email, password)` *(classmethod)* | Self-register, returns account + API key. |
|
|
90
|
+
|
|
91
|
+
### Examples
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Batch (<=100), synchronous
|
|
95
|
+
batch = client.verify_batch(
|
|
96
|
+
["a@example.com", "b@example.com"],
|
|
97
|
+
exclude_role_accounts=True,
|
|
98
|
+
)
|
|
99
|
+
for item in batch["results"]:
|
|
100
|
+
print(item["email"], item["result"])
|
|
101
|
+
|
|
102
|
+
# List hygiene without spending credits
|
|
103
|
+
print(client.clean(["A@Example.com ", "a@example.com", "bad"])["..."])
|
|
104
|
+
print(client.extract("contact us at sales@acme.io or ceo@acme.io"))
|
|
105
|
+
|
|
106
|
+
# Async bulk + polling
|
|
107
|
+
job = client.submit_bulk(emails=[...], webhook_url="https://you/webhook")
|
|
108
|
+
status = client.job(job["job_id"] if "job_id" in job else job["job"]["id"])
|
|
109
|
+
results = client.job_results(job_id)
|
|
110
|
+
|
|
111
|
+
# Account / billing
|
|
112
|
+
print(client.credits())
|
|
113
|
+
print(client.packages())
|
|
114
|
+
checkout = client.buy_credits("pro", method="stripe")
|
|
115
|
+
checkout = client.buy_credits("pro", method="crypto", currency="USDT")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Errors
|
|
119
|
+
|
|
120
|
+
API error envelopes (`{"success": false, "error": {...}}`) and non-2xx
|
|
121
|
+
responses raise `VeriflyError`:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
try:
|
|
125
|
+
client.verify("nope")
|
|
126
|
+
except VeriflyError as e:
|
|
127
|
+
e.code # e.g. "invalid_email", "insufficient_credits", "rate_limit_exceeded"
|
|
128
|
+
e.message
|
|
129
|
+
e.request_id # from the x-request-id response header
|
|
130
|
+
e.status # HTTP status
|
|
131
|
+
e.suggestion # optional remediation hint
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Retries & idempotency
|
|
135
|
+
|
|
136
|
+
- `429` and `5xx` responses are retried (default 3 times) with exponential
|
|
137
|
+
backoff, honoring the `Retry-After` header. Configure via
|
|
138
|
+
`VeriflyClient(..., max_retries=N, timeout=seconds)`.
|
|
139
|
+
- `buy_credits` and `submit_bulk` send an `Idempotency-Key` header. One is
|
|
140
|
+
auto-generated per call; pass your own with `idempotency_key=...` to make a
|
|
141
|
+
specific retry safe end-to-end.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
verifly_email/__init__.py
|
|
4
|
+
verifly_email/client.py
|
|
5
|
+
verifly_email/py.typed
|
|
6
|
+
verifly_email.egg-info/PKG-INFO
|
|
7
|
+
verifly_email.egg-info/SOURCES.txt
|
|
8
|
+
verifly_email.egg-info/dependency_links.txt
|
|
9
|
+
verifly_email.egg-info/requires.txt
|
|
10
|
+
verifly_email.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
verifly_email
|