---
title: SMS Campaigns
description: Complete guide to setting up the Termux Android SMS bridge for text message outreach, contact management, and response tracking.
icon: material/message-text
status: new
tags:
  - guide
  - admin
  - broadcast
  - sms
---

# 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.


![SMS Dashboard](../../../assets/images/screenshots/admin/sms.png){ loading=lazy }

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:

```mermaid
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) |

!!! danger "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](https://f-droid.org). 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:

1. **Install F-Droid** — download from [f-droid.org](https://f-droid.org) if you don't have it
2. **Install Termux** — search in F-Droid and install
3. **Install Termux:API** — search in F-Droid and install
4. **Install Termux:Boot** (optional) — for auto-start on phone reboot. Open once after install to register.
5. **Install Tailscale** (recommended) — from Play Store, connect to your tailnet for a stable IP

!!! note "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:

```bash
# Clone the SMS server (first time only)
pkg install -y git && git clone --depth 1 --filter=blob:none --sparse \
  https://gitea.bnkops.com/admin/changemaker.lite.git ~/sms-server \
  && cd ~/sms-server && git sparse-checkout set termux-sms

# Run the setup script — paste your API key at the end
bash ~/sms-server/termux-sms/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:

```bash
cd ~/sms-server && bash termux-sms/setup-services.sh
```

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:

1. Open **Android Settings → Apps → Termux → Battery** → set to **Unrestricted**
2. Lock Termux in the recent apps view (long-press the app card → Lock/Pin)
3. Samsung phones: also add Termux to **Settings → Device Care → Battery → Never Sleeping Apps**

### Updating

To pull the latest server code and re-run setup:

```bash
cd ~/sms-server && git pull && bash android/setup.sh YOUR_API_KEY_HERE
```

### Service Management

If you installed `termux-services` (recommended):

```bash
# 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):

```bash
# 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:

```bash
# 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:

```bash
# 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:

=== "Tailscale Auto-Discovery (Recommended)"

    1. Enter your Tailscale API key (`tskey-api-...`)
    2. Click **Discover Devices**
    3. The wizard queries the Tailscale API and lists all devices on your tailnet
    4. Select your Android phone — the URL auto-fills with its stable `100.x.x.x` IP

    !!! info "Getting a Tailscale API Key"
        Go to [Tailscale Admin Console](https://login.tailscale.com/admin/settings/keys) → Settings → Keys → Generate auth key or API access token.

=== "Manual URL Entry"

    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

1. Click **Test Connection** — the wizard calls the phone's `/health` endpoint
2. On success, you'll see device uptime and message count
3. Click **Save Configuration** — stores the URL and key encrypted in the database
4. The `enableSms` feature flag is automatically enabled

---

## How It Works

### Sending Messages

1. Admin creates an SMS campaign with a message template and contact list
2. Campaign is started → messages are queued in **BullMQ** (one at a time, serial delivery)
3. For each message, the Express API calls `POST /api/sms/send` on the phone
4. The Flask server on the phone executes `termux-sms-send` to send via Android's native SMS
5. A notification appears on the phone for each sent message
6. 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:

1. Calls `GET /api/sms/inbox?since=<last_sync_timestamp>` on the phone
2. The Flask server runs `termux-sms-list` to get new messages
3. Incoming messages are matched to contacts and classified by keyword
4. 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:

```bash
export SMS_API_SECRET='your-64-char-hex-key'
```

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:**

1. **Is the Flask server running?** Check Termux — you should see the startup banner
2. **Is the IP correct?** Run `ifconfig` in Termux to find the current IP
3. **Are they on the same network?** If not using Tailscale, both must be on the same LAN
4. **Is Tailscale connected?** Check the Tailscale app on the phone — it should show "Connected"
5. **Firewall?** Android rarely blocks incoming connections on Termux, but check if any firewall app is installed

```bash
# Quick test from your server
curl http://PHONE_IP:5001/health
```

### "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.

```bash
# 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:**

1. **SMS permissions granted?** Go to Android Settings → Apps → Termux:API → Permissions → SMS
2. **Active SIM card?** The phone needs a working SIM with SMS capability
3. **Message too long?** Maximum 1600 characters per message
4. **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:**

1. **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
2. **Disable battery optimization:** Android Settings → Apps → Termux → Battery → **Unrestricted**
3. **Lock Termux in recent apps** — long-press the app card → Lock/Pin
4. Samsung: also add Termux, Termux:API, and Termux:Boot to **Settings → Device Care → Battery → Never Sleeping Apps**
5. **Acquire wake lock:** Run `termux-wake-lock` in 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:

```bash
# 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:

1. Open **Google Messages** on the phone
2. Tap the profile icon (top right) → **Messages settings**
3. Tap **RCS chats** (or "Chat features")
4. **Turn off** "Turn on RCS chats"

!!! warning "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:

```bash
cd ~/sms-server
git pull

# Restart the server
sv restart sms-api
# Or without termux-services: pkill -f termux-sms-api-server.py && cd android && python termux-sms-api-server.py
```
