veqen 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.
- veqen-0.1.0/LICENSE +21 -0
- veqen-0.1.0/PKG-INFO +50 -0
- veqen-0.1.0/README.md +30 -0
- veqen-0.1.0/pyproject.toml +28 -0
- veqen-0.1.0/setup.cfg +4 -0
- veqen-0.1.0/tests/test_client.py +90 -0
- veqen-0.1.0/veqen/__init__.py +143 -0
- veqen-0.1.0/veqen.egg-info/PKG-INFO +50 -0
- veqen-0.1.0/veqen.egg-info/SOURCES.txt +10 -0
- veqen-0.1.0/veqen.egg-info/dependency_links.txt +1 -0
- veqen-0.1.0/veqen.egg-info/requires.txt +2 -0
- veqen-0.1.0/veqen.egg-info/top_level.txt +1 -0
veqen-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Keyveve, Inc.
|
|
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.
|
veqen-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veqen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://veqen-app.com
|
|
7
|
+
Project-URL: Documentation, https://veqen-app.com/docs
|
|
8
|
+
Project-URL: Source, https://github.com/Keyveve/veqen-sdks/tree/main/python
|
|
9
|
+
Project-URL: Issues, https://github.com/Keyveve/veqen-sdks/issues
|
|
10
|
+
Keywords: email validation,disposable email,fraud,signup,ip lookup,phone validation,vpn detection
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.25
|
|
18
|
+
Requires-Dist: certifi>=2026.10.19
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# veqen (Python)
|
|
22
|
+
|
|
23
|
+
Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from veqen import Veqen, VeqenError
|
|
31
|
+
|
|
32
|
+
client = Veqen() # reads VEQEN_API_KEY from the environment
|
|
33
|
+
|
|
34
|
+
result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
|
|
35
|
+
print(result["risk"]["verdict"]) # "refuse"
|
|
36
|
+
for reason in result["risk"]["reasons"]:
|
|
37
|
+
print(reason["code"], reason["message"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Method | Endpoint | Credits |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
|
|
43
|
+
| `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
|
|
44
|
+
| `ip(address)` | `/v1/ip/check` | 1 |
|
|
45
|
+
| `phone(number, country=None)` | `/v1/phone/check` | 1 |
|
|
46
|
+
| `domain(name)` | `/v1/domain/check` | 1 |
|
|
47
|
+
| `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
|
|
48
|
+
| `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
|
|
49
|
+
|
|
50
|
+
Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
|
veqen-0.1.0/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# veqen (Python)
|
|
2
|
+
|
|
3
|
+
Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from veqen import Veqen, VeqenError
|
|
11
|
+
|
|
12
|
+
client = Veqen() # reads VEQEN_API_KEY from the environment
|
|
13
|
+
|
|
14
|
+
result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
|
|
15
|
+
print(result["risk"]["verdict"]) # "refuse"
|
|
16
|
+
for reason in result["risk"]["reasons"]:
|
|
17
|
+
print(reason["code"], reason["message"])
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Method | Endpoint | Credits |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
|
|
23
|
+
| `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
|
|
24
|
+
| `ip(address)` | `/v1/ip/check` | 1 |
|
|
25
|
+
| `phone(number, country=None)` | `/v1/phone/check` | 1 |
|
|
26
|
+
| `domain(name)` | `/v1/domain/check` | 1 |
|
|
27
|
+
| `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
|
|
28
|
+
| `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
|
|
29
|
+
|
|
30
|
+
Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "veqen"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
dependencies = ["httpx>=0.25", "certifi>=2026.10.19"]
|
|
10
|
+
keywords = ["email validation", "disposable email", "fraud", "signup", "ip lookup", "phone validation", "vpn detection"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Security",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://veqen-app.com"
|
|
19
|
+
Documentation = "https://veqen-app.com/docs"
|
|
20
|
+
Source = "https://github.com/Keyveve/veqen-sdks/tree/main/python"
|
|
21
|
+
Issues = "https://github.com/Keyveve/veqen-sdks/issues"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=77"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
packages = ["veqen"]
|
veqen-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from veqen import Veqen, VeqenError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make(handler, **kwargs):
|
|
10
|
+
return Veqen("vq_live_test", base_url="https://api.test", transport=httpx.MockTransport(handler), **kwargs)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_signup_sends_auth_and_drops_empty_fields():
|
|
14
|
+
seen = {}
|
|
15
|
+
|
|
16
|
+
def handler(request):
|
|
17
|
+
seen["auth"] = request.headers["Authorization"]
|
|
18
|
+
seen["path"] = request.url.path
|
|
19
|
+
seen["body"] = json.loads(request.content)
|
|
20
|
+
return httpx.Response(200, json={"risk": {"verdict": "refuse"}})
|
|
21
|
+
|
|
22
|
+
result = make(handler).signup(email="a@b.com", ip="1.2.3.4")
|
|
23
|
+
assert result["risk"]["verdict"] == "refuse"
|
|
24
|
+
assert seen == {"auth": "Bearer vq_live_test", "path": "/v1/signup/check", "body": {"email": "a@b.com", "ip": "1.2.3.4", "verify_mailbox": False}}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_errors_raise_with_type():
|
|
28
|
+
def handler(request):
|
|
29
|
+
return httpx.Response(402, json={"error": {"type": "insufficient_credits", "message": "Buy credits"}}, headers={"X-Request-Id": "r1"})
|
|
30
|
+
|
|
31
|
+
with pytest.raises(VeqenError) as info:
|
|
32
|
+
make(handler).email("a@b.com")
|
|
33
|
+
assert info.value.status == 402 and info.value.type == "insufficient_credits" and info.value.request_id == "r1"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_retries_on_429(monkeypatch):
|
|
37
|
+
monkeypatch.setattr("time.sleep", lambda s: None)
|
|
38
|
+
calls = []
|
|
39
|
+
|
|
40
|
+
def handler(request):
|
|
41
|
+
calls.append(1)
|
|
42
|
+
if len(calls) < 2:
|
|
43
|
+
return httpx.Response(429, json={"error": {"type": "rate_limited", "message": "slow"}}, headers={"Retry-After": "1"})
|
|
44
|
+
return httpx.Response(200, json={"ok": True})
|
|
45
|
+
|
|
46
|
+
assert make(handler).ip("1.2.3.4") == {"ok": True}
|
|
47
|
+
assert len(calls) == 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_batch_chunks_by_100():
|
|
51
|
+
sizes = []
|
|
52
|
+
|
|
53
|
+
def handler(request):
|
|
54
|
+
emails = json.loads(request.content)["emails"]
|
|
55
|
+
sizes.append(len(emails))
|
|
56
|
+
return httpx.Response(200, json={"results": [{"email": e} for e in emails], "count": len(emails)})
|
|
57
|
+
|
|
58
|
+
results = make(handler).emails([f"u{i}@a.com" for i in range(250)])
|
|
59
|
+
assert sizes == [100, 100, 50] and len(results) == 250
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_idempotency_header():
|
|
63
|
+
def handler(request):
|
|
64
|
+
assert request.headers["Idempotency-Key"] == "k1"
|
|
65
|
+
return httpx.Response(200, json={})
|
|
66
|
+
|
|
67
|
+
make(handler).phone("+447400123456", idempotency_key="k1")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_requires_key(monkeypatch):
|
|
71
|
+
monkeypatch.delenv("VEQEN_API_KEY", raising=False)
|
|
72
|
+
with pytest.raises(ValueError):
|
|
73
|
+
Veqen()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_retry_preserves_automatic_idempotency_key(monkeypatch):
|
|
77
|
+
monkeypatch.setattr("time.sleep", lambda s: None)
|
|
78
|
+
keys = []
|
|
79
|
+
|
|
80
|
+
def handler(request):
|
|
81
|
+
keys.append(request.headers["Idempotency-Key"])
|
|
82
|
+
if len(keys) == 1:
|
|
83
|
+
raise httpx.ReadTimeout("Response lost after server charged", request=request)
|
|
84
|
+
return httpx.Response(200, json={})
|
|
85
|
+
|
|
86
|
+
client = make(handler)
|
|
87
|
+
client.ip("1.2.3.4")
|
|
88
|
+
client.ip("1.2.3.4")
|
|
89
|
+
assert keys[0] == keys[1]
|
|
90
|
+
assert keys[1] != keys[2]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Official Python client for the Veqen signup-check API.
|
|
2
|
+
|
|
3
|
+
from veqen import Veqen
|
|
4
|
+
|
|
5
|
+
client = Veqen() # reads VEQEN_API_KEY
|
|
6
|
+
result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
|
|
7
|
+
print(result["risk"]["verdict"])
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
__all__ = ["Veqen", "VeqenError"]
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
DEFAULT_BASE_URL = "https://veqen-app.com"
|
|
23
|
+
RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VeqenError(Exception):
|
|
27
|
+
"""Raised for any non-2xx response. ``type`` matches the API's error.type."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, status: int, type_: str, message: str, request_id: str | None = None):
|
|
30
|
+
super().__init__(f"{status} {type_}: {message}")
|
|
31
|
+
self.status = status
|
|
32
|
+
self.type = type_
|
|
33
|
+
self.message = message
|
|
34
|
+
self.request_id = request_id
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Veqen:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
*,
|
|
42
|
+
base_url: str | None = None,
|
|
43
|
+
timeout: float = 10.0,
|
|
44
|
+
max_retries: int = 2,
|
|
45
|
+
transport: httpx.BaseTransport | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
key = api_key or os.environ.get("VEQEN_API_KEY")
|
|
48
|
+
if not key:
|
|
49
|
+
raise ValueError("Pass api_key or set VEQEN_API_KEY.")
|
|
50
|
+
self.max_retries = max_retries
|
|
51
|
+
self._client = httpx.Client(
|
|
52
|
+
base_url=(base_url or os.environ.get("VEQEN_BASE_URL") or DEFAULT_BASE_URL).rstrip("/"),
|
|
53
|
+
headers={"Authorization": f"Bearer {key}", "User-Agent": f"veqen-python/{__version__}"},
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
transport=transport,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# --- checks ----------------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def email(self, email: str, *, verify_mailbox: bool = False, idempotency_key: str | None = None) -> dict:
|
|
61
|
+
return self._post("/v1/email/check", {"email": email, "verify_mailbox": verify_mailbox}, idempotency_key)
|
|
62
|
+
|
|
63
|
+
def emails(self, emails: list[str], *, verify_mailbox: bool = False) -> list[dict]:
|
|
64
|
+
results: list[dict] = []
|
|
65
|
+
for start in range(0, len(emails), 100):
|
|
66
|
+
chunk = emails[start : start + 100]
|
|
67
|
+
results.extend(self._post("/v1/email/batch", {"emails": chunk, "verify_mailbox": verify_mailbox})["results"])
|
|
68
|
+
return results
|
|
69
|
+
|
|
70
|
+
def ip(self, ip: str, *, idempotency_key: str | None = None) -> dict:
|
|
71
|
+
return self._post("/v1/ip/check", {"ip": ip}, idempotency_key)
|
|
72
|
+
|
|
73
|
+
def phone(self, phone: str, country: str | None = None, *, idempotency_key: str | None = None) -> dict:
|
|
74
|
+
return self._post("/v1/phone/check", {"phone": phone, "country": country}, idempotency_key)
|
|
75
|
+
|
|
76
|
+
def domain(self, domain: str, *, idempotency_key: str | None = None) -> dict:
|
|
77
|
+
return self._post("/v1/domain/check", {"domain": domain}, idempotency_key)
|
|
78
|
+
|
|
79
|
+
def signup(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
email: str | None = None,
|
|
83
|
+
ip: str | None = None,
|
|
84
|
+
phone: str | None = None,
|
|
85
|
+
phone_country: str | None = None,
|
|
86
|
+
verify_mailbox: bool = False,
|
|
87
|
+
idempotency_key: str | None = None,
|
|
88
|
+
) -> dict:
|
|
89
|
+
body = {"email": email, "ip": ip, "phone": phone, "phone_country": phone_country, "verify_mailbox": verify_mailbox}
|
|
90
|
+
return self._post("/v1/signup/check", {k: v for k, v in body.items() if v not in (None, "")}, idempotency_key)
|
|
91
|
+
|
|
92
|
+
# --- account ---------------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def account(self) -> dict:
|
|
95
|
+
return self._request("GET", "/v1/account")
|
|
96
|
+
|
|
97
|
+
def usage(self, days: int = 30) -> dict:
|
|
98
|
+
return self._request("GET", "/v1/usage", params={"days": days})
|
|
99
|
+
|
|
100
|
+
# --- plumbing --------------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _post(self, path: str, body: dict, idempotency_key: str | None = None) -> Any:
|
|
103
|
+
# Reuse one key across transport/5xx retries, but generate a fresh key per logical call.
|
|
104
|
+
headers = {"Idempotency-Key": idempotency_key or str(uuid.uuid4())}
|
|
105
|
+
return self._request("POST", path, json=body, headers=headers)
|
|
106
|
+
|
|
107
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
108
|
+
attempt = 0
|
|
109
|
+
while True:
|
|
110
|
+
try:
|
|
111
|
+
response = self._client.request(method, path, **kwargs)
|
|
112
|
+
except httpx.TransportError:
|
|
113
|
+
if attempt >= self.max_retries:
|
|
114
|
+
raise
|
|
115
|
+
attempt += 1
|
|
116
|
+
time.sleep(min(2**attempt * 0.25, 4))
|
|
117
|
+
continue
|
|
118
|
+
if response.status_code in RETRY_STATUSES and attempt < self.max_retries:
|
|
119
|
+
attempt += 1
|
|
120
|
+
retry_after = response.headers.get("Retry-After")
|
|
121
|
+
time.sleep(float(retry_after) if retry_after and retry_after.isdigit() else min(2**attempt * 0.25, 4))
|
|
122
|
+
continue
|
|
123
|
+
if response.status_code >= 400:
|
|
124
|
+
try:
|
|
125
|
+
error = response.json().get("error", {})
|
|
126
|
+
except ValueError:
|
|
127
|
+
error = {}
|
|
128
|
+
raise VeqenError(
|
|
129
|
+
response.status_code,
|
|
130
|
+
error.get("type", "http_error"),
|
|
131
|
+
error.get("message", response.text[:200]),
|
|
132
|
+
response.headers.get("X-Request-Id"),
|
|
133
|
+
)
|
|
134
|
+
return response.json()
|
|
135
|
+
|
|
136
|
+
def close(self) -> None:
|
|
137
|
+
self._client.close()
|
|
138
|
+
|
|
139
|
+
def __enter__(self) -> Veqen:
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def __exit__(self, *exc: object) -> None:
|
|
143
|
+
self.close()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veqen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Veqen signup-check API: disposable emails, datacenter IPs, VoIP numbers and more.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://veqen-app.com
|
|
7
|
+
Project-URL: Documentation, https://veqen-app.com/docs
|
|
8
|
+
Project-URL: Source, https://github.com/Keyveve/veqen-sdks/tree/main/python
|
|
9
|
+
Project-URL: Issues, https://github.com/Keyveve/veqen-sdks/issues
|
|
10
|
+
Keywords: email validation,disposable email,fraud,signup,ip lookup,phone validation,vpn detection
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Security
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.25
|
|
18
|
+
Requires-Dist: certifi>=2026.10.19
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# veqen (Python)
|
|
22
|
+
|
|
23
|
+
Official Python client for [Veqen](https://veqen-app.com): catch disposable emails, typos, datacenter and Tor IPs, and VoIP numbers with one API call.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install https://veqen-app.com/static/sdk/veqen-0.1.0-py3-none-any.whl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from veqen import Veqen, VeqenError
|
|
31
|
+
|
|
32
|
+
client = Veqen() # reads VEQEN_API_KEY from the environment
|
|
33
|
+
|
|
34
|
+
result = client.signup(email="jane@mailinator.com", ip="185.220.101.1")
|
|
35
|
+
print(result["risk"]["verdict"]) # "refuse"
|
|
36
|
+
for reason in result["risk"]["reasons"]:
|
|
37
|
+
print(reason["code"], reason["message"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Method | Endpoint | Credits |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `email(address, verify_mailbox=False)` | `/v1/email/check` | 1 (+5) |
|
|
43
|
+
| `emails(list)` | `/v1/email/batch` (chunks of 100) | 1 each |
|
|
44
|
+
| `ip(address)` | `/v1/ip/check` | 1 |
|
|
45
|
+
| `phone(number, country=None)` | `/v1/phone/check` | 1 |
|
|
46
|
+
| `domain(name)` | `/v1/domain/check` | 1 |
|
|
47
|
+
| `signup(email=, ip=, phone=, phone_country=)` | `/v1/signup/check` | 3 |
|
|
48
|
+
| `account()`, `usage(days=30)` | `/v1/account`, `/v1/usage` | 0 |
|
|
49
|
+
|
|
50
|
+
Errors raise `VeqenError` with `.status`, `.type` (e.g. `insufficient_credits`) and `.message`. Requests are retried on 429 and 5xx. Docs: https://veqen-app.com/docs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
veqen
|