Files
chelsea 1d9e7e0fab Add static API-key auth, dockerize service, publish façade docs
- 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>
2026-07-04 05:09:43 +00:00
..
2026-07-04 04:39:53 +00:00
2026-07-04 04:39:53 +00:00
2026-07-04 04:39:53 +00:00
2026-07-04 04:39:53 +00:00
2026-07-04 04:39:53 +00:00
2026-07-04 04:39:53 +00:00

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

  1. Enable the REST API and set an API password in the portal: Main Menu → SOAP / REST API → API Security.
  2. Whitelist this machine's public IP in the same API Security page.
  3. The DID you send from must have SMS enabled (Manage DIDs → Edit DID).
  4. 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_filesecrets 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 DailySendCounter for a shared store (Redis, etc.).
  • Use https://voip.ms (not www.voip.ms) — the www host 302-redirects and drops POST bodies, producing missing_method errors.
  • Dialing mode normalizes did/dst on send: nanpa → 10 digits, e164+1 + 10 digits.
  • No markSMSRead method 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 ok and persists {TO}/{FROM}/{MESSAGE}/{ID}/{TIMESTAMP}/{MEDIA}.