- 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>
145 lines
5.5 KiB
Markdown
145 lines
5.5 KiB
Markdown
# sms — voip.ms SMS/MMS API façade
|
|
|
|
A small FastAPI service that wraps the [voip.ms](https://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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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/`).
|
|
|
|
```sh
|
|
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}`. |