Skip to main content

termux-bulk-sms

Send a personalised SMS to a list of people from your own Android phone, through Termux:API, driven from a web page on your local network.

  • Paste a CSV, write a message with [name], set the delay, press Start, watch it go.
  • Sendings are campaigns: every recipient can be edited, removed or put on the block list.
  • Two types: Fixed (a list sent through once) and Ongoing (stays on until you turn it off; whoever you add is texted automatically). Several campaigns can be on at once — messages still leave the phone one at a time.
  • Nobody is texted twice: duplicates in the paste, everyone ever texted (history) and a manual block list are all skipped.
  • Optional remote lookup: before sending, ask your own backend which numbers it already knows (existing customers, members …) and skip those.
  • Python standard library only. No pip, no npm, no build step, no CDN.

It is one process on the phone. The page is served only to the local network and needs a password.

Install (in Termux)

pkg install python termux-api termux-services      # plus the Termux:API app, with SMS permission
pip install termux-bulk-sms
termux-bulk-sms init                                 # writes the config file and says where
termux-bulk-sms set-password
termux-bulk-sms run                                  # prints the address to open

Restart Termux once after installing termux-services. Edit the config file (init prints its path, normally ~/.config/termux-bulk-sms/config.json) before the first real run — see below.

As a service you can switch on and off

termux-bulk-sms service install     # once
termux-bulk-sms service enable      # on: now and whenever Termux starts
termux-bulk-sms service disable     # off: nothing is listening
termux-bulk-sms service status | restart | logs

The config file is read at start, so termux-bulk-sms service restart after editing it.

From a git checkout instead

git clone https://github.com/Logicbloke/termux-bulk-sms && cd termux-bulk-sms
cp config.example.json config.json
python -m termux_bulk_sms set-password
python -m termux_bulk_sms run
./service.sh install && ./service.sh enable

A config.json in the directory you run from is used when there is one; BULKSMS_CONFIG overrides both locations.

config.json

key default meaning
title "Bulk SMS" Name shown on the page.
port 8742 Port the page is served on.
dry_run true Nothing is sent while this is true. A rehearsal: rows are marked "Rehearsed", the history is left alone, and when the app is restarted with false those rows go back in the queue.
lookup.url "" Remote lookup endpoint. Empty = no lookup. Must be https://.
lookup.token "" Sent as Authorization: Bearer ….
lookup.label "our database" Shown as "Already in …" on skipped rows.
country_code "46" Country that numbers starting with 0 belong to.
default_delay, default_jitter 20, 5 Seconds to wait between rounds, and a random ± on top. Per campaign on the page.
default_batch 1 Messages per round: that many go out back to back, then the delay. Per campaign on the page.
window_start, window_end 08:00, 21:00 Default allowed hours (the phone's clock). Empty = any time.
sim_slot null SIM slot for dual-SIM phones (0 or 1).
max_consecutive_failures 3 A campaign pauses itself after this many failures in a row.
host, data_dir 0.0.0.0, data Listen address; where the SQLite file lives.

password_hash is written by set-password. The SQLite file lives in data/ next to the config file.

The remote lookup

Any backend can implement it:

POST <lookup.url>
Authorization: Bearer <lookup.token>
Content-Type: application/x-www-form-urlencoded

numbers=%2B46701234567%0A%2B46735556677        (one E.164 number per line)

200 {"results": ["known", "unknown"]}          (same order; "invalid" is the third value)

With a lookup configured, a row is sendable only after the lookup has answered unknown for the number it has now. The lookup is asked when a number is added or edited, when you press "Check numbers again", and once more when a campaign is started — never before each message.

The phone does not need a data connection to send. If the lookup cannot be reached, numbers that were already checked keep their answer and are sent; numbers added while offline stay "Not checked" and are not sent until a check succeeds. It never falls back to "probably fine".

How it avoids sending twice

Each row is marked sending in SQLite before termux-sms-send is called and sent after. If the app or the phone dies in between, the row comes back as Uncertain and the campaign as Paused; nothing resumes by itself and you settle the row by hand ("It was sent" / "It was not"). The history is kept when a campaign is deleted.

After a restart of the app nothing is on: a fixed campaign waits for Resume and an ongoing one for Turn on.

"Sent" means Android accepted the message from Termux. Delivery reports are not available through Termux:API.

Recipients and placeholders

Comma, semicolon or tab separated; a header row is optional; the phone column is found by content. Free-form lines (Anna Andersson 070-123 45 67) work too. [name] is the name column; with a header row every other column is a placeholder as well ([town]). A row whose placeholder would be empty is held back until you fix it. Placeholders that match nothing are flagged before you start.

Security

  • Only loopback and private-network clients (RFC 1918, link-local, ULA) are answered; a request arriving over mobile data gets 403.
  • Password login (PBKDF2), throttled; the session cookie is HttpOnly; SameSite=Strict.
  • The page is plain HTTP: anyone who can sniff your Wi-Fi can read it. Use it on a network you trust, and termux-bulk-sms service disable when you are done.

Good to know

  • Android's own SMS limit. Android asks for confirmation when an app sends many messages in a short time (historically 30 per 30 minutes; it varies by version and vendor), and that dialog stalls an unattended run. Watch the first larger campaign. The limit can be raised over adb: adb shell settings put global sms_outgoing_check_max_count 500.
  • Battery. A wake lock is held while a campaign runs — for an ongoing campaign, for as long as it is on. Also exempt Termux and Termux:API from battery optimisation, or Android may suspend them with the screen off.
  • The law. In most countries unsolicited marketing texts to private persons need prior consent, and every message must say who it is from and how to decline. The page reminds you when the message has no "stop" wording; the block list is where a "stop" reply belongs. Your carrier's terms may also limit bulk sending from a consumer subscription.

Releasing

  1. Bump __version__ in termux_bulk_sms/__init__.py, commit, push.
  2. Publish a GitHub release with the tag v<version>. The publish workflow runs the tests, builds and uploads to PyPI.

One-time setup on pypi.org: Your account → Publishing → Add a pending publisher with owner Logicbloke, repository termux-bulk-sms, workflow publish.yml, environment pypi.

Tests

python -m unittest discover -s tests

Licence

MIT — see LICENSE.

Metadata

Release files for termux-bulk-sms 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for termux-bulk-sms 0.1.0
File Size Uploaded Provenance Actions
termux_bulk_sms-0.1.0.tar.gz 43.1 kB With provenance Details for termux_bulk_sms-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for termux-bulk-sms 0.1.0
File Interpreter ABI Platform Actions
termux_bulk_sms-0.1.0-py3-none-any.whl Python 3 none any Details for termux_bulk_sms-0.1.0-py3-none-any.whl

Total release size: 83.8 kB

Release files / termux_bulk_sms-0.1.0.tar.gz

Download URL termux_bulk_sms-0.1.0.tar.gz
Size 43.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1538397f4861b39ef2b0901bdfd2530f58ecaa63f2f7186890b595a58474c589
BLAKE2b-256 checksum
How to use checksums
0de3c0c10d27cc743478bb2ecb0249d647f6bbbbe9a10d0f1e68121ad41bc432
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / termux_bulk_sms-0.1.0-py3-none-any.whl

Download URL termux_bulk_sms-0.1.0-py3-none-any.whl
Size 40.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
092dd6f9b2940bda53a9d53dec814491452a926f56af174648b0098f73bd0ae5
BLAKE2b-256 checksum
How to use checksums
434bddce3e68ea02406ea164b72d8a1060ccb7554feee15fefc04cc19999c3bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Security for termux-bulk-sms 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for termux_bulk_sms-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page