You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Callstack already has POST /sms/subscribe and forwards incoming SMS to subscribed webhook URLs, but delivery is currently best-effort: one POST per subscriber, a single 5s timeout, warning-only failure handling, no retry visibility, and no authenticity signal for receivers. That is risky for unattended Raspberry Pi/MFA/automation deployments because transient network failures can silently drop SMS events and unauthenticated callbacks are hard for downstream apps to trust.
This is a small v0.3 hardening slice that improves the existing webhook adapter without changing modem behavior or requiring real hardware.
User journey
An operator starts the HTTP server with an API key and a webhook signing secret.
A local integration subscribes a webhook URL through the authenticated API.
An SMS arrives while the receiver is briefly unavailable.
Callstack retries delivery with bounded exponential backoff and records the final delivery state.
The receiver verifies the callback signature before accepting the SMS event.
The operator can inspect recent webhook delivery attempts without exposing SMS bodies or secrets in logs.
API / UX sketch
Configuration should be explicit and not log secrets:
Optional diagnostic endpoint, authenticated like the other HTTP routes:
GET /sms/webhook-deliveries?limit=50
Return only delivery metadata by default: delivery id, subscriber URL host/path or redacted URL, attempt count, status, last error class/message, timestamps. Do not include API keys, signing secrets, SIM identifiers, or full SMS bodies in logs/diagnostics.
Technical approach
Extract webhook delivery out of module globals in server.py into a small testable helper, for example WebhookDispatcher.
Keep the first implementation in-process and bounded; durable webhook queues can be a later issue if needed.
Generate one opaque delivery id per event/subscriber and compute HMAC over a canonical string such as timestamp + "." + raw_json_body.
Add bounded exponential backoff with jitter; avoid infinite loops and make retry timings injectable for fast tests.
Record recent delivery attempts in an in-memory ring buffer with redacted URL display.
Integrate the dispatcher from the existing on_sms handler; do not alter SMS parsing/reassembly semantics.
Docs follow-up (not in this lane): README HTTP/webhook section after implementation
Existing context:
server.py currently stores webhook_urls, received_messages, and delivery_reports as process globals.
notify_webhooks() currently uses one aiohttp.ClientSession and logs failures only.
tests/test_api_auth.py already uses pytest-aiohttp and can be mirrored for HTTP route tests.
Hardware / modem caveats
This feature must be fully testable with fake SMS events and aiohttp test servers; no modem, SIM, phone numbers, or carrier network access should be required.
Do not claim exactly-once delivery: retries can create duplicates, so downstream consumers should use X-Callstack-Delivery-Id idempotently.
Avoid logging full phone numbers, SMS bodies, webhook URLs containing tokens, or signing secrets.
Acceptance criteria
Webhook callbacks include X-Callstack-Event, X-Callstack-Delivery-Id, X-Callstack-Timestamp, and X-Callstack-Signature: sha256=... when a signing secret is configured.
HMAC verification can be reproduced in a unit test from the exact body bytes and timestamp header.
Failed webhook POSTs retry with bounded exponential backoff up to a configurable max attempt count.
Retry tests run quickly by injecting sleep/backoff behavior; no real waiting for seconds.
A permanently failing subscriber records a final failed status instead of looping forever.
One failing subscriber does not prevent other subscribers from receiving the same SMS event.
Logs and any diagnostics redact webhook secrets/tokens and avoid SMS body disclosure.
Existing /sms/send, /sms/subscribe, /sms/messages, /sms/delivery-reports, and /ussd/send behavior remains compatible except for documented new auth/signature behavior.
Motivation
Callstack already has
POST /sms/subscribeand forwards incoming SMS to subscribed webhook URLs, but delivery is currently best-effort: one POST per subscriber, a single 5s timeout, warning-only failure handling, no retry visibility, and no authenticity signal for receivers. That is risky for unattended Raspberry Pi/MFA/automation deployments because transient network failures can silently drop SMS events and unauthenticated callbacks are hard for downstream apps to trust.This is a small v0.3 hardening slice that improves the existing webhook adapter without changing modem behavior or requiring real hardware.
User journey
API / UX sketch
Configuration should be explicit and not log secrets:
Callback request shape stays compatible enough for existing subscribers, with new headers:
Payload:
{ "event": "sms.received", "sender": "+155****0100", "body": "example", "received_at": "2026-06-24T00:00:00" }Optional diagnostic endpoint, authenticated like the other HTTP routes:
Return only delivery metadata by default: delivery id, subscriber URL host/path or redacted URL, attempt count, status, last error class/message, timestamps. Do not include API keys, signing secrets, SIM identifiers, or full SMS bodies in logs/diagnostics.
Technical approach
server.pyinto a small testable helper, for exampleWebhookDispatcher.timestamp + "." + raw_json_body.on_smshandler; do not alter SMS parsing/reassembly semantics.Affected modules and tests
Likely files:
server.pycallstack/webhooks.pytests/test_webhooks.pyand/ortests/test_api_auth.pyExisting context:
server.pycurrently storeswebhook_urls,received_messages, anddelivery_reportsas process globals.notify_webhooks()currently uses oneaiohttp.ClientSessionand logs failures only.tests/test_api_auth.pyalready usespytest-aiohttpand can be mirrored for HTTP route tests.Hardware / modem caveats
aiohttptest servers; no modem, SIM, phone numbers, or carrier network access should be required.X-Callstack-Delivery-Ididempotently.Acceptance criteria
X-Callstack-Event,X-Callstack-Delivery-Id,X-Callstack-Timestamp, andX-Callstack-Signature: sha256=...when a signing secret is configured./sms/send,/sms/subscribe,/sms/messages,/sms/delivery-reports, and/ussd/sendbehavior remains compatible except for documented new auth/signature behavior.Exact verification gates
Run these before opening the PR:
If packaging/config files change, also run:
Non-goals