
Programming
My own SMS gateway: a GSM modem, two SIM cards and an API for many systems
My systems send text messages about interrupted streams, failing information boards and changes spotted by backups. Instead of paying for a commercial gateway, I built my own: a Quectel M95 modem, two SIM cards, a Python API, signed webhooks, delivery reports and a panel with two-factor login.
05/11/2026
Several of my systems sometimes need to alert someone quickly. ChurchStream broadcasts report an interrupted stream, information boards and the kiosk report failures, and nightly backups detect changes in website code.
Emails are easy to miss and commercial SMS gateways charge per message. That's why I built my own solution: a small GSM modem in my home lab and a service that exposes it to other systems via an API.
(enlarge)Hardware: a modem for a few dozen zlotys
The heart of the gateway is the a-GSM II Shield board with the Quectel M95 modem. It has two SIM card slots and connects to the server via USB. Both cards are in the Play network: one for my systems, and another for the institution's applications.
The modem communicates using AT commands via a serial port. The service monitors whether the modem responds, is registered in the network, and checks its signal strength. The public state, without a key, refreshes every 30 seconds for monitoring purposes.
Version 1.0: A simple Python service
The first version was created in March 2026. It did what my scripts needed:
- sending individual SMS messages and bulk sending via a queue in Redis,
- message history in the database with filters by number, date and direction,
- switching SIM cards and receiving messages from an inactive card every 5 minutes,
- query limit per IP address,
- compatibility with the commercial SMSEagle SMS gateway's API, so ready-made integrations worked without changes.
Version 2.0: multiple clients per gateway
More applications started using the gateway, including an SMS panel for people. A single shared API key was no longer enough, so in November I rebuilt the gateway. A client is now an application or a group of integrations. Each one has:
- an assigned SIM card from which it sends and receives replies,
- its own API keys with permissions for sending, reading, status and SIM cards,
- an optional daily message limit,
- its own webhook URL for events.
The key is shown only once, when it is created. Only its SHA-256 hash is stored in the database. The old key from the configuration file belongs to the "Existing integrations" client, so nothing that already worked needed changing.
(enlarge)Long messages and Polish characters
The gateway operates in PDU mode. Long messages are sent as a single multi-part SMS, which the phone combines into one message. Text with Polish letters is encoded in UCS-2, allowing for 70 characters per SMS and 67 characters per part of a longer message.
Each SMS is sent with a request for delivery confirmation. This allows the gateway to know not only that it has forwarded the message to the network but also that it has reached the phone.
(enlarge)Signed webhooks
Instead of polling the gateway, an application can receive events: SMS received, sent, delivered, undelivered, failed or a change in gateway status. Each event is signed with HMAC-SHA256 together with the send time, so the client can check that it comes from the gateway and is not a replay.
When the client does not respond, the gateway retries after 10 seconds, 30 seconds, 2 minutes, 10 minutes, and 30 minutes, then every hour. After approximately 6 hours, it marks the event as failed, and you can manually retry it in the panel.
(enlarge)Admin panel
The gateway features a simple server-side rendered dashboard. Logging in requires a password and a code from an authenticator app, with sessions stored in the database. The dashboard includes:
- a dashboard showing traffic on both cards and a chart of the last 24 hours,
- API clients and keys,
- all messages with filters,
- daily statistics by client and card,
- webhooks with event content and manual retries,
- event log: logins, client and key changes, unusual messages from the modem.
(enlarge)
(enlarge)Go easy on bulk sending
The operator blocks cards that send a lot of messages in a short time without specifying the threshold. That's why bulk sending is deliberately slow:
- about 8 seconds between messages, with random jitter,
- a 5 minute pause after every 50 messages,
- at most 200 SMS parts per hour.
So, while sending notifications to several hundred people takes longer, the card won't be blocked in the middle of the process.
An SMS gateway is a small modem and a bit of patience. The hardest part wasn't sending the message, but making sure it arrived and that the carrier wouldn't block the card.
- Aleksander Kowal
What's next
For people who do not want to use the API, I built an SMS panel for sending notifications from the browser. Next on my list are downloading MMS content, since the gateway already recognises MMS notifications, and a LoRa network node as a separate application next to the gateway.
Stack
- Python, FastAPI and pyserial for handling the modem with AT commands,
- SQLAlchemy with SQLite, Redis as a bulk sending queue,
- Jinja2 template panel, Argon2id passwords, TOTP codes,
- Docker on a server in my homelab.
Was this useful?
New posts by e-mail
Every now and then, when a new post or project goes up. No ads.
Got an idea, a question or a comment? Write to me:Enable JavaScript to see the e-mail address
