- sms/auth.py: verify_api_key dependency (Bearer/X-API-Key, SHA-256 hash compare via secrets.compare_digest, fail-closed 503 if no hash). All routes gated via a protected router; /health stays open for probes. - config.py: new sms_api_key_hash setting (VOIPMS_SMS_API_KEY_HASH). - Dockerfile + .dockerignore + docker-compose.yml: lean python:3.13-slim image; secrets injected via env_file, never baked in; host-localhost-only port mapping (SMS_PORT override); /health healthcheck. - .env.example: committed template (placeholders only, no secrets). - sms/docs/sms-facade.dokuwiki.txt: abstraction API reference (published to the apidoc wiki at voipms:sms-facade). - README: Authentication section, Docker section, 401/503 error rows. Co-Authored-By: Claude <noreply@anthropic.com>
5.5 KiB
sms — voip.ms SMS/MMS API façade
A small FastAPI service that wraps the voip.ms SMS/MMS REST
API (https://voip.ms/api/v1/rest.php) behind typed Python + REST endpoints,
with a per-day outbound send guard.
Files
| File | Purpose |
|---|---|
config.py |
Settings (env-loaded creds, dialing mode, daily limit, timeout) |
exceptions.py |
VoipMsError hierarchy (Auth, Api, RateLimit) |
models.py |
Pydantic request/response models |
client.py |
VoipMsSMSClient — async httpx wrappers for the 7 methods |
guard.py |
DailySendCounter — UTC-day counter + send logging |
auth.py |
Static API-key dependency (SHA-256 hash compare, fail-closed) |
app.py |
FastAPI app exposing the operations as REST endpoints |
requirements.txt |
Dependencies |
voip.ms prerequisites
- Enable the REST API and set an API password in the portal: Main Menu → SOAP / REST API → API Security.
- Whitelist this machine's public IP in the same API Security page.
- The DID you send from must have SMS enabled (Manage DIDs → Edit DID).
- Sending is capped at 100 SMS/MMS per day via the API. Pricing: $0.0075/SMS, $0.02/MMS, each direction.
Configure
export VOIPMS_API_USERNAME=...
export VOIPMS_API_PASSWORD=...
export VOIPMS_SMS_API_KEY_HASH=$(python3 -c "import secrets,hashlib;print(hashlib.sha256(secrets.token_urlsafe(32).encode()).hexdigest())")
# optional overrides:
# export VOIPMS_BASE_URL=https://voip.ms/api/v1/rest.php
# export VOIPMS_DIALING_MODE=nanpa # or e164
# export VOIPMS_DAILY_LIMIT=100
# export VOIPMS_TIMEOUT=30
(Settings also reads a .env file in the working directory.)
Authentication
Every endpoint except /health requires a static API key. Send it as either:
Authorization: Bearer <your-api-key>
X-API-Key: <your-api-key>
The server stores only the SHA-256 hash of accepted keys in
VOIPMS_SMS_API_KEY_HASH (comma-separated list allowed, for rotation). The raw
key is never persisted — generate one, hash it, and keep the raw key wherever
your clients live:
python3 -c "import secrets,hashlib; k=secrets.token_urlsafe(32); print('KEY:',k); print('HASH:',hashlib.sha256(k.encode()).hexdigest())"
Put the HASH: value in VOIPMS_SMS_API_KEY_HASH; hand the KEY: value to
your calling services. To rotate, add the new hash alongside the old, switch
callers over, then remove the old hash.
If VOIPMS_SMS_API_KEY_HASH is empty/unset, the service is fail-closed:
protected routes return 503 until a hash is configured. /health stays open
for liveness probes.
Run
pip install -r sms/requirements.txt
uvicorn sms.app:app --reload
Open http://127.0.0.1:8000/docs for the Swagger UI.
Run with Docker
A Dockerfile and docker-compose.yml live at the repo root. The compose
stack builds the image and runs it, injecting secrets from the local .env via
env_file — secrets are never baked into the image (.dockerignore excludes
.env, .venv, .git, sms/docs/).
docker compose up -d # build + start
docker compose logs -f sms # follow logs
docker compose ps # status / healthcheck
docker compose down # stop
The container binds 0.0.0.0:8000 internally; compose maps it to
127.0.0.1:${SMS_PORT:-8000} on the host — localhost-only by default. To
expose on the LAN, set the mapping to 8000:8000 (access is still gated by the
VOIPMS_SMS_API_KEY_HASH API key). If host port 8000 is taken, set
SMS_PORT=8001 (or any free port) in .env.
A healthcheck polls /health every 30s. The image is lean — reference docs and
the virtualenv are excluded; only sms/ + dependencies are inside.
Endpoints
| Method | Path | Upstream | Guarded |
|---|---|---|---|
| POST | /sms/send |
sendSMS |
✅ |
| POST | /mms/send |
sendMMS |
✅ |
| GET | /sms |
getSMS |
— |
| GET | /mms |
getMMS |
— |
| GET | /mms/{id}/media |
getMediaMMS |
— |
| DELETE | /sms/{id} |
deleteSMS |
— |
| DELETE | /mms/{id} |
deleteMMS |
— |
| GET | /health |
— | — |
Query params for GET /sms / GET /mms
from, to (YYYY-MM-DD), type (1=received / 0=sent), did, contact,
limit, timezone (-12..13), all_messages (1=SMS+MMS combined, 0=single
type; the message id must be 0 when set). For /sms the id query param is
sms; for /mms it is id.
Error mapping
| Condition | HTTP | Body |
|---|---|---|
| Missing API key | 401 | {"detail":"missing api key"} |
| Invalid API key | 401 | {"detail":"invalid api key"} |
| No API key hash configured | 503 | {"detail":"api key auth not configured"} |
| Daily limit exceeded | 429 | {detail, limit, used_today} |
| voip.ms returns non-success status | 502 | upstream message |
| Auth / IP-whitelist failure | 500 | upstream authentication failure (details logged server-side) |
Notes & limitations
- The daily counter is per-process. For multiple instances, swap
DailySendCounterfor a shared store (Redis, etc.). - Use
https://voip.ms(notwww.voip.ms) — thewwwhost 302-redirects and drops POST bodies, producingmissing_methoderrors. - Dialing mode normalizes
did/dston send:nanpa→ 10 digits,e164→+1+ 10 digits. - No
markSMSReadmethod exists in the voip.ms API; read state is not manageable via REST. - Inbound URL Callback receiver is not included here; it can be added
as a separate route set that replies with the literal
okand persists{TO}/{FROM}/{MESSAGE}/{ID}/{TIMESTAMP}/{MEDIA}.