voicetel-api 2.2.10__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.
- voicetel_api-2.2.10/LICENSE +21 -0
- voicetel_api-2.2.10/PKG-INFO +300 -0
- voicetel_api-2.2.10/README.md +265 -0
- voicetel_api-2.2.10/pyproject.toml +86 -0
- voicetel_api-2.2.10/src/voicetel/__init__.py +46 -0
- voicetel_api-2.2.10/src/voicetel/_http.py +265 -0
- voicetel_api-2.2.10/src/voicetel/_version.py +1 -0
- voicetel_api-2.2.10/src/voicetel/auth.py +57 -0
- voicetel_api-2.2.10/src/voicetel/client.py +154 -0
- voicetel_api-2.2.10/src/voicetel/exceptions.py +88 -0
- voicetel_api-2.2.10/src/voicetel/models/__init__.py +352 -0
- voicetel_api-2.2.10/src/voicetel/models/_base.py +37 -0
- voicetel_api-2.2.10/src/voicetel/models/account.py +254 -0
- voicetel_api-2.2.10/src/voicetel/models/acl.py +66 -0
- voicetel_api-2.2.10/src/voicetel/models/authentication.py +69 -0
- voicetel_api-2.2.10/src/voicetel/models/common.py +34 -0
- voicetel_api-2.2.10/src/voicetel/models/e911.py +97 -0
- voicetel_api-2.2.10/src/voicetel/models/gateways.py +84 -0
- voicetel_api-2.2.10/src/voicetel/models/inumbering.py +241 -0
- voicetel_api-2.2.10/src/voicetel/models/lookups.py +43 -0
- voicetel_api-2.2.10/src/voicetel/models/messaging.py +164 -0
- voicetel_api-2.2.10/src/voicetel/models/numbers.py +327 -0
- voicetel_api-2.2.10/src/voicetel/models/support.py +265 -0
- voicetel_api-2.2.10/src/voicetel/py.typed +0 -0
- voicetel_api-2.2.10/src/voicetel/resources/__init__.py +38 -0
- voicetel_api-2.2.10/src/voicetel/resources/_base.py +29 -0
- voicetel_api-2.2.10/src/voicetel/resources/account.py +148 -0
- voicetel_api-2.2.10/src/voicetel/resources/acl.py +36 -0
- voicetel_api-2.2.10/src/voicetel/resources/authentication.py +26 -0
- voicetel_api-2.2.10/src/voicetel/resources/e911.py +72 -0
- voicetel_api-2.2.10/src/voicetel/resources/gateways.py +77 -0
- voicetel_api-2.2.10/src/voicetel/resources/inumbering.py +152 -0
- voicetel_api-2.2.10/src/voicetel/resources/lookups.py +29 -0
- voicetel_api-2.2.10/src/voicetel/resources/messaging.py +132 -0
- voicetel_api-2.2.10/src/voicetel/resources/numbers.py +394 -0
- voicetel_api-2.2.10/src/voicetel/resources/support.py +108 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VoiceTel Communications
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voicetel-api
|
|
3
|
+
Version: 2.2.10
|
|
4
|
+
Summary: Official Python SDK for the VoiceTel REST API
|
|
5
|
+
Project-URL: Homepage, https://github.com/voicetel/python-sdk
|
|
6
|
+
Project-URL: Documentation, https://voicetel.com/docs/api/v2.2/
|
|
7
|
+
Project-URL: Repository, https://github.com/voicetel/python-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/voicetel/python-sdk/issues
|
|
9
|
+
Author-email: VoiceTel <support@voicetel.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: e911,sip,sms,telephony,voicetel,voip
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Communications :: Telephony
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pydantic>=2.6
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# 📞 VoiceTel Python SDK
|
|
37
|
+
|
|
38
|
+
The official Python client for the [VoiceTel REST API](https://voicetel.com/docs/api/v2.2/) — provision numbers, place orders, validate e911, send messages, and manage your account, all with type-safe, async-ready Python.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
## 📚 Table of Contents
|
|
47
|
+
|
|
48
|
+
- [Features](#-features)
|
|
49
|
+
- [Installation](#-installation)
|
|
50
|
+
- [Quickstart](#-quickstart)
|
|
51
|
+
- [Authentication](#-authentication)
|
|
52
|
+
- [Resource Reference](#-resource-reference)
|
|
53
|
+
- [Error Handling](#-error-handling)
|
|
54
|
+
- [Async Support](#-async-support)
|
|
55
|
+
- [Rate Limits](#-rate-limits)
|
|
56
|
+
- [Development](#-development)
|
|
57
|
+
- [API Documentation](#-api-documentation)
|
|
58
|
+
- [Contributors](#-contributors)
|
|
59
|
+
- [Sponsors](#-sponsors)
|
|
60
|
+
- [License](#-license)
|
|
61
|
+
|
|
62
|
+
## ✨ Features
|
|
63
|
+
|
|
64
|
+
### 🛡️ Strongly Typed End-to-End
|
|
65
|
+
- **Pydantic v2 models** for every one of the 73 API operations — request bodies validated before they leave your machine, responses validated when they arrive.
|
|
66
|
+
- **mypy-strict clean.** Full type coverage, including async, generics, and discriminated unions.
|
|
67
|
+
- **Autocomplete everywhere.** Your IDE knows the shape of every field — no more guessing what's in `result["data"]["numbers"]`.
|
|
68
|
+
|
|
69
|
+
### ⚡ Sync + Async, Same Surface
|
|
70
|
+
- `Client` for blocking calls, `AsyncClient` for `await`-based async — identical method names, identical return types.
|
|
71
|
+
- Built on `httpx` — supports HTTP/2, connection pooling, and custom transports if you need them.
|
|
72
|
+
|
|
73
|
+
### 🔁 Production-Grade Transport
|
|
74
|
+
- **Automatic retry** with exponential backoff on 429 / 5xx — honors `Retry-After` headers.
|
|
75
|
+
- **Configurable timeouts** per client or per call.
|
|
76
|
+
- **Bearer auth** managed for you; password→key exchange handled by `client.login()`.
|
|
77
|
+
- **Structured exception hierarchy** — `RateLimitError`, `AuthenticationError`, `NotFoundError`, etc. all subclasses of `ApiError` you can catch broadly or narrowly.
|
|
78
|
+
|
|
79
|
+
### 📞 Complete API Coverage
|
|
80
|
+
- **Numbers** — list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
|
|
81
|
+
- **Account** — profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
|
|
82
|
+
- **e911** — record provisioning, address validation, lookup, removal.
|
|
83
|
+
- **Gateways** — list, create, update, delete, view bound numbers.
|
|
84
|
+
- **Messaging** — SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
|
|
85
|
+
- **Lookups** — CNAM and LRN dips.
|
|
86
|
+
- **iNumbering** — inventory search, coverage queries, number orders, port-in submissions, port-out availability checks.
|
|
87
|
+
- **Support** — ticket create / read / update / delete, threaded messages, replies.
|
|
88
|
+
- **ACL** — IP allowlist management with structured 409 conflict bodies.
|
|
89
|
+
- **Authentication** — switch between Digest, IP-only, or hybrid modes; rotate passwords.
|
|
90
|
+
|
|
91
|
+
### 🧪 Battle-Tested
|
|
92
|
+
- **108 unit tests** at **100% statement + branch coverage.**
|
|
93
|
+
- **Integration test suite** that runs read-only operations against `api.voicetel.com` — gated by env vars, safe for CI.
|
|
94
|
+
- **No mocks-pretending-to-be-tests.** Mocked HTTP layer with `respx`, real Pydantic validation on every fixture so spec drift gets caught.
|
|
95
|
+
|
|
96
|
+
### 📦 Clean Distribution
|
|
97
|
+
- Zero codegen footprint — every byte hand-written.
|
|
98
|
+
- Built with `hatchling`; ships as wheel + sdist.
|
|
99
|
+
- `py.typed` marker — downstream type checkers see your imports natively.
|
|
100
|
+
|
|
101
|
+
## 🚀 Installation
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install voicetel-api
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Requires Python 3.10 or later. Tested against 3.10, 3.11, 3.12, and 3.13.
|
|
108
|
+
|
|
109
|
+
## 🏁 Quickstart
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from voicetel import Client
|
|
113
|
+
|
|
114
|
+
with Client() as c:
|
|
115
|
+
# Exchange username + password for an API key (one-time per session)
|
|
116
|
+
c.login(username=1000000001, password="hunter2")
|
|
117
|
+
|
|
118
|
+
# Typed responses — your IDE knows what `me` is.
|
|
119
|
+
me = c.account.get()
|
|
120
|
+
print(f"Balance: ${me.cash:.2f} | Caller ID: {me.callerId}")
|
|
121
|
+
|
|
122
|
+
# List your numbers
|
|
123
|
+
for n in c.numbers.list().numbers:
|
|
124
|
+
print(f"{n.number} route={n.route} cnam={n.cnam} sms={n.smsEnabled}")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or, if you already have an API key:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from voicetel import Client
|
|
131
|
+
|
|
132
|
+
with Client(api_key="32hex...") as c:
|
|
133
|
+
coverage = c.inumbering.coverage(state="NJ")
|
|
134
|
+
for bucket in coverage.coverage:
|
|
135
|
+
print(f"{bucket.npa}-{bucket.nxx}: {bucket.count} TNs available")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 🔑 Authentication
|
|
139
|
+
|
|
140
|
+
Every endpoint requires `Authorization: Bearer <apikey>` **except** `POST /v2.2/account/api-key`, which exchanges username + password for a fresh key. `Client.login()` (and `AsyncClient.login()`) handles the exchange and installs the returned key on the transport.
|
|
141
|
+
|
|
142
|
+
Re-fetch the API key after any password change — the old one is invalidated.
|
|
143
|
+
|
|
144
|
+
> Don't have credentials yet? Get them at **[voicetel.com/docs/api/v2.2/credentials](https://voicetel.com/docs/api/v2.2/credentials/)**.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from voicetel import Client
|
|
148
|
+
|
|
149
|
+
with Client() as c:
|
|
150
|
+
key = c.login(username=1000000001, password="hunter2")
|
|
151
|
+
# `key` is the new 32-hex bearer; the client already has it installed.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 🗺️ Resource Reference
|
|
155
|
+
|
|
156
|
+
| Resource | Operations | Example |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `client.account` | Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | `c.account.cdr(start=t1, end=t2)` |
|
|
159
|
+
| `client.acl` | IP allowlist (CIDR entries) | `c.acl.add(AclModifyRequest(acl=[...]))` |
|
|
160
|
+
| `client.authentication` | SIP/HTTP auth mode + password | `c.authentication.update(AuthPutRequest(authType=1))` |
|
|
161
|
+
| `client.e911` | Records, address validation, provisioning | `c.e911.validate(E911AddressRequest(...))` |
|
|
162
|
+
| `client.gateways` | Termination routes | `c.gateways.list()` |
|
|
163
|
+
| `client.inumbering` | Inventory, orders, port-ins | `c.inumbering.search_inventory(npa=201)` |
|
|
164
|
+
| `client.lookups` | CNAM & LRN dips | `c.lookups.lrn("2015551234", ani="2012548000")` |
|
|
165
|
+
| `client.messaging` | SMS/MMS, 10DLC brands & campaigns | `c.messaging.send(MessageSendRequest(...))` |
|
|
166
|
+
| `client.numbers` | All operations on TNs on the account | `c.numbers.assign_campaign("2015551234", ...)` |
|
|
167
|
+
| `client.support` | Tickets, replies, attachments | `c.support.create(TicketCreateRequest(...))` |
|
|
168
|
+
|
|
169
|
+
Every method that takes a request body accepts a typed Pydantic model imported from `voicetel.models`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from voicetel import Client
|
|
173
|
+
from voicetel.models import (
|
|
174
|
+
MessageSendRequest,
|
|
175
|
+
NumberCampaignAssignRequest,
|
|
176
|
+
PortSubmitRequest,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
with Client(api_key=key) as c:
|
|
180
|
+
sent = c.messaging.send(MessageSendRequest(
|
|
181
|
+
fromNumber="2012548000",
|
|
182
|
+
toNumber="2015551234",
|
|
183
|
+
text="Your code is 482917",
|
|
184
|
+
))
|
|
185
|
+
print(f"Sent: {sent.id} ({sent.parts} segment(s))")
|
|
186
|
+
|
|
187
|
+
c.numbers.assign_campaign(
|
|
188
|
+
"2015551234",
|
|
189
|
+
NumberCampaignAssignRequest(campaignId="CABC123"),
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 🚨 Error Handling
|
|
194
|
+
|
|
195
|
+
All HTTP errors raise subclasses of `voicetel.ApiError`. Catch broadly or narrowly:
|
|
196
|
+
|
|
197
|
+
| Status | Exception |
|
|
198
|
+
|--------|------------------------|
|
|
199
|
+
| 400 | `BadRequestError` |
|
|
200
|
+
| 401 | `AuthenticationError` |
|
|
201
|
+
| 403 | `PermissionDeniedError`|
|
|
202
|
+
| 404 | `NotFoundError` |
|
|
203
|
+
| 409 | `ConflictError` |
|
|
204
|
+
| 429 | `RateLimitError` |
|
|
205
|
+
| 5xx | `ServerError` |
|
|
206
|
+
| other | `ApiError` |
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from voicetel import Client, NotFoundError, RateLimitError
|
|
210
|
+
|
|
211
|
+
with Client(api_key=key) as c:
|
|
212
|
+
try:
|
|
213
|
+
n = c.numbers.get("9999999999")
|
|
214
|
+
except NotFoundError:
|
|
215
|
+
print("That number isn't on your account.")
|
|
216
|
+
except RateLimitError as e:
|
|
217
|
+
print(f"Slow down — retry in {e.body.get('retryAfter', '?')}s")
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## ⚡ Async Support
|
|
221
|
+
|
|
222
|
+
Identical surface to `Client`, with `await`-based methods:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import asyncio
|
|
226
|
+
from voicetel import AsyncClient
|
|
227
|
+
|
|
228
|
+
async def fetch_state(numbers: list[str]) -> None:
|
|
229
|
+
async with AsyncClient(api_key="...") as c:
|
|
230
|
+
state = await c.messaging.numbers_state(numbers=numbers)
|
|
231
|
+
for s in state.numbers:
|
|
232
|
+
print(f"{s.number}: network={s.network} campaign={s.campaign}")
|
|
233
|
+
|
|
234
|
+
asyncio.run(fetch_state(["2015551234", "2015551235"]))
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## ⏱️ Rate Limits
|
|
238
|
+
|
|
239
|
+
These endpoints are limited to **6 requests per hour per IP**:
|
|
240
|
+
|
|
241
|
+
- `account/info`
|
|
242
|
+
- `account/mrc` (`client.account.recurring_charges()`)
|
|
243
|
+
- `account/cdr` (`client.account.cdr()`)
|
|
244
|
+
- `account/api-key` (`client.login()`)
|
|
245
|
+
|
|
246
|
+
The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To bump it:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
Client(api_key=key, max_retries=4, timeout=60.0)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 🛠️ Development
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
git clone https://github.com/voicetel/python-sdk
|
|
256
|
+
cd python-sdk
|
|
257
|
+
python -m venv .venv && source .venv/bin/activate
|
|
258
|
+
pip install -e ".[dev]"
|
|
259
|
+
|
|
260
|
+
# Unit tests (fast, no network)
|
|
261
|
+
pytest tests/unit
|
|
262
|
+
|
|
263
|
+
# 100% coverage gate
|
|
264
|
+
pytest tests/unit --cov --cov-fail-under=100
|
|
265
|
+
|
|
266
|
+
# Lint + type-check
|
|
267
|
+
ruff check src tests
|
|
268
|
+
mypy src
|
|
269
|
+
|
|
270
|
+
# Integration tests (live api.voicetel.com, read-only)
|
|
271
|
+
cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
|
|
272
|
+
pytest tests/integration
|
|
273
|
+
|
|
274
|
+
# Build wheel + sdist
|
|
275
|
+
python -m build
|
|
276
|
+
twine check dist/*
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## 📖 API Documentation
|
|
280
|
+
|
|
281
|
+
- **Reference docs:** [voicetel.com/docs/api/v2.2/](https://voicetel.com/docs/api/v2.2/)
|
|
282
|
+
- **Interactive playground:** [voicetel.com/docs/api/v2.2/playground/](https://voicetel.com/docs/api/v2.2/playground/) — try the API in your browser without writing any code
|
|
283
|
+
- **API credentials:** [voicetel.com/docs/api/v2.2/credentials/](https://voicetel.com/docs/api/v2.2/credentials/)
|
|
284
|
+
- **Type definitions:** see the `voicetel.models` module — every wire shape has a Pydantic model.
|
|
285
|
+
|
|
286
|
+
## 🙌 Contributors
|
|
287
|
+
|
|
288
|
+
- [Michael Mavroudis](https://github.com/mavroudis) — Lead Developer
|
|
289
|
+
|
|
290
|
+
Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
|
|
291
|
+
|
|
292
|
+
## 💖 Sponsors
|
|
293
|
+
|
|
294
|
+
| Sponsor | Contribution |
|
|
295
|
+
|---------|--------------|
|
|
296
|
+
| [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
|
|
297
|
+
|
|
298
|
+
## 📄 License
|
|
299
|
+
|
|
300
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# 📞 VoiceTel Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python client for the [VoiceTel REST API](https://voicetel.com/docs/api/v2.2/) — provision numbers, place orders, validate e911, send messages, and manage your account, all with type-safe, async-ready Python.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 📚 Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Features](#-features)
|
|
14
|
+
- [Installation](#-installation)
|
|
15
|
+
- [Quickstart](#-quickstart)
|
|
16
|
+
- [Authentication](#-authentication)
|
|
17
|
+
- [Resource Reference](#-resource-reference)
|
|
18
|
+
- [Error Handling](#-error-handling)
|
|
19
|
+
- [Async Support](#-async-support)
|
|
20
|
+
- [Rate Limits](#-rate-limits)
|
|
21
|
+
- [Development](#-development)
|
|
22
|
+
- [API Documentation](#-api-documentation)
|
|
23
|
+
- [Contributors](#-contributors)
|
|
24
|
+
- [Sponsors](#-sponsors)
|
|
25
|
+
- [License](#-license)
|
|
26
|
+
|
|
27
|
+
## ✨ Features
|
|
28
|
+
|
|
29
|
+
### 🛡️ Strongly Typed End-to-End
|
|
30
|
+
- **Pydantic v2 models** for every one of the 73 API operations — request bodies validated before they leave your machine, responses validated when they arrive.
|
|
31
|
+
- **mypy-strict clean.** Full type coverage, including async, generics, and discriminated unions.
|
|
32
|
+
- **Autocomplete everywhere.** Your IDE knows the shape of every field — no more guessing what's in `result["data"]["numbers"]`.
|
|
33
|
+
|
|
34
|
+
### ⚡ Sync + Async, Same Surface
|
|
35
|
+
- `Client` for blocking calls, `AsyncClient` for `await`-based async — identical method names, identical return types.
|
|
36
|
+
- Built on `httpx` — supports HTTP/2, connection pooling, and custom transports if you need them.
|
|
37
|
+
|
|
38
|
+
### 🔁 Production-Grade Transport
|
|
39
|
+
- **Automatic retry** with exponential backoff on 429 / 5xx — honors `Retry-After` headers.
|
|
40
|
+
- **Configurable timeouts** per client or per call.
|
|
41
|
+
- **Bearer auth** managed for you; password→key exchange handled by `client.login()`.
|
|
42
|
+
- **Structured exception hierarchy** — `RateLimitError`, `AuthenticationError`, `NotFoundError`, etc. all subclasses of `ApiError` you can catch broadly or narrowly.
|
|
43
|
+
|
|
44
|
+
### 📞 Complete API Coverage
|
|
45
|
+
- **Numbers** — list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
|
|
46
|
+
- **Account** — profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
|
|
47
|
+
- **e911** — record provisioning, address validation, lookup, removal.
|
|
48
|
+
- **Gateways** — list, create, update, delete, view bound numbers.
|
|
49
|
+
- **Messaging** — SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
|
|
50
|
+
- **Lookups** — CNAM and LRN dips.
|
|
51
|
+
- **iNumbering** — inventory search, coverage queries, number orders, port-in submissions, port-out availability checks.
|
|
52
|
+
- **Support** — ticket create / read / update / delete, threaded messages, replies.
|
|
53
|
+
- **ACL** — IP allowlist management with structured 409 conflict bodies.
|
|
54
|
+
- **Authentication** — switch between Digest, IP-only, or hybrid modes; rotate passwords.
|
|
55
|
+
|
|
56
|
+
### 🧪 Battle-Tested
|
|
57
|
+
- **108 unit tests** at **100% statement + branch coverage.**
|
|
58
|
+
- **Integration test suite** that runs read-only operations against `api.voicetel.com` — gated by env vars, safe for CI.
|
|
59
|
+
- **No mocks-pretending-to-be-tests.** Mocked HTTP layer with `respx`, real Pydantic validation on every fixture so spec drift gets caught.
|
|
60
|
+
|
|
61
|
+
### 📦 Clean Distribution
|
|
62
|
+
- Zero codegen footprint — every byte hand-written.
|
|
63
|
+
- Built with `hatchling`; ships as wheel + sdist.
|
|
64
|
+
- `py.typed` marker — downstream type checkers see your imports natively.
|
|
65
|
+
|
|
66
|
+
## 🚀 Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install voicetel-api
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Requires Python 3.10 or later. Tested against 3.10, 3.11, 3.12, and 3.13.
|
|
73
|
+
|
|
74
|
+
## 🏁 Quickstart
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from voicetel import Client
|
|
78
|
+
|
|
79
|
+
with Client() as c:
|
|
80
|
+
# Exchange username + password for an API key (one-time per session)
|
|
81
|
+
c.login(username=1000000001, password="hunter2")
|
|
82
|
+
|
|
83
|
+
# Typed responses — your IDE knows what `me` is.
|
|
84
|
+
me = c.account.get()
|
|
85
|
+
print(f"Balance: ${me.cash:.2f} | Caller ID: {me.callerId}")
|
|
86
|
+
|
|
87
|
+
# List your numbers
|
|
88
|
+
for n in c.numbers.list().numbers:
|
|
89
|
+
print(f"{n.number} route={n.route} cnam={n.cnam} sms={n.smsEnabled}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or, if you already have an API key:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from voicetel import Client
|
|
96
|
+
|
|
97
|
+
with Client(api_key="32hex...") as c:
|
|
98
|
+
coverage = c.inumbering.coverage(state="NJ")
|
|
99
|
+
for bucket in coverage.coverage:
|
|
100
|
+
print(f"{bucket.npa}-{bucket.nxx}: {bucket.count} TNs available")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 🔑 Authentication
|
|
104
|
+
|
|
105
|
+
Every endpoint requires `Authorization: Bearer <apikey>` **except** `POST /v2.2/account/api-key`, which exchanges username + password for a fresh key. `Client.login()` (and `AsyncClient.login()`) handles the exchange and installs the returned key on the transport.
|
|
106
|
+
|
|
107
|
+
Re-fetch the API key after any password change — the old one is invalidated.
|
|
108
|
+
|
|
109
|
+
> Don't have credentials yet? Get them at **[voicetel.com/docs/api/v2.2/credentials](https://voicetel.com/docs/api/v2.2/credentials/)**.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from voicetel import Client
|
|
113
|
+
|
|
114
|
+
with Client() as c:
|
|
115
|
+
key = c.login(username=1000000001, password="hunter2")
|
|
116
|
+
# `key` is the new 32-hex bearer; the client already has it installed.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 🗺️ Resource Reference
|
|
120
|
+
|
|
121
|
+
| Resource | Operations | Example |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `client.account` | Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | `c.account.cdr(start=t1, end=t2)` |
|
|
124
|
+
| `client.acl` | IP allowlist (CIDR entries) | `c.acl.add(AclModifyRequest(acl=[...]))` |
|
|
125
|
+
| `client.authentication` | SIP/HTTP auth mode + password | `c.authentication.update(AuthPutRequest(authType=1))` |
|
|
126
|
+
| `client.e911` | Records, address validation, provisioning | `c.e911.validate(E911AddressRequest(...))` |
|
|
127
|
+
| `client.gateways` | Termination routes | `c.gateways.list()` |
|
|
128
|
+
| `client.inumbering` | Inventory, orders, port-ins | `c.inumbering.search_inventory(npa=201)` |
|
|
129
|
+
| `client.lookups` | CNAM & LRN dips | `c.lookups.lrn("2015551234", ani="2012548000")` |
|
|
130
|
+
| `client.messaging` | SMS/MMS, 10DLC brands & campaigns | `c.messaging.send(MessageSendRequest(...))` |
|
|
131
|
+
| `client.numbers` | All operations on TNs on the account | `c.numbers.assign_campaign("2015551234", ...)` |
|
|
132
|
+
| `client.support` | Tickets, replies, attachments | `c.support.create(TicketCreateRequest(...))` |
|
|
133
|
+
|
|
134
|
+
Every method that takes a request body accepts a typed Pydantic model imported from `voicetel.models`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from voicetel import Client
|
|
138
|
+
from voicetel.models import (
|
|
139
|
+
MessageSendRequest,
|
|
140
|
+
NumberCampaignAssignRequest,
|
|
141
|
+
PortSubmitRequest,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
with Client(api_key=key) as c:
|
|
145
|
+
sent = c.messaging.send(MessageSendRequest(
|
|
146
|
+
fromNumber="2012548000",
|
|
147
|
+
toNumber="2015551234",
|
|
148
|
+
text="Your code is 482917",
|
|
149
|
+
))
|
|
150
|
+
print(f"Sent: {sent.id} ({sent.parts} segment(s))")
|
|
151
|
+
|
|
152
|
+
c.numbers.assign_campaign(
|
|
153
|
+
"2015551234",
|
|
154
|
+
NumberCampaignAssignRequest(campaignId="CABC123"),
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 🚨 Error Handling
|
|
159
|
+
|
|
160
|
+
All HTTP errors raise subclasses of `voicetel.ApiError`. Catch broadly or narrowly:
|
|
161
|
+
|
|
162
|
+
| Status | Exception |
|
|
163
|
+
|--------|------------------------|
|
|
164
|
+
| 400 | `BadRequestError` |
|
|
165
|
+
| 401 | `AuthenticationError` |
|
|
166
|
+
| 403 | `PermissionDeniedError`|
|
|
167
|
+
| 404 | `NotFoundError` |
|
|
168
|
+
| 409 | `ConflictError` |
|
|
169
|
+
| 429 | `RateLimitError` |
|
|
170
|
+
| 5xx | `ServerError` |
|
|
171
|
+
| other | `ApiError` |
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from voicetel import Client, NotFoundError, RateLimitError
|
|
175
|
+
|
|
176
|
+
with Client(api_key=key) as c:
|
|
177
|
+
try:
|
|
178
|
+
n = c.numbers.get("9999999999")
|
|
179
|
+
except NotFoundError:
|
|
180
|
+
print("That number isn't on your account.")
|
|
181
|
+
except RateLimitError as e:
|
|
182
|
+
print(f"Slow down — retry in {e.body.get('retryAfter', '?')}s")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## ⚡ Async Support
|
|
186
|
+
|
|
187
|
+
Identical surface to `Client`, with `await`-based methods:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import asyncio
|
|
191
|
+
from voicetel import AsyncClient
|
|
192
|
+
|
|
193
|
+
async def fetch_state(numbers: list[str]) -> None:
|
|
194
|
+
async with AsyncClient(api_key="...") as c:
|
|
195
|
+
state = await c.messaging.numbers_state(numbers=numbers)
|
|
196
|
+
for s in state.numbers:
|
|
197
|
+
print(f"{s.number}: network={s.network} campaign={s.campaign}")
|
|
198
|
+
|
|
199
|
+
asyncio.run(fetch_state(["2015551234", "2015551235"]))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## ⏱️ Rate Limits
|
|
203
|
+
|
|
204
|
+
These endpoints are limited to **6 requests per hour per IP**:
|
|
205
|
+
|
|
206
|
+
- `account/info`
|
|
207
|
+
- `account/mrc` (`client.account.recurring_charges()`)
|
|
208
|
+
- `account/cdr` (`client.account.cdr()`)
|
|
209
|
+
- `account/api-key` (`client.login()`)
|
|
210
|
+
|
|
211
|
+
The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To bump it:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
Client(api_key=key, max_retries=4, timeout=60.0)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 🛠️ Development
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git clone https://github.com/voicetel/python-sdk
|
|
221
|
+
cd python-sdk
|
|
222
|
+
python -m venv .venv && source .venv/bin/activate
|
|
223
|
+
pip install -e ".[dev]"
|
|
224
|
+
|
|
225
|
+
# Unit tests (fast, no network)
|
|
226
|
+
pytest tests/unit
|
|
227
|
+
|
|
228
|
+
# 100% coverage gate
|
|
229
|
+
pytest tests/unit --cov --cov-fail-under=100
|
|
230
|
+
|
|
231
|
+
# Lint + type-check
|
|
232
|
+
ruff check src tests
|
|
233
|
+
mypy src
|
|
234
|
+
|
|
235
|
+
# Integration tests (live api.voicetel.com, read-only)
|
|
236
|
+
cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
|
|
237
|
+
pytest tests/integration
|
|
238
|
+
|
|
239
|
+
# Build wheel + sdist
|
|
240
|
+
python -m build
|
|
241
|
+
twine check dist/*
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 📖 API Documentation
|
|
245
|
+
|
|
246
|
+
- **Reference docs:** [voicetel.com/docs/api/v2.2/](https://voicetel.com/docs/api/v2.2/)
|
|
247
|
+
- **Interactive playground:** [voicetel.com/docs/api/v2.2/playground/](https://voicetel.com/docs/api/v2.2/playground/) — try the API in your browser without writing any code
|
|
248
|
+
- **API credentials:** [voicetel.com/docs/api/v2.2/credentials/](https://voicetel.com/docs/api/v2.2/credentials/)
|
|
249
|
+
- **Type definitions:** see the `voicetel.models` module — every wire shape has a Pydantic model.
|
|
250
|
+
|
|
251
|
+
## 🙌 Contributors
|
|
252
|
+
|
|
253
|
+
- [Michael Mavroudis](https://github.com/mavroudis) — Lead Developer
|
|
254
|
+
|
|
255
|
+
Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
|
|
256
|
+
|
|
257
|
+
## 💖 Sponsors
|
|
258
|
+
|
|
259
|
+
| Sponsor | Contribution |
|
|
260
|
+
|---------|--------------|
|
|
261
|
+
| [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
|
|
262
|
+
|
|
263
|
+
## 📄 License
|
|
264
|
+
|
|
265
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "voicetel-api"
|
|
7
|
+
version = "2.2.10"
|
|
8
|
+
description = "Official Python SDK for the VoiceTel REST API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "VoiceTel", email = "support@voicetel.com" }]
|
|
13
|
+
keywords = ["voicetel", "telephony", "sip", "sms", "e911", "voip"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Communications :: Telephony",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"pydantic>=2.6",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/voicetel/python-sdk"
|
|
34
|
+
Documentation = "https://voicetel.com/docs/api/v2.2/"
|
|
35
|
+
Repository = "https://github.com/voicetel/python-sdk"
|
|
36
|
+
Issues = "https://github.com/voicetel/python-sdk/issues"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
"pytest-asyncio>=0.23",
|
|
42
|
+
"pytest-cov>=5.0",
|
|
43
|
+
"respx>=0.21",
|
|
44
|
+
"ruff>=0.5",
|
|
45
|
+
"mypy>=1.10",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/voicetel"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src/voicetel", "README.md", "LICENSE"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
markers = [
|
|
58
|
+
"integration: hits the live api.voicetel.com (requires VOICETEL_USERNAME and VOICETEL_PASSWORD env vars)",
|
|
59
|
+
]
|
|
60
|
+
addopts = "-ra --strict-markers"
|
|
61
|
+
|
|
62
|
+
[tool.coverage.run]
|
|
63
|
+
source = ["src/voicetel"]
|
|
64
|
+
branch = true
|
|
65
|
+
|
|
66
|
+
[tool.coverage.report]
|
|
67
|
+
exclude_lines = [
|
|
68
|
+
"pragma: no cover",
|
|
69
|
+
"if TYPE_CHECKING:",
|
|
70
|
+
"raise NotImplementedError",
|
|
71
|
+
"\\.\\.\\.",
|
|
72
|
+
]
|
|
73
|
+
show_missing = true
|
|
74
|
+
skip_covered = false
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 110
|
|
78
|
+
target-version = "py310"
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
82
|
+
|
|
83
|
+
[tool.mypy]
|
|
84
|
+
python_version = "3.10"
|
|
85
|
+
strict = true
|
|
86
|
+
files = ["src/voicetel"]
|