Browse by type
This is a free, open-source Android app that automatically forwards incoming SMS messages to a specified URL as JSON via HTTP POST. * Forward SMS from specific numbers, all senders, or senders matching a regular expression. * Retries failed requests with exponential backoff. * Optionally stores messages that exhaust all retries so you can re-send them later. * Includes sender, message, timestamp, SIM slot, and more. * Forward messages directly to Telegram bots or channels. * Optional heartbeat ping so an external monitor can alert you if the phone goes offline. * Built-in test message sender and error log viewer. * No cloud services or user registration required.
If you've used or tested it, please take a minute to fill out this short survey – it helps me understand real-world usage and prioritize new features. Thank you!
Download apk from release page
Or download it from F-Droid
It can't be. Google Play's SMS & Call Log permissions policy
only allows the RECEIVE_SMS permission for a short list of approved use cases
(such as being the device's default SMS handler). "Forward incoming SMS to a
user-defined URL" is not one of them, so an app built around that permission is
not eligible for the Play Store, regardless of how it's written. Please install
from the GitHub releases page
or from F-Droid
instead — that is the intended, supported way to install the app.
This is a false positive, and it is expected. Play Protect flags the app because reading incoming SMS and sending their contents to a remote URL is the same behavior pattern used by SMS-stealing malware. Play Protect is an automated classifier — it cannot tell that you configured the destination URL and you consented, so it errs on the side of warning. The app has no backend and no accounts, and only ever sends SMS to the URL you enter yourself. The full source is in this repository, so you can verify exactly what it does.
To install anyway:
If you'd rather not disable Play Protect, the F-Droid build is signed with a stable key whose reputation improves over time, which reduces how often the warning appears.
Set up App Permissions for you phone after installation. For example, enable "Autostart" if needed and "Display pop-up windows while running in the background" from Xiaomi devices.
Set sender phone number or name and URL. It should match the number or name you see in the SMS messenger app. If you want to send any SMS to URL, use * (asterisk symbol) as a name. To match a family of senders (e.g. a fixed sender ID with a rotating operator prefix), enable Sender is a regular expression — see Match the sender with a regex below.
Every incoming SMS will be sent immediately to the provided URL. If the response code is not 2XX or the request ended with a connection error, the app will try to send again up to 10 times (can be changed in parameters). Minimum first retry will be after 10 seconds, later wait time will increase exponentially. If the phone is not connected to the internet, the app will wait for the connection before the next attempt.
If at least one Forwarding config is created and all needed permissions granted - you should see F icon in the status bar, means the app is listening for the SMS.
On Android 13 and newer the app also asks for the notification permission. SMS forwarding works without it, but the F status-bar indicator only appears once it is allowed. If you don't see the icon, check that notifications are enabled for the app (and that the system hasn't restricted it in the battery settings).
Press the Test button to make a test request to the server.
Press the Syslog button to view errors stored in the Logcat.
This app forwards SMS only. If some messages never arrive at your URL, your
phone may be delivering them over RCS (Google's chat protocol used by Google
Messages) instead of SMS. RCS is a different transport that does not trigger the
SMS_RECEIVED broadcast this app relies on, and Android provides no public API
for third-party apps to receive RCS messages — so it cannot be supported (see
issue #46).
Workaround: turn off RCS in Google Messages (Settings → RCS chats → Turn off RCS chats). Messages will then arrive as normal SMS and be forwarded correctly.
By default the sender is matched exactly (or use * to catch every sender). Tick
Sender is a regular expression to instead treat the sender field as a Java regular
expression (java.util.regex.Pattern, the same flavour as the text filter below). The
expression is tested against the incoming sender as a substring match (find()), so it
matches when the pattern occurs anywhere in the sender. For example BANK matches
VM-BANK and AD-BANK, useful where an operator prefixes a fixed sender ID with a
rotating code. Prefix (?i) to ignore case. A bad pattern matches nothing
(fails closed), so a typo never silently forwards unrelated senders. The * wildcard
keeps meaning "any sender" even with this option enabled.
By default a rule matches on the sender only and forwards every SMS from it. The optional
Text filter (regex, optional) field narrows this down by the message body: leave it empty to
forward everything (the historic behaviour), or enter a Java regular expression and the rule will
forward a message only when that expression matches the body (substring match, case-sensitive —
prefix (?i) to ignore case). It combines with the sender filter, so use sender * to filter purely
on content (see issue #52). A bad pattern is ignored (the message is still forwarded) so a typo never
silently drops SMS.
A single regex covers both directions:
OTP forwards any message
containing "OTP"; OTP|verification code forwards messages containing either phrase.(?s)^(?!.*OTP) forwards every message that does not contain "OTP";
(?si)^(?!.*(spam|promo)) excludes (case-insensitively) anything containing "spam" or "promo".
The leading (?s) makes . span newlines so a keyword on any line of a multi-line SMS is caught.Selecting this option will allow you to sign the request with a provided secret. The hex signature
is created from the request payload and the provided secret, and will be added to the request with
the header X-Signature.
This is a per-rule option in the forwarding config. When enabled, any message that exhausts all of its automatic retries (or fails permanently) is kept on the device instead of being dropped. The action bar then shows a Retry N failed item — tap it to re-send every stored message. Messages that fail again stay in the store so you can try once more later.
Open Settings from the action bar to enable a periodic heartbeat. While enabled, the app POSTs to a URL you provide at a chosen interval (in minutes), so an external dead-man's-switch monitor (e.g. healthchecks.io, Uptime Kuma push monitors, cronitor) can alert you if the phone dies, is killed, or loses connectivity and the pings stop. Lower intervals detect failures sooner but use more battery and data. Use the Test button to send one ping immediately.
By default a delivery waits for a validated internet connection before it runs. If your webhook lives on the local network (e.g. Home Assistant or a Raspberry Pi on a Wi-Fi without upstream internet), that wait never ends and nothing is forwarded. Enable Local network mode in the rule's advanced parameters to drop the internet requirement: the request is sent immediately on whatever network the phone has (see issue #83). Failed requests still retry with the usual exponential backoff.
An advanced per-rule switch controls how the POST body is streamed. New rules default to
fixed-length (a normal Content-Length request), because many webhook servers — notably common
PHP setups — receive a chunked (Transfer-Encoding: chunked) body as empty (see issue #97).
Existing rules keep whatever mode they were created with; if your PHP endpoint sees an empty
php://input, edit the rule and turn Chunked mode off.
Open Settings from the action bar to export all forwarding rules to a JSON file, or import a previously exported file — useful for backups and for migrating to a new phone (see issue #76). Importing merges by rule: rules from the file overwrite the existing rules they were exported from and new ones are added, so re-importing the same file doesn't create duplicates. The export file contains your webhook URLs, custom headers and HMAC secrets in plain text — store it accordingly. Heartbeat settings and stored failed messages are not part of the backup.
HTTP method: POST
Content-type: application/json; charset=utf-8
Sample payload:
{
"from": "%from%",
"text": "%text%",
"sentStamp": "%sentStamp%",
"receivedStamp": "%receivedStamp%",
"sim": "%sim%"
}
Available placeholders: %from% %text% %sentStamp% (time the SMS was sent, Unix epoch in milliseconds; or %sentStamp=% for a formatted date) %receivedStamp% (time the SMS was received by the device, Unix epoch in milliseconds; or %receivedStamp=% for a formatted date) %sim% %version% (app version name) %battery% (battery charge 0-100, or -1 if unknown) %power% (power source: ac / usb / wireless / unplugged / unknown) %network% (active network: wifi / mobile / ethernet / other / none) %Regex=...%
The device-health placeholders %version%, %battery%, %power% and %network%
report the phone's state at the moment the SMS is forwarded — useful for monitoring
a fleet of devices (see issue #39). %battery% is a number, so use it unquoted
("battery": %battery%); the others are strings and go inside quotes. None of them
requires an extra runtime permission.
%sentStamp% and %receivedStamp% are Unix timestamps in milliseconds (the
Android/Java convention), e.g. 1768556698000, not the 10-digit seconds form. If
your receiver expects seconds, divide by 1000 (drop the last 3 digits). They are
plain numbers, so use them unquoted ("sentStamp": %sentStamp%).
%sentStamp=<format>% / %receivedStamp=<format>%If your receiver can't process epoch milliseconds (e.g. a no-code webhook, a
spreadsheet, or a chat message), append =<format> to either stamp to insert a
human-readable date instead, e.g. %sentStamp=yyyy-MM-dd HH:mm:ss% →
2026-06-07 14:30:00 (see issue #42).
<format> is a Java SimpleDateFormat
pattern (yyyy year, MM month, dd day, HH hour, mm minute, ss second,
and so on). The pattern runs up to the first %.XXX for the offset, e.g. yyyy-MM-dd'T'HH:mm:ssXXX → 2026-06-07T14:30:00+02:00)."sentDate": "%sentStamp=yyyy-MM-dd HH:mm:ss%") — unlike the bare numeric
%sentStamp%, which is used unquoted.The bare %sentStamp% / %receivedStamp% (no =) keep emitting epoch
milliseconds exactly as before, and both forms can be combined in one template:
{
"sentStamp": %sentStamp%,
"sentDate": "%sentStamp=yyyy-MM-dd HH:mm:ss%"
}
%Regex=...%Use %Regex=<pattern>% to forward only a part of the SMS body instead of the whole
%text% — handy for pulling out an OTP code, a transaction amount, etc.
The pattern is a standard Java regular expression applied to the message text:
( ... ), the content of the first
group is inserted.Matching is case-sensitive; prefix the pattern with (?i) to make it
case-insensitive. The extracted value is JSON-escaped, so it is safe to place
inside a JSON string. The first unescaped % ends the
$ claude mcp add android_income_sms_gateway_webhook \
-- python -m otcore.mcp_server <graph>