Skip to content

Building a Reliable SMS-to-Webhook Gateway on Android

Forwarding an SMS to a webhook looks trivial until real conditions hit it: dropped networks, reboots, battery killers, duplicate messages and forgery. This is how to engineer a gateway that survives all of them.

By Karani Geoffrey, Founder & CEO, Upeosoft
In short

A reliable SMS gateway needs four things a naive script lacks: a durable offline queue so nothing is lost when the network fails, exactly-once delivery via message hashing and nonces, HMAC signing so the server can trust each message, and a persistent foreground service plus a scheduler backstop so it survives reboots and battery optimisation. Get those right and forwarding becomes trustworthy.

Key takeaways
  • The naive read-SMS-then-POST loop fails on dropped networks, reboots, battery killers, duplicates and forged messages.
  • Store every message in a durable, encrypted on-device queue before you try to send it.
  • Sign each message with HMAC over a canonical byte form so the server can reject anything not from your device.
  • Guarantee exactly-once with a message hash plus nonce, and have the server report duplicates it already stored.
  • Run a foreground service for real-time sync and a scheduled worker as a backstop against OS throttling.
  • Reject stale timestamps and keep secrets in the platform keystore so they never travel over the wire.

The problem is reliability, not reading SMS

Reading an incoming SMS on Android is a solved problem: register a receiver, get the message, done. That is why people underestimate this. The actual engineering problem is delivering that message to a backend reliably, under conditions that are actively hostile to background work - flaky mobile data, aggressive battery management, reboots, and an OS that would rather kill your process than let it run.

When the payload is a payment, reliability is not a nice-to-have. A dropped message is a payment your system never learns about. So the design has to assume that everything that can fail will, and lose nothing when it does. Upeosoft's SMS Gateway is built around that assumption, and it is open source (github.com/Upeosoft-Limited/upeo-sms-gateway), so the patterns below can be read in full rather than taken on faith.

Durability first: queue before you send

The foundational rule is that a message is persisted before any network attempt. The receiver's only job is to write the incoming SMS into a durable local store; sending happens separately against that store.

  • Write the message to an encrypted database (SQLCipher) the instant it arrives.
  • A separate sync path reads pending rows and attempts delivery.
  • A row is only marked delivered after the server acknowledges it.
  • Failed sends stay pending and are retried, so a crash or network drop never loses data.

Trust: sign every message

Your webhook is on the public internet, so the server must be able to prove that a message genuinely came from your device and was not tampered with. That is what HMAC-SHA256 signing provides. The device computes a signature over a canonical byte representation of the message using a shared secret, and the server recomputes the same signature to verify it.

Two details make this actually work. First, canonicalisation: device and server must serialise the message identically, byte for byte, or genuine messages will fail verification. Second, secret handling: the shared secret lives in the Android Keystore and is used only to compute the signature - it never travels over the wire. The server should also reject messages with a stale timestamp, so an intercepted request cannot be replayed hours later.

Exactly-once: hashes, nonces and duplicate reporting

SMS and networks both create duplicates. M-Pesa-style confirmations can be re-read, retries can double-send, and the OS can redeliver. If a duplicate becomes a second ledger entry, you have corrupted the books. Exactly-once semantics prevent that.

The mechanism is a stable message hash - derived from the canonical content - plus a per-send nonce. The server keeps a record of hashes it has already processed and returns a duplicate status when it sees one again, so the second arrival is acknowledged but not re-recorded. On the device side, delivery is idempotent: because a message is only marked done on acknowledgement, retrying a message the server already has is safe. The contract between device and server is what makes 'at least once' transport behave as 'exactly once' in effect.

Liveness: foreground service plus a scheduled backstop

None of the above matters if the app is not running when messages arrive or when the network returns. Android will throttle or kill background work, and OEM battery managers are worse. The answer is layered liveness.

A persistent foreground service owns SMS reception and the primary, real-time sync path - it is visible to the user and far harder for the OS to kill. Behind it, a scheduled worker (WorkManager) runs on an interval as a backstop, sweeping the pending queue in case the service was ever suspended. A boot receiver restarts the whole thing after a reboot or an app update. And because OEM behaviour varies wildly, first-run setup should walk the operator through the battery-optimisation exemption for their specific device, since that step is what makes long-term reliability hold.

The retry policy that ties it together

Retries need to be aggressive enough to recover quickly but polite enough not to hammer a struggling server or drain the battery. Exponential backoff with full jitter is the standard answer: each failed attempt waits longer than the last, jitter spreads retries so many devices do not sync in lockstep, and a cap keeps the interval bounded. A maximum attempt count with a sensible ceiling prevents a permanently failing message from retrying forever.

Put together - durable queue, signed messages, exactly-once contract, layered liveness, and disciplined retries - these turn 'read an SMS and POST it' into a gateway you can trust with real money. That is the difference between a demo and production, and it is the same difference we build for whenever a business depends on a message actually arriving.

Frequently asked questions

Why not just read the SMS and POST it to a webhook?

Because that loop assumes conditions that do not hold in the field. If the network is down at the moment the SMS arrives, the POST fails and the message is gone. If the phone reboots, your listener is dead until someone opens the app. If Android's battery optimisation kills the process, sync silently stops. And with nothing signed, anyone who learns your endpoint can inject fake payments. Each of those is a real failure, and each needs a specific fix.

How do you guarantee exactly-once delivery?

You give every message a stable identity - a hash of its canonical content - and attach a nonce to each send. The server stores processed message hashes and returns a duplicate status when it sees one again, so a message delivered twice is recorded once. On the device, the queue only marks a message done after the server acknowledges it, so a crash mid-send results in a safe retry rather than a lost message.

What is canonicalisation and why does it matter for signing?

Canonicalisation means both the device and the server agree, byte for byte, on how a message is serialised before it is signed. If the device signs one byte layout and the server reconstructs a slightly different one, the HMAC will not match even for a genuine message. Fixing field order, encoding and separators up front is what makes signature verification work consistently.

How do you survive Android battery optimisation?

With two layers. A persistent foreground service owns SMS reception and the primary sync path, which keeps the app alive and visible to the user. Then a scheduled worker runs on an interval as a backstop, sweeping the queue in case the service was ever throttled or killed. A boot receiver restarts everything after a reboot or app update. Belt and braces, because no single mechanism is guaranteed across every OEM.

Where should the shared secret live?

In the platform keystore, never in plain preferences or in the message payload. The secret is used on-device to compute the HMAC, and only the signature travels over the wire. Storing it in Android's Keystore means it is hardware-backed where available and never leaves the device, so intercepting traffic does not reveal it.

Karani Geoffrey
Karani Geoffrey
Founder & CEO, Upeosoft

Karani Geoffrey is the Founder & CEO of Upeosoft, a software and automation company rooted in Kenya. He builds custom software, AI systems, and production-grade ERPNext for businesses across East Africa, and writes about the Kenyan realities - eTIMS, M-Pesa, SHIF, unreliable internet and power - that make or break real systems.

Next step

Want this working in your business?

Upeosoft builds and hardens the systems behind this article - for real Kenyan operations, with eTIMS, M-Pesa and offline realities handled.

Keep reading

Kenyan Compliance and Integrations

How to Automatically Record M-Pesa Payments from the Confirmation SMS

Every M-Pesa payment sends a confirmation SMS. A small Android gateway can forward that message to your system the instant it arrives, so payments record themselves instead of being typed in by hand.

7 min readRead article →
For Builders

Bridging a Web App to a Bluetooth Thermal Printer with a WebView Bridge

Browsers cannot reach a Bluetooth Classic thermal printer, but a native WebView host can. This is how to build a safe JavaScript bridge, generate ESC/POS, and keep the printer connection alive.

7 min readRead article →
For Builders

Building Offline-First Web Apps: Lessons From Kenya

How to build web apps that keep working when the network does not: service workers, IndexedDB, a durable sync queue, and conflict resolution, drawn from shipping software for real Kenyan connectivity.

8 min readRead article →