Complete SMS sending solution with rate limiting, retry logic, error handling, logging, and reporting.
# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Launch web dashboard
python main.py --streamlitOpens interactive dashboard at http://localhost:8501 by default.
Sends personalized SMS messages to recipients from CSV files using the Text-Ware SMS API.
Features:
- ✅ Flexible CSV selection (sample or uploaded)
- ✅ Unsaved imported recipient lists can be used immediately
- ✅ Batch cleaner for raw recipient exports in
resources/input/ - ✅ Single & bulk SMS sending
- ✅ Automatic name personalization {name}
- ✅ Smart name handling - limits to first 2 words
- ✅ Automatic phone cleanup to Sri Lanka format 94XXXXXXXXX
- ✅ Email format validation on import and manual add
- ✅ Invalid CSV rows are rejected with row-level reasons
- ✅ Rate limiting (adjustable) - prevents API overload
- ✅ Automatic retry (3 attempts) - handles failures
- ✅ Detailed logging - all events recorded
- ✅ JSON reports - campaign results
- ✅ Error handling - graceful failure handling
- ✅ Web dashboard - full interactive UI
- ✅ CLI tools - command line interface
- ✅ Menu system - user-friendly navigation
sms-sender-python-textware/
├── sms_sender.py Core SMS engine
├── main.py CLI entry point
├── streamlit_app.py Web dashboard (recommended)
├── quickstart.py Menu system
├── clean_recipient_batches.py Batch CSV cleaner for raw recipient exports
├── .env SMS credentials (NOT committed - see .env.sample)
├── .env.sample Environment template (reference)
├── recipients.csv Optional saved recipient list
├── resources/
│ ├── message_template.txt Default SMS template (editable)
│ ├── sample-recipients.csv Bundled sample data
│ ├── input/ Raw CSV imports to be cleaned (gitignored, `.gitkeep` kept)
│ └── output/ Cleaned CSV exports from the batch cleaner (gitignored, `.gitkeep` kept)
├── requirements.txt Python dependencies
├── pytest.ini Pytest configuration
├── tests/ Automated test suite
├── README.md This documentation
└── venv/ Virtual environment
Auto-generated folders:
├── logs/ Application logs with timestamps
└── reports/ JSON campaign reports
Security Note: .env file is in .gitignore and never committed. Only .env.sample is in the repo as a template.
python main.py --streamlitOpens at http://localhost:8501 with full UI.
Direct Streamlit command also works:
python -m streamlit run streamlit_app.pyDashboard Features:
1️⃣ Recipients Tab
- 🔄 CSV File Selection: Switch between:
Sample(default, backed by bundled sample data)recipients.csv(saved custom list, if you choose to persist one)Imported (...)(temporary in-memory upload)
- 📊 View Recipients: Table showing all current recipients
- ➕ Add Recipient: Single form entry for one person
- 📤 Upload CSV: Upload custom recipients file
- Show total recipient count
2️⃣ Campaigns Tab
- ✏️ Message Template: Customize SMS with
{name}placeholder - ⚙️ Campaign Settings:
- Shows selected recipients count
- Rate limit slider (1-10 seconds)
- 👁️ Preview: See list of recipients who'll receive SMS
- 🧪 Test Send: Send one test SMS to verify
- 🚀 Send Campaign: Send to all recipients (with confirmation)
- 📊 Results: View success/failure metrics
3️⃣ Reports Tab
- 📋 View Reports: Select from all campaign reports
- 📈 Summary Metrics: Total, accepted, error, and skipped counts
- 📝 Detailed Log: Compact table for each SMS iteration
- 📄 Full Message Drill-down: Open the full stored message body for any row
- 📤 CSV Export: Download the current filtered report rows as CSV
- 📥 Download: Export report as JSON
4️⃣ Settings Tab
- View SMS API configuration (read-only for security)
- View log and report file counts
# Menu system (interactive)
python quickstart.py
# Send test SMS
python main.py --test
# Send to all recipients
python main.py --bulkClean every pending raw CSV in resources/input/ into resources/output/:
python clean_recipient_batches.pyClean one specific file:
python clean_recipient_batches.py --file "resources/input/your-file.csv"Optional flags:
--forcerebuilds an output even if a cleaned file already exists--input-dirscans a different input directory--output-dirwrites cleaned files to a different output directory
from sms_sender import SMSSender, get_sms_message
sender = SMSSender()
message = get_sms_message()
result = sender.send_sms("0768622302", message, "Name", "email@example.com")resources/sample-recipients.csvis bundled with the app and always available through the Sample source- Contains: Sayuru Akash, test@gmail.com, 0777123456
- Best option for first-time setup and quick verification
- Go to Recipients Tab
- Click Upload Recipients CSV
- Select your CSV file
- Choose Use now to work from the cleaned upload immediately without writing any file
- Choose Save as recipients.csv only if you want the cleaned list persisted on disk
name,email,contact_number
Sayuru Akash,test@gmail.com,0777123456
John Doe,john@example.com,0761234567
Jane Smith,jane@example.com,0768765432
Requirements:
- name: Optional, used for personalization when present (automatically limited to first 2 words)
- email: Optional, but if provided it must be a valid format (example: info@codezela.com)
- contact_number: Required and must be a valid Sri Lanka mobile number
Minimum supported CSV:
contact_number
0777123456
0761234567Accepted phone input formats (all normalized to 94XXXXXXXXX):
- 7XXXXXXXX
- 07XXXXXXXX
- 947XXXXXXXX
- +94 7X XXX XXXX
- Variations with spaces, dashes, or parentheses
Upload behavior:
- CSV importer auto-cleans name, email, and phone fields
- Rows with only
contact_numberare valid and supported - Invalid rows are shown with row numbers and reasons
- Valid cleaned rows can be used immediately as a temporary imported source
- Saving to
recipients.csvis optional - Duplicate phone numbers are automatically deduplicated (first row kept)
If you have a larger exported sheet with extra columns, place it in resources/input/ and run:
python clean_recipient_batches.pyWhat the cleaner does:
- keeps only
contact_number,name, andemail - requires a valid Sri Lankan mobile number
- drops any row where
Payment DetailsorRegisteredis truthy - removes rows with missing or invalid numbers
- removes duplicate numbers and keeps the first valid occurrence
- blanks invalid email values instead of dropping otherwise-valid rows
- writes
<original-name>_cleaned.csvintoresources/output/ - skips files that already have a cleaned output unless you pass
--force
This is useful for raw call sheets, CRM exports, and spreadsheet dumps that contain many non-SMS columns.
Name Handling:
- Names with more than 2 words are automatically limited to the first 2 words
- Example: "Muhammad Abdullah Hassan" → "Muhammad Abdullah"
- This applies to both uploaded CSVs and manually added recipients
- Prevents long names in personalization
- Use sidebar selector to choose which CSV to use
- Shows recipient count for selected file
- Defaults to
Sample - Automatically switches to uploaded file when created
Create .env file from .env.sample with your Text-Ware credentials:
SMS_USERNAME=your_textware_username_here
SMS_PASSWORD=your_textware_password_here
SMS_SOURCE=YOUR_SENDER_ID
SMS_API_URL=https://msg.text-ware.com/send_sms.phpSecurity:
- ✅
.envis in.gitignore- never committed to repo - ✅ Only
.env.sampleis shared - acts as template - ✅ Credentials are local-only
- ✅ Safe to commit the project
Two options available:
-
Sample source (default)
- Built-in test data
- Always available
- Backed by
resources/sample-recipients.csv - Good for testing
-
recipients.csv (your uploads)
- Optional saved list on disk
- Persists between sessions
- Can be edited through the dashboard or manually in a text editor / spreadsheet tool
Default Template:
The default SMS text is loaded from resources/message_template.txt.
Dear Student, {name}
We're excited to announce that the CCA Bootcamp Programs Inauguration
Ceremony will be held today.
This will be the official launch session for our bootcamp programs under
BYOW, DDIGITAL, RANDS, and VAT0. We warmly invite you to join us and be
part of this important beginning.
Date: 22 March 2026
Time: 9.00 PM
Platform: Zoom
Join Link: https://us06web.zoom.us/j/85143454719?pwd=...
Passcode: 331423
We look forward to having you with us tonight.
SITC Campus X CodeZela
Personalization:
- Use
{name}placeholder for recipient's name - Example: "Dear Student, Sayuru Akash"
- If name missing, sends without replacement
Customize in Streamlit Dashboard:
- Go to Campaigns > Message Template
- Edit text directly in the web UI
- The edited message is kept in Streamlit session state for the current app session
- Sending from the dashboard uses the edited message directly without rewriting Python files
- This is the recommended way to adjust a campaign message before sending
Or edit the default template file:
resources/message_template.txt
- This controls the default message used by the CLI and by new Streamlit sessions
- Streamlit message edits do not rewrite this file
- Edit this file only when you want to change the default template for future runs
| Setting | Value | Adjustable |
|---|---|---|
| Rate limit | 2 seconds | Yes (1-10 in UI) |
| Retry attempts | 3 times | Code only |
| Timeout | 30 seconds | Code only |
| API method | POST | Auto fallback to GET |
- Speed: ~30 SMS/minute (with 2-sec rate limit)
- 100 recipients: ~3-4 minutes
- 500 recipients: ~15-20 minutes
- 1000 recipients: ~30-40 minutes
- Success rate: 99%+ with retry logic
- In Dashboard: Campaigns tab > Rate Limit slider
- In Code: Edit
sms_sender.py, line withself.rate_limit_delay
Edit sms_sender.py:
max_retries = 3 # Change this value
timeout = 30 # Request timeout in secondsLocation: reports/sms_report_YYYYMMDD_HHMMSS.json
Contains:
- Report version and generated timestamp
- Campaign start and finish timestamps
- Total recipient rows processed
- Gateway acceptance, error, and skipped breakdown
- Run context such as channel, source, and message template
- Individual SMS results:
- Iteration number
- Status (
success,error, orskipped) - Phone number / contact number
- Recipient name & email
- Full message body
- Message preview
- API response
- Operation ID when available
- Error details (if failed)
Note: a successful send in this app means the SMS gateway accepted the request. It does not by itself confirm handset delivery.
View report:
cat reports/sms_report_*.json | python -m json.toolLocation: logs/sms_sender_YYYYMMDD_HHMMSS.log
Contains:
- All API calls and responses
- Error messages with context
- Rate limiting info
- Retry attempts
- Success confirmations
View logs:
tail -f logs/sms_sender_*.logRun the full suite:
python -m pytestRun with coverage:
python -m pytest --cov=. --cov-report=term-missingThe suite covers:
- core SMS sender helpers and API-request behavior
- bulk sending, reports, and fallback paths
- CLI entry points in
main.py - quickstart/menu flows in
quickstart.py - Streamlit app state and recipient workflows
- batch-cleaning script behavior for pending and one-off CSV imports
Current local verification:
110tests passing93%total coverage frompython -m pytest --cov=. --cov-report=term-missing
python main.py --streamlit- Open
http://localhost:8501 - Go to Campaigns tab
- Click Send Test SMS
- Check report in Reports tab
python main.py --testSends one SMS to first recipient in CSV.
python main.py --bulkSends to all recipients with confirmation.
-
.envfile is in.gitignore(credentials never committed) -
.env.sampleincluded as template - Virtual environment in
.gitignore -
__pycache__/in.gitignore - Logs and reports auto-generated (safe to ignore)
- No hardcoded credentials in code
- README updated with all features
- All required files present
Before commit, verify:
git status # Should show no .env file
cat .gitignore | grep -E "\.env|venv" # Should find both| Problem | Solution |
|---|---|
| Streamlit not starting | Ensure venv is activated: source venv/bin/activate |
| Module not found errors | Install dependencies: pip install -r requirements.txt |
| ".env not found" | Create .env from .env.sample with your credentials |
| No recipients showing | Use the bundled Sample source or upload a custom file |
| Cleaner says phone column missing | Rename or map your phone column to a supported header such as Phone Number, contact_number, or mobile |
| API connection failed | Check internet, verify SMS credentials in .env |
| SMS not sending | Check phone format (07XXXXXXXX for Sri Lanka or full international) |
| Port 8501 already in use | Stop the existing process with `lsof -ti:8501 |
| venv not activating | Recreate: python3 -m venv venv && source venv/bin/activate |
Debug steps:
- Check if venv is active: You should see
(venv)in terminal - View logs:
cat logs/sms_sender_*.log - Check reports:
cat reports/sms_report_*.json | python -m json.tool - Test credentials:
echo $SMS_USERNAME(should show username) - Verify bundled sample:
head -5 resources/sample-recipients.csv - Verify pending raw inputs:
find resources/input -maxdepth 1 -name "*.csv"
Credential Safety:
- ✅
.envfile is.gitignored - never committed - ✅ Only
.env.samplein repo as reference - ✅ Credentials never logged or displayed
- ✅ API calls use HTTPS only
- ✅ No hardcoded secrets in code
Data Protection:
- ✅ Recipient data in transit via HTTPS
- ✅ Reports saved locally (not uploaded)
- ✅ Logs contain no passwords
- ✅ Can safely commit project to public repo
Best Practices:
- Never share
.envfile - Rotate credentials periodically
- Monitor logs for errors
- Keep dependencies updated
Provider: Text-Ware SMS Gateway
Request Details:
- Endpoint:
https://msg.text-ware.com/send_sms.php - Method: POST (with GET fallback)
- Protocol: HTTPS
- Timeout: 30 seconds
Required Parameters:
username- API username (from.env)password- API password (from.env)src- Sender ID (fromSMS_SOURCE)dst- Destination phone numbertext- Message body
Response:
- Format: JSON
- Success: HTTP 200 with gateway acceptance response, often including an operation ID
- Error: HTTP 400+ with error message
Delivery-status note:
- TextWare's public API docs show send endpoints and examples that include a
drflag, but this project does not currently have a documented public delivery-status lookup endpoint to poll - The app therefore records gateway acceptance and operation IDs, but does not claim confirmed handset delivery
Retry Strategy:
- Automatic on: 429, 500, 502, 503, 504
- Backoff: 1 second between attempts
- Max retries: 3 (configurable)
Required packages:
requests # HTTP library
python-dotenv # Environment variables
pandas # CSV handling
streamlit # Web dashboard
watchdog # Faster Streamlit file watching on macOS/Linux
pytest # Test runner
pytest-cov # Coverage reporting
Install all:
pip install -r requirements.txtBefore first use:
-
.envexists with SMS credentials - Bundled sample source is available, or upload/create
recipients.csv -
pip install -r requirements.txtcompleted - Internet connection working
- Python 3.8+ installed
Best option in the dashboard:
- Use Recipients > Upload CSV > Use now for a temporary in-memory list
- Use Save as recipients.csv only when you want a reusable saved list
Manual file edit also works when needed. Edit recipients.csv and add rows:
name,email,contact_number
New Person,email@example.com,07XXXXXXXXOr use a phone-only list:
contact_number
07XXXXXXXXpython clean_recipient_batches.pyFor a one-off file:
python clean_recipient_batches.py --file "resources/input/your-file.csv"- Recommended: edit the message directly in the Streamlit Campaigns tab
- Optional: edit
resources/message_template.txtif you want to change the default template shown in new app sessions and used by the CLI
Reduce rate_limit_delay in sms_sender.py (faster but more API requests)
Increase max_retries in sms_sender.py (slower but fewer failures)
cat reports/sms_report_*.json | python -m json.tooltail -f logs/sms_sender_*.logStep 1: Choose recipients
Recommended path:
- Start with the
Samplesource for a quick end-to-end test - Or upload your own CSV and click Use now to work without writing a file
Optional saved path:
# Edit recipients.csv with your recipients
nano recipients.csvStep 2: Customize message (optional)
Recommended path:
- Edit the draft message directly in the Streamlit Campaigns tab
- This uses the updated message immediately without rewriting files
Optional default-template edit:
# Edit the default message template if needed
nano resources/message_template.txtStep 3: Send test SMS
python main.py --testStep 4: Check result
cat reports/sms_report_*.json | python -m json.toolStep 5: Send full campaign
python main.py --bulkStep 6: Review results
cat reports/sms_report_*.json | python -m json.tool
tail -f logs/sms_sender_*.log- Load recipients from the selected source (
Sample,recipients.csv, or an imported in-memory list) - Personalize message - replace
{name}with recipient name - Send SMS via Text-Ware API
- Rate limit - wait 2 seconds before next SMS
- Handle response - log result
- Retry on failure - up to 3 attempts with backoff
- Generate report - save all results to JSON
- Create logs - record all events
- Network timeout → Automatic retry
- Connection error → Automatic retry with backoff
- HTTP error (400-599) → Log and report
- Missing data → Skip with warning
- Invalid CSV → Error message with details
- Prevents API overload by spacing requests
- 2 seconds between SMS (default)
- Configurable based on API limits
- Important for reliability
- Tries 3 times on failure
- Exponential backoff: 1s, 2s, 4s
- Handles transient failures
- Comprehensive error reporting
-
Clone/download project to
/Users/sayuru/PycharmProjects/sms-sender-python-textware -
Install dependencies:
source venv/bin/activate pip install -r requirements.txt -
Verify setup:
python main.py --streamlit
-
Choose recipients: start with the
Samplesource, upload a CSV and click Use now, or editrecipients.csvif you want a saved list -
Send SMS:
python main.py --bulk
-
View results:
cat reports/sms_report_*.json | python -m json.tool
If something is not working as expected, start with these checks:
- Review
logs/sms_sender_*.logfor request, retry, and error details - Inspect
reports/sms_report_*.jsonto confirm which recipients succeeded, failed, or were skipped - Run
python main.py --testto verify credentials and API connectivity before a full campaign - Confirm
.envcontains valid Text-Ware credentials and sender configuration - Reinstall dependencies with
pip install --upgrade -r requirements.txtif your environment is out of sync
For Streamlit-specific issues, restart the app with:
python main.py --streamlitFor test execution and local verification, use:
python -m pytest --cov=. --cov-report=term-missing- GitHub: sayuru-akash/sms-sender-python-textware
- Dashboard:
python main.py --streamlit - CLI test send:
python main.py --test - Full campaign:
python main.py --bulk - Interactive menu:
python quickstart.py
For bugs, regressions, or feature requests, use the GitHub issue tracker:
When reporting a problem, include:
- the command you ran
- whether you used
sample-recipients.csv,recipients.csv, or an imported in-memory list - the relevant error from
logs/sms_sender_*.log - the related report file from
reports/if a campaign started
Contributions are welcome through GitHub pull requests:
- Fork the repository
- Create a branch for your change
- Run
pytest --cov=. --cov-report=term-missing - Update documentation when behavior changes
- Open a pull request with a clear summary
Keep changes focused, avoid committing .env, and include tests for any behavior change in the sender, CLI, or Streamlit app.