The official Python client for the VoiceTel REST API β provision numbers, place orders, validate e911, send messages, and manage your account, all with type-safe, async-ready Python.
- Features
- Installation
- Quickstart
- Authentication
- Resource Reference
- Error Handling
- Async Support
- Rate Limits
- Development
- API Documentation
- Contributors
- Sponsors
- License
- Pydantic v2 models for every one of the 73 API operations β request bodies validated before they leave your machine, responses validated when they arrive.
- mypy-strict clean. Full type coverage, including async, generics, and discriminated unions.
- Autocomplete everywhere. Your IDE knows the shape of every field β no more guessing what's in
result["data"]["numbers"].
Clientfor blocking calls,AsyncClientforawait-based async β identical method names, identical return types.- Built on
httpxβ supports HTTP/2, connection pooling, and custom transports if you need them.
- Automatic retry with exponential backoff on 429 / 5xx β honors
Retry-Afterheaders. - Configurable timeouts per client or per call.
- Bearer auth managed for you; passwordβkey exchange handled by
client.login(). - Structured exception hierarchy β
RateLimitError,AuthenticationError,NotFoundError, etc. all subclasses ofApiErroryou can catch broadly or narrowly.
- Numbers β list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
- Account β profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
- e911 β record provisioning, address validation, lookup, removal.
- Gateways β list, create, update, delete, view bound numbers.
- Messaging β SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
- Lookups β CNAM and LRN dips.
- iNumbering β inventory search, coverage queries, number orders, port-in submissions, port-out availability checks.
- Support β ticket create / read / update / delete, threaded messages, replies.
- ACL β IP allowlist management with structured 409 conflict bodies.
- Authentication β switch between Digest, IP-only, or hybrid modes; rotate passwords.
- 108 unit tests at 100% statement + branch coverage.
- Integration test suite that runs read-only operations against
api.voicetel.comβ gated by env vars, safe for CI. - No mocks-pretending-to-be-tests. Mocked HTTP layer with
respx, real Pydantic validation on every fixture so spec drift gets caught.
- Zero codegen footprint β every byte hand-written.
- Built with
hatchling; ships as wheel + sdist. py.typedmarker β downstream type checkers see your imports natively.
pip install voicetel-apiRequires Python 3.10 or later. Tested against 3.10, 3.11, 3.12, and 3.13.
from voicetel import Client
with Client() as c:
# Exchange username + password for an API key (one-time per session)
c.login(username=1000000001, password="hunter2")
# Typed responses β your IDE knows what `me` is.
me = c.account.get()
print(f"Balance: ${me.cash:.2f} | Caller ID: {me.callerId}")
# List your numbers
for n in c.numbers.list().numbers:
print(f"{n.number} route={n.route} cnam={n.cnam} sms={n.smsEnabled}")Or, if you already have an API key:
from voicetel import Client
with Client(api_key="32hex...") as c:
coverage = c.inumbering.coverage(state="NJ")
for bucket in coverage.coverage:
print(f"{bucket.npa}-{bucket.nxx}: {bucket.count} TNs available")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.
Re-fetch the API key after any password change β the old one is invalidated.
Don't have credentials yet? Get them at voicetel.com/docs/api/v2.2/credentials.
from voicetel import Client
with Client() as c:
key = c.login(username=1000000001, password="hunter2")
# `key` is the new 32-hex bearer; the client already has it installed.| Resource | Operations | Example |
|---|---|---|
client.account |
Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | c.account.cdr(start=t1, end=t2) |
client.acl |
IP allowlist (CIDR entries) | c.acl.add(AclModifyRequest(acl=[...])) |
client.authentication |
SIP/HTTP auth mode + password | c.authentication.update(AuthPutRequest(authType=1)) |
client.e911 |
Records, address validation, provisioning | c.e911.validate(E911AddressRequest(...)) |
client.gateways |
Termination routes | c.gateways.list() |
client.inumbering |
Inventory, orders, port-ins | c.inumbering.search_inventory(npa=201) |
client.lookups |
CNAM & LRN dips | c.lookups.lrn("2015551234", ani="2012548000") |
client.messaging |
SMS/MMS, 10DLC brands & campaigns | c.messaging.send(MessageSendRequest(...)) |
client.numbers |
All operations on TNs on the account | c.numbers.assign_campaign("2015551234", ...) |
client.support |
Tickets, replies, attachments | c.support.create(TicketCreateRequest(...)) |
Every method that takes a request body accepts a typed Pydantic model imported from voicetel.models:
from voicetel import Client
from voicetel.models import (
MessageSendRequest,
NumberCampaignAssignRequest,
PortSubmitRequest,
)
with Client(api_key=key) as c:
sent = c.messaging.send(MessageSendRequest(
fromNumber="2012548000",
toNumber="2015551234",
text="Your code is 482917",
))
print(f"Sent: {sent.id} ({sent.parts} segment(s))")
c.numbers.assign_campaign(
"2015551234",
NumberCampaignAssignRequest(campaignId="CABC123"),
)All HTTP errors raise subclasses of voicetel.ApiError. Catch broadly or narrowly:
| Status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 429 | RateLimitError |
| 5xx | ServerError |
| other | ApiError |
from voicetel import Client, NotFoundError, RateLimitError
with Client(api_key=key) as c:
try:
n = c.numbers.get("9999999999")
except NotFoundError:
print("That number isn't on your account.")
except RateLimitError as e:
print(f"Slow down β retry in {e.body.get('retryAfter', '?')}s")Identical surface to Client, with await-based methods:
import asyncio
from voicetel import AsyncClient
async def fetch_state(numbers: list[str]) -> None:
async with AsyncClient(api_key="...") as c:
state = await c.messaging.numbers_state(numbers=numbers)
for s in state.numbers:
print(f"{s.number}: network={s.network} campaign={s.campaign}")
asyncio.run(fetch_state(["2015551234", "2015551235"]))These endpoints are limited to 6 requests per hour per IP:
account/infoaccount/mrc(client.account.recurring_charges())account/cdr(client.account.cdr())account/api-key(client.login())
The SDK automatically retries 429 responses with Retry-After honored, up to max_retries (default 2). To bump it:
Client(api_key=key, max_retries=4, timeout=60.0)git clone https://github.com/voicetel/python-sdk
cd python-sdk
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Unit tests (fast, no network)
pytest tests/unit
# 100% coverage gate
pytest tests/unit --cov --cov-fail-under=100
# Lint + type-check
ruff check src tests
mypy src
# Integration tests (live api.voicetel.com, read-only)
cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
pytest tests/integration
# Build wheel + sdist
python -m build
twine check dist/*- Reference docs: voicetel.com/docs/api/v2.2/
- Interactive playground: voicetel.com/docs/api/v2.2/playground/ β try the API in your browser without writing any code
- API credentials: voicetel.com/docs/api/v2.2/credentials/
- Type definitions: see the
voicetel.modelsmodule β every wire shape has a Pydantic model.
- Michael Mavroudis β Lead Developer
Contributions welcome. Open an issue describing the change you want to make, or send a pull request against main.
| Sponsor | Contribution |
|---|---|
| VoiceTel Communications | Primary development and production hosting |
This project is licensed under the MIT License β see the LICENSE file for details.