SMS Campaigns¶
Text message outreach via a Termux Android bridge. Uses a real Android phone to send and receive SMS — no third-party SMS gateway or Twilio account needed.

Enable with ENABLE_SMS=true or via the setup wizard.
Architecture Overview¶
The SMS system uses a three-tier architecture where your server communicates with a lightweight Python Flask API running on an Android phone:
graph LR
A[Admin Dashboard<br/>Campaign UI] -->|API calls| B[Express API<br/>BullMQ Queue]
B -->|HTTP + API Key| C[Android Phone<br/>Flask on Termux]
C -->|termux-sms-send| D[Android SMS]
D -->|Carrier Network| E[Recipients]
E -->|Reply SMS| D
D -->|termux-sms-list| C
C -->|HTTP response| B
Why this approach?
- No SaaS dependency — your phone is the SMS gateway, no Twilio/MessageBird/etc.
- Real phone number — recipients see a real number, not a short code
- Two-way messaging — incoming replies sync automatically
- Low cost — just your phone plan's SMS allowance
- Full control — FOSS stack end-to-end
Prerequisites¶
Before starting setup, you'll need:
| Item | Details |
|---|---|
| Android phone | Any Android 7+ device with an active SIM card and SMS plan |
| Termux | Terminal emulator — install from F-Droid (not Play Store) |
| Termux:API | Termux plugin for SMS/contacts/battery — install from F-Droid |
| Tailscale (recommended) | VPN mesh for stable IP — install from Play Store |
| Network access | Phone must be reachable from the server (Tailscale, LAN, or port forwarding) |
Both Apps MUST Come from F-Droid
The Play Store version of Termux is abandoned and incompatible with the API plugin. If you install Termux from the Play Store and Termux:API from F-Droid (or vice versa), SMS commands will fail with:
Termux:API is not yet available on Google Play
Fix: Uninstall both apps, then reinstall both from F-Droid. They must come from the same source because Android verifies matching app signatures for inter-process communication.
Phone Setup¶
Step 1: Install Apps from F-Droid¶
On your Android phone:
- Install F-Droid — download from f-droid.org if you don't have it
- Install Termux — search in F-Droid and install
- Install Termux:API — search in F-Droid and install
- Install Termux:Boot (optional) — for auto-start on phone reboot. Open once after install to register.
- Install Tailscale (recommended) — from Play Store, connect to your tailnet for a stable IP
termux-api package
You need two things called "termux-api": the F-Droid app (Termux:API) and the Termux package (pkg install termux-api). The setup script installs the package automatically, but the F-Droid app must be installed manually.
Step 2: Generate API Key¶
Go to the admin dashboard SMS Setup page (/app/sms/setup) and click Generate API Key. Copy the key — you'll paste it into the setup command.
Step 3: Run the Setup Script¶
Open Termux on the phone and run:
# Clone the SMS server (first time only)
pkg install -y git && git clone https://gitea.bnkops.com/admin/campaign_connector.git ~/sms-server
# Run the setup script — paste your API key at the end
bash ~/sms-server/android/setup.sh YOUR_API_KEY_HERE
The setup script automatically:
- Installs Python, Flask, termux-api, and openssh
- Saves the API key to
~/.bashrc - Requests SMS and Contacts permissions (tap Allow when prompted)
- Creates a Termux:Boot auto-start script (if Termux:Boot is installed)
- Starts the SMS server
When done, note the Phone URL displayed (e.g. http://100.64.0.5:5001).
Recommended: Install Service Supervisor¶
After initial setup, install termux-services for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:
This registers two supervised services:
- sms-api — Flask SMS API server (port 5001)
- sshd-custom — SSH daemon for remote management (port 8022)
Step 4: Prevent Android from Killing Termux¶
This is required for the server to run reliably in the background:
- Open Android Settings → Apps → Termux → Battery → set to Unrestricted
- Lock Termux in the recent apps view (long-press the app card → Lock/Pin)
- Samsung phones: also add Termux to Settings → Device Care → Battery → Never Sleeping Apps
Updating¶
To pull the latest server code and re-run setup:
Service Management¶
If you installed termux-services (recommended):
# Check status
sv status sms-api
# Restart
sv restart sms-api
# Stop
sv down sms-api
# Start
sv up sms-api
# View logs
tail -f ~/logs/sms-api.log
# Health check
curl http://127.0.0.1:5001/health
Without termux-services (legacy watchdog):
# Check if the server is running
curl http://127.0.0.1:5001/health
# Restart manually
cd ~/sms-server/android && bash sms-watchdog.sh
Accessing the Phone¶
There are several ways to run commands on the phone:
Direct (on phone)¶
Simply open the Termux app and type commands. Best for initial setup.
SSH (remote access)¶
Start the SSH server in Termux, then connect from your computer:
# On the phone (first time only):
pkg install openssh
passwd # Set a password
sshd # Start SSH server on port 8022
# From your computer:
ssh -p 8022 your-phone-ip
# Or with Tailscale:
ssh -p 8022 100.x.x.x
scrcpy (screen mirror)¶
Mirror the phone screen to your computer — great for setup:
# Install scrcpy on your computer (Ubuntu)
sudo apt install scrcpy
# Connect via USB
scrcpy
# Or wireless (phone must be on same network)
scrcpy --tcpip=phone-ip:5555
Setup Wizard¶
The admin panel provides a guided three-step wizard at /app/sms/setup:
Step 1: Prepare Phone¶
Walks you through installing apps, cloning the server, setting the API key, and starting the Flask server. Generates a shared API key that both the server and phone use for authentication.
Step 2: Connect¶
Choose how to find your phone's IP address:
- Enter your Tailscale API key (
tskey-api-...) - Click Discover Devices
- The wizard queries the Tailscale API and lists all devices on your tailnet
- Select your Android phone — the URL auto-fills with its stable
100.x.x.xIP
Getting a Tailscale API Key
Go to Tailscale Admin Console → Settings → Keys → Generate auth key or API access token.
Enter the phone's URL directly:
- With Tailscale:
http://100.x.x.x:5001(stable IP, works across networks) - On same LAN:
http://192.168.x.x:5001(changes if phone reconnects) - Via port forward:
http://your-public-ip:5001(requires router config)
Step 3: Test & Save¶
- Click Test Connection — the wizard calls the phone's
/healthendpoint - On success, you'll see device uptime and message count
- Click Save Configuration — stores the URL and key encrypted in the database
- The
enableSmsfeature flag is automatically enabled
How It Works¶
Sending Messages¶
- Admin creates an SMS campaign with a message template and contact list
- Campaign is started → messages are queued in BullMQ (one at a time, serial delivery)
- For each message, the Express API calls
POST /api/sms/sendon the phone - The Flask server on the phone executes
termux-sms-sendto send via Android's native SMS - A notification appears on the phone for each sent message
- Results are tracked in the database (success/failure per recipient)
Receiving Responses¶
A background service (sms-response-sync.service.ts) polls the phone's inbox at a configurable interval:
- Calls
GET /api/sms/inbox?since=<last_sync_timestamp>on the phone - The Flask server runs
termux-sms-listto get new messages - Incoming messages are matched to contacts and classified by keyword
- Threaded conversations are maintained per contact
Device Monitoring¶
A background service (sms-device-monitor.service.ts) checks phone health periodically:
- Battery level, charging status, temperature
- Server uptime and total messages sent
- Connection status (available/unreachable)
- Results displayed on the SMS Dashboard
Key Features¶
- Contact lists — import, tag, and segment contacts for targeted outreach
- Message templates — reusable templates with
{name}variable placeholders - BullMQ queue — serial delivery with configurable delays between messages
- Response sync — incoming SMS replies synced and classified automatically
- Device monitoring — battery, uptime, and connectivity reported in real-time
- Conversation view — threaded message history per contact
- Retry logic — configurable retry attempts for failed deliveries
Admin Routes¶
| Route | Description |
|---|---|
/app/sms/setup |
Guided setup wizard with Tailscale auto-discovery |
/app/sms |
SMS dashboard — campaign overview and device status |
/app/sms/contacts |
Manage contact lists and entries |
/app/sms/campaigns |
Create and monitor SMS campaigns |
/app/sms/conversations |
View threaded conversations with contacts |
Configuration¶
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
ENABLE_SMS |
false |
Feature flag (also set via setup wizard) |
TERMUX_API_URL |
— | Phone URL, e.g. http://100.x.x.x:5001 |
TERMUX_API_KEY |
— | Shared API key for authentication |
SMS_DELAY_BETWEEN_MS |
1000 |
Delay between messages in a campaign (ms) |
SMS_MAX_RETRIES |
3 |
Retry attempts for failed sends |
SMS_RESPONSE_SYNC_INTERVAL_MS |
10000 |
How often to check for incoming replies (ms) |
SMS_DEVICE_MONITOR_INTERVAL_MS |
30000 |
How often to check device health (ms) |
TAILSCALE_API_KEY |
— | Tailscale API key for auto-discovery |
TAILSCALE_TAILNET |
— | Tailscale tailnet name (optional) |
Note
When you use the setup wizard, configuration is stored in the database and takes priority over environment variables. You don't need to set env vars if you use the wizard.
Phone-Side Configuration¶
On the phone, only one environment variable is needed:
The Flask server also accepts TERMUX_API_KEY as an alias for backwards compatibility.
Phone API Endpoints¶
The Flask server running on the phone exposes these endpoints on port 5001:
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
No | Server status, uptime, messages sent |
GET |
/ |
No | Web dashboard with endpoint documentation |
POST |
/api/sms/send |
Yes | Send an SMS message |
POST |
/api/sms/send-reply |
Yes | Send a reply with conversation tracking |
GET |
/api/sms/inbox |
No | Get incoming messages (with since filter) |
GET |
/api/sms/list |
No | List messages with pagination |
GET |
/api/sms/history |
No | Get SMS history for a phone number |
GET |
/api/device/battery |
No | Battery level, health, temperature |
GET |
/api/device/location |
No | GPS coordinates (requires permission) |
GET |
/api/device/info |
No | Device info + battery + uptime |
GET |
/api/contacts/list |
No | Phone address book (with search) |
POST |
/api/campaign/notify |
No | Push notification to device |
Authentication uses the X-API-Key header with the shared secret.
Troubleshooting¶
Phone can't be reached¶
Symptoms: Test connection fails, "Connection refused" or timeout.
Checks:
- Is the Flask server running? Check Termux — you should see the startup banner
- Is the IP correct? Run
ifconfigin Termux to find the current IP - Are they on the same network? If not using Tailscale, both must be on the same LAN
- Is Tailscale connected? Check the Tailscale app on the phone — it should show "Connected"
- Firewall? Android rarely blocks incoming connections on Termux, but check if any firewall app is installed
"Authentication required" errors¶
Symptoms: API calls return 401 with "Authentication required".
Fix: The API key on the phone doesn't match the one in the admin panel.
# On the phone, check the current key
echo $SMS_API_SECRET
# If it doesn't match, update it
export SMS_API_SECRET='correct-key-from-admin-panel'
echo 'export SMS_API_SECRET="correct-key-from-admin-panel"' >> ~/.bashrc
# Restart the server
sv restart sms-api
# Or without termux-services: pkill -f termux-sms-api-server.py && cd ~/sms-server/android && python termux-sms-api-server.py
SMS not sending¶
Symptoms: Server responds successfully but messages don't arrive.
Checks:
- SMS permissions granted? Go to Android Settings → Apps → Termux:API → Permissions → SMS
- Active SIM card? The phone needs a working SIM with SMS capability
- Message too long? Maximum 1600 characters per message
- Rate limited? Minimum 1 second between messages (carrier may enforce longer delays)
Termux keeps getting killed¶
Symptoms: Server stops after some time, especially when phone screen is off.
Fix:
- Install
termux-services(if not already):bash ~/sms-server/android/setup-services.sh— this uses runit, a proper service supervisor that auto-restarts the server immediately if it crashes - Disable battery optimization: Android Settings → Apps → Termux → Battery → Unrestricted
- Lock Termux in recent apps — long-press the app card → Lock/Pin
- Samsung: also add Termux, Termux:API, and Termux:Boot to Settings → Device Care → Battery → Never Sleeping Apps
- Acquire wake lock: Run
termux-wake-lockin Termux (included in boot script)
Server won't start — "Missing SMS_API_SECRET"¶
Symptoms: Server exits immediately with a security error.
Fix: Set the API key environment variable:
# Generate a new key if you don't have one
python -c "import secrets; print(secrets.token_hex(32))"
# Set it
export SMS_API_SECRET='your-generated-key'
echo 'export SMS_API_SECRET="your-key"' >> ~/.bashrc
RCS / Chat Features interfering with replies¶
Symptoms: You send SMS messages successfully but some or all replies never appear in the system. Recipients say they replied, but the conversation shows no inbound messages.
Cause: Google Messages enables RCS (Rich Communication Services) by default. When RCS is active, replies from recipients who also have RCS may be sent over data/Wi-Fi instead of the carrier SMS channel. The Termux server only reads the SMS inbox via termux-sms-list, so RCS messages are invisible to it.
Fix: Disable RCS on the SMS phone:
- Open Google Messages on the phone
- Tap the profile icon (top right) → Messages settings
- Tap RCS chats (or "Chat features")
- Turn off "Turn on RCS chats"
This must be done on the phone running the SMS server
Disabling RCS on the server phone forces all outgoing messages to use plain SMS, and ensures replies also come back as SMS. You do not need recipients to change anything on their end — when the server phone sends a plain SMS, the reply will be plain SMS as well (unless the recipient's carrier forces RCS-only, which is rare).
Additional checks:
- Some carriers (e.g. Google Fi, Jio) enable RCS at the carrier level. If disabling in the app doesn't help, contact the carrier to disable RCS on the SIM.
- If the phone has Samsung Messages instead of Google Messages, go to Samsung Messages → Settings → Chat settings → turn off.
- After disabling RCS, restart the phone and verify by sending a test message — the send button should show an SMS label, not "Chat".
Updating the SMS server¶
To pull the latest version of the server code: