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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+
2
+ [dev]
3
+ pytest>=7
@@ -0,0 +1 @@
1
+ verifly_email