Self-Hosted SMS Gateway
Send SMS through your own Android phone
- Year
- 2024
- Kind
- Systems
- Status
- Finished
$0.00059 per SMS
A Django API that sends SMS through an Android phone you own, at about $0.00059 a message. Your code calls one endpoint, a Firebase push wakes the phone, and it sends the text from its own SIM.
A self-hosted SMS gateway in two parts. A Django REST Framework server takes a message and a number at /api/send-sms/, logs it and pushes it to the account's phone as a Firebase Cloud Messaging data message. A Flutter app on the phone catches the push, even in the background, and sends the SMS from its own SIM, so a text costs only what the phone's plan charges: about $0.00059. Accounts and devices are managed in the Django admin.
The problem
SMS APIs charge for every message, yet a spare Android phone with a SIM plan can already send texts. A small Django server takes an API call and hands the message to a registered phone, which sends it from its own number. Each message costs $0.00059.
How it works
- 01
Register the phone
The Flutter app on the phone signs in to the Django server and gets a JWT, a signed token it attaches to every request. It then reports its Firebase Cloud Messaging (FCM) token, the address Google's push service uses to reach this install, and sends it again whenever FCM rotates it. Each account has one phone, and registering a new one replaces the old.
- 02
Your code calls one endpoint
To send a text, your service POSTs a message and a recipient number to /api/send-sms/ with the account's JWT. Django REST Framework checks the token, looks up the account's phone and logs the message in SQLite. Each phone can carry a daily limit; once it is reached the endpoint answers 429 until the next day (UTC), and a server without Firebase credentials answers 503 instead of claiming success. Accounts are created in the Django admin; there is no sign-up flow.
- 03
Push to the phone through FCM
The server never connects to the phone directly. It sends an FCM data message, a silent push that carries a payload instead of showing a notification, with the text and the number inside, at high priority, so Android delivers it at once even when the phone is idle. Google already keeps a channel open to every Android phone, so the phone needs no public address and no open port.
- 04
The phone sends it from its SIM
When the push lands, a background handler runs: a Dart function marked as an entry point, so Android can start it even when the app is closed. It passes the text and number to the flutter_background_messenger plugin, which sends the SMS through Android's own SMS service. The message goes out from the phone's number, on its plan. When the service is stopped in the app, the handler drops incoming messages instead of saving them up to send later.
Results
- $0.00059
- per SMS, sent from the phone's own SIM
Decisions
Push, not polling
The phone doesn't ask the server for work. FCM wakes the app when there's a message, so the phone holds no connection of its own and the server needs no way to reach it.
Django admin instead of a Flutter admin
Users, devices and the message log are managed in Django's built-in admin, themed with django-unfold.
Retry with backoff on the phone
When the server answers 502, the phone's API client retries up to 5 times, waiting 1, 2, 4, 8 and then 16 seconds. A phone that reaches the server mid-restart tries again instead of losing its token update.