# SessionTalk customer-hosted SMS bridge Reference implementation for SessionTalk Pro/Free build **734 or later**. It implements SessionTalk's JSON inbox API, not Acrobits' XML/JSON templates. One deployment serves one SMS number/mailbox and any of its devices. Run separate instances with separate database volumes, keys and tokens for separate users. No SessionCloud portal project is needed. Teams Hosted and portal-provisioned SessionCloud messaging use their existing services. The bridge accepts authenticated carrier callbacks, commits encrypted message bodies and push jobs to SQLite, and then acknowledges the provider. The separate worker retries notification failures. Fetching never deletes messages. Outgoing SMS is sent once; an uncertain provider response must be checked before retrying. This example supports text SMS, not MMS, delivery receipts or sent-message history across devices. Carrier charges still apply. ## Start on a Linux server 1. Point a DNS hostname at the server. Install Docker Compose and Caddy; allow public HTTPS on port 443. Keep port 8000 bound to loopback as supplied. 2. Run `python3 bootstrap.py`. This writes new random credentials to `.env`, with owner-only permissions, and never prints them. Keep `.env` out of source control. 3. Edit `.env`: set `PUBLIC_URL` to the exact HTTPS origin, `SMS_NUMBER` to the receiving number in `+` international format, and `PROVIDER` to `aa`, `twilio`, `telnyx` or `custom`. Set that provider's credentials below. Preserve the generated `MAILBOX_ID`, `MAILBOX_TOKEN`, `DATABASE_KEY` and `WEBHOOK_SECRET`. 4. Replace `sms.example.com` in `Caddyfile` with the same hostname. Install this configuration in a dedicated Caddy instance, validate it, and start Caddy. The example suppresses access and proxy error logs to avoid recording A&A's secret webhook path. Configure health/status monitoring without raw URLs, request headers, bodies or push credentials. 5. Run `docker compose up -d --build`. Both the web service and worker must run. Never publish the Flask development server. The example uses Gunicorn. 6. Set the carrier's inbound webhook as described below, then configure the app. Back up the encrypted database volume and the separate `DATABASE_KEY`; losing either loses message history. Restrict access to both. Add disk monitoring, retention, dependency updates, firewall/request rate limits and operational alerts before production use. This is a single-server reference, not a highly available multi-tenant service. Keep only one worker per database. Restarts can repeat a push after an uncertain result; message UUIDs prevent duplicate inbox entries. ## Provider configuration | Provider | Server settings | Carrier inbound destination | | --- | --- | --- | | A&A | `AA_PASSWORD`: the number's separate outgoing SMS password | `https://YOUR_HOST/webhooks/aa/WEBHOOK_SECRET` with the generated secret substituted | | Twilio | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | POST `https://YOUR_HOST/webhooks/twilio` for incoming messages on the number | | Telnyx | `TELNYX_API_KEY`, `TELNYX_PUBLIC_KEY` (base64 Ed25519 public key) | `https://YOUR_HOST/webhooks/telnyx` on the number's messaging profile | | Custom | `CUSTOM_SEND_URL`, `CUSTOM_API_KEY`, generated `WEBHOOK_SECRET` | POST `https://YOUR_HOST/webhooks/custom` using the contract below | A&A: configure **SMS Inbound** in its control pages. Do not append `?` or `&`; this example accepts POST form fields `oa`, `da`, `ud`, `scts`. A&A does not document a signature for this callback, so the random secret URL is a bearer credential. Keep it private and rotate it if exposed. The digest of these fields and `udh` deduplicates exact retries; identical messages with identical timestamps cannot be distinguished by this API. This example expects provider-assembled text; add UDH multipart reassembly if your A&A service delivers individual segments. Twilio: the SDK checks `X-Twilio-Signature` against `PUBLIC_URL` plus the exact `/webhooks/twilio` path and all form parameters. It also checks the Account SID and destination number. Do not add query parameters or rewrite the public path. Telnyx: verify Ed25519 over the timestamp, `|`, and exact raw request bytes; reject timestamps outside five minutes. Only `message.received` is stored. Other signed event types receive 204. The destination must match `SMS_NUMBER`. Custom: send JSON `{"id":"stable-provider-id","from":"+15555550102", "to":"+15555550101","text":"Hello"}`. Set `X-SMS-Timestamp` to Unix seconds and `X-SMS-Signature` to lowercase hexadecimal HMAC-SHA256 with `WEBHOOK_SECRET` over `timestamp + "." + rawBody`. Retries must retain the same message ID. The signature window is five minutes; sign each later retry with a fresh timestamp. The outgoing custom adapter sends JSON `{from,to,text}` with `Authorization: Bearer CUSTOM_API_KEY`; its API must return 2xx only on acceptance. Adapt this adapter if your provider uses a different API. It is not the portal's Custom webhook schema. ## Configure the standalone app Disable the SIP account while editing. Enable SMS and select the **HTTPS** transport with the **Custom** preset. Save these outgoing settings first: | App field | Value | | --- | --- | | Gateway URL | `https://YOUR_HOST/send` | | SMS username | `MAILBOX_ID` from `.env` | | SMS password | `MAILBOX_TOKEN` from `.env` | | Username/password parameter | `username` / `password` | | Destination/message parameter | `da` / `ud` | | Success response prefix | `OK:` | In **Advanced SIP settings > Messaging > Incoming messages**, enable **Use my own messaging service** and enter: | App field | Value | | --- | --- | | Incoming message fetch URL | `https://YOUR_HOST/inbox` | | Push registration URL | `https://YOUR_HOST/devices` | | Mailbox ID | `MAILBOX_ID` | | SMS number | `SMS_NUMBER` | | Mailbox access token | `MAILBOX_TOKEN` | Choose **Connect mailbox**, re-enable the account, enable account push and allow notifications. Keep the app open for initial device registration. The server does not need the SIP username or SIP password. The account's SIP identity binds the inbox locally. Existing outgoing A&A settings can remain pointed directly at A&A instead of `/send` if preferred. ## API contract All app-to-bridge requests use `Authorization: Bearer MAILBOX_TOKEN`, HTTPS, no redirects, and JSON responses. The fetch/report URLs cannot have credentials, query strings or fragments. They can use different HTTPS hosts, but both receive the mailbox token: only enter servers you trust with that mailbox. * `GET /inbox?after=0`: `{messages:[...],next:123,more:false}`. `next` is a monotonically increasing integer sequence (a decimal string is also accepted). Return at most 100 messages per page, ordered by sequence. `next` is the last returned sequence, or the requested cursor for an empty page. Do not use UUIDs as the cursor. Do not delete messages when they are fetched. * A message is `{id,peer,body,direction,state,created_at}`. `id` is a stable UUID, `peer` the sender's international number or short alphanumeric sender ID, `body` UTF-8 text up to 65,536 bytes, `direction` `inbound`, `state` `received`, and `created_at` an ISO 8601 UTC timestamp. Keep the same UUID for retries. * `GET /inbox/UUID`: `{message:{...}}`, or 404 if absent. Both iOS and Android use this endpoint to fetch the full message after a background push. The same token and HTTPS checks apply. Return promptly; mobile background execution is bounded. * `PUT /devices`: JSON `{deviceId,profileId,mailboxId,pushUrl,pushToken,expiresAt}`. Upsert per device/profile. `expiresAt` is Unix seconds. Return `{"ok":true}`. Store the push credential encrypted. Allow only your approved SessionTalk relay hosts and the `/byo/webservice/push` path; never POST to arbitrary supplied URLs. The app obtains a grant using its existing device push token privately with the selected SessionTalk relay. The bridge receives only the resulting seven-day capability. Foreground synchronization refreshes the report about every five minutes and when routing changes. Open the app at least once every seven days to renew it. A stale/absent push route never prevents foreground inbox fetching. Token, account identity or permanent push-profile removal invalidates the grant. Disconnecting the mailbox makes the app discard its pushes; remove its stored device route on your server too. A previously issued grant otherwise expires after seven days. Never request or distribute the relay's shared server secret. After committing an incoming message, the worker sends: ```http POST Authorization: Bearer Content-Type: application/json {"messageId":"stable-message-uuid","remoteNumber":"+15555550102","text":"Hello"} ``` The relay fixes the device, account and mailbox from the grant and always requests a full fetch. A 200 `{"ok":true}` acknowledges submission to APNs/FCM, not delivery or reading. Retry network errors, 429 and 5xx with backoff using the same message UUID. A 401 requires a new device report; retain the message for fetching. Never send SessionTalk device tokens to Acrobits' push service. Private push deployments must run the matching relay implementation; set `PUSH_HOSTS` explicitly to their approved hostnames in this example. Do not add arbitrary hosts on user input. ## Test before enabling customers ```sh python3 -m venv .venv . .venv/bin/activate pip install -r requirements.txt python -m unittest -v test_bridge ``` Tests use synthetic credentials and mock outbound HTTP; they do not send SMS. Then test a real provider callback, one reply, duplicate callback, wrong signature, app foreground, locked Android/iPhone, reconnect and two devices. Verify full Unicode text, long-message fetching and correct account routing. Carrier delivery and APNs/FCM need your real provider account and devices; passing these unit tests alone does not establish them. Provider references: https://support.aa.net.uk/SMS_API, https://www.twilio.com/docs/usage/webhooks/webhooks-security, https://developers.telnyx.com/docs/messaging/messages/receiving-webhooks.