====== apifrontend SMS façade ====== A small **FastAPI service** that wraps the [[voipms:sms|voip.ms SMS/MMS REST API]] behind typed Python + clean REST endpoints, with a per-day outbound send guard and static API-key auth. This page documents the **façade's own API** (what callers see); the upstream method reference lives at [[voipms:sms]]. Source: private Gitea repo ''sms-api-wrapper''. Sanitized — no account credentials, phone numbers, or API keys appear here; those are supplied at runtime via environment variables and kept local. ===== Architecture ===== ^ File ^ Purpose ^ | ''config.py'' | ''Settings'' — env-loaded creds, dialing mode, daily limit, API-key hash, timeout | | ''exceptions.py'' | ''VoipMsError'' hierarchy: ''Auth'', ''Api'', ''RateLimit'' | | ''models.py'' | Pydantic request/response models | | ''client.py'' | ''VoipMsSMSClient'' — async ''httpx'' wrappers for the 7 upstream 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 + router exposing the operations as REST endpoints | The service is single-process: one shared ''VoipMsSMSClient'' and one ''DailySendCounter'' live in ''app.state'' for the lifetime of the app. ===== Configuration ===== Loaded by ''Settings'' (pydantic-settings, env prefix ''voipms_'', also reads a ''.env'' file in the working directory). ^ Env var ^ Req ^ Notes ^ | ''VOIPMS_API_USERNAME'' | yes | voip.ms portal login **email** (not the account ID) | | ''VOIPMS_API_PASSWORD'' | yes | API password from **API Security** (distinct from portal login password) | | ''VOIPMS_SMS_API_KEY_HASH'' | yes* | SHA-256 hex hash of the accepted client API key; comma-separated list allowed for rotation. Empty = **fail-closed** (503) | | ''VOIPMS_DIALING_MODE'' | no | ''nanpa'' (default, 10 digits) or ''e164'' (''+1'' + 10 digits) | | ''VOIPMS_DAILY_LIMIT'' | no | Per-UTC-day outbound cap, default ''100'' (matches upstream API limit) | | ''VOIPMS_TIMEOUT'' | no | HTTP timeout seconds, default ''30'' | | ''VOIPMS_BASE_URL'' | no | Default ''https://voip.ms/api/v1/rest.php'' (avoid ''www.voip.ms'') | * ''VOIPMS_SMS_API_KEY_HASH'' is optional in code but the service is fail-closed without it — set it for any real deployment. Upstream prerequisites (on the voip.ms side): enable the REST API + set an API password, **whitelist this machine's public IP** under API Security, and **enable SMS on the sending DID**. ===== Authentication ===== Every endpoint except ''/health'' requires a static API key. Send it as either: * ''Authorization: Bearer '' * ''X-API-Key: '' The server stores only the **SHA-256 hash** of accepted keys (''VOIPMS_SMS_API_KEY_HASH''); the raw key is never persisted. Comparison is constant-time (''secrets.compare_digest''). Generate a key + hash: 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. **Rotation:** add the new hash alongside the old, switch callers, then remove the old hash. If ''VOIPMS_SMS_API_KEY_HASH'' is empty/unset, protected routes return ''503'' (fail-closed). ''/health'' stays open for liveness probes. ===== Endpoints ===== ^ Method ^ Path ^ Upstream ^ Send-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'' | — | — (no auth) | ==== POST /sms/send ==== ^ Field ^ Req ^ Notes ^ | did | yes | Sender DID | | dst | yes | Destination number | | message | yes | Max **160** chars | Returns ''{"id": }''. ''did''/''dst'' are normalized per ''VOIPMS_DIALING_MODE'' before sending. ==== POST /mms/send ==== ^ Field ^ Req ^ Notes ^ | did | yes | Sender DID | | dst | yes | Destination | | message | yes | Max **2048** chars | | media1 | no | URL to a media file | | media2 | no | Base64 image (''data:image/png;base64,...'') | | media3 | no | Reserved | Returns ''{"id": }''. See [[voipms:sms|upstream reference]] for media type/size limits. ==== GET /sms ==== Query params: ''sms'' (specific id), ''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 id must be ''0'' when set). Returns a list of SMS records. Empty upstream results (''status: no_sms'') are normalized to ''[]'' — not an error. ==== GET /mms ==== Same shape as ''/sms'' but the id query param is ''id''. ''all_messages=1'' returns MMS+SMS combined (id must be ''0''). ==== GET /mms/{id}/media ==== Query param: ''media_as_array'' (bool, default false). Returns ''{"id","date","media":[url,...]}''. ==== DELETE /sms/{id} · DELETE /mms/{id} ==== Returns ''{"status": "success"}'' on success. ==== GET /health ==== Unauthenticated. Returns the literal text ''ok'' (200). Use for liveness probes. ===== 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 send limit exceeded | 429 | ''{detail, limit, used_today}'' | | Upstream returns non-success | 502 | upstream message | | Upstream auth / IP-whitelist failure | 500 | ''upstream authentication failure'' (details logged server-side, never leaked to caller) | ===== Outbound send guard ===== ''DailySendCounter'' caps outbound sends (''/sms/send'', ''/mms/send'') at ''VOIPMS_DAILY_LIMIT'' per **UTC day**. The counter is checked **before** the upstream call and incremented in-process; every successful send is logged. Resets automatically on UTC-day rollover. **Per-process only:** for multiple instances, replace ''DailySendCounter'' with a shared store (Redis, etc.). ===== Dialing mode ===== ''nanpa'' → numbers sent as 10 digits. ''e164'' → ''+1'' + 10 digits. Applied to ''did''/''dst'' on send. Pick the mode that matches what your DID/carrier expects; normalize on your side for inbound. ===== Notes & limitations ===== * No ''markSMSRead'' exists upstream; read state is not manageable via this façade. * Use ''https://voip.ms'' (not ''www.voip.ms'') — the ''www'' host 302-redirects and drops POST bodies. * Inbound URL Callback receiver is **not** included; it can be added as a separate route set that replies with the literal ''ok'' and persists ''{TO}/{FROM}/{MESSAGE}/{ID}/{TIMESTAMP}/{MEDIA}''. * No automated test suite; verify manually per the README. ---- //Façade over the [[voipms:sms|voip.ms SMS/MMS REST API]]. Upstream behavior, limits, and gotchas are documented there.//