Releases: remappingBrilliance/SMSAPI
Release list
SMS API First Release
SMSAPI — Android SMS/MMS/Call Gateway
A native Android application that acts as a transparent gateway between a physical SIM card and a REST API. It receives incoming SMS/MMS and forwards them to your server, polls your server for outbound messages to send, and logs call metadata (CDR) and call recordings.
Designed to run silently in the background as a persistent foreground service.
Features
- Receive SMS/MMS — incoming messages are POSTed to your server immediately
- Send SMS/MMS — polls your server for queued outbound messages and delivers them via the device's SIM
- Delivery status — reports send success/failure back to your server
- Call Detail Records (CDR) — logs inbound, outbound, and missed calls with timestamps and duration
- Call recording upload — uploads call recordings produced by the device's dialer app
Requirements
- Android device with a SIM card
- Android 16 (API 36) or higher
- Your own REST server implementing the API endpoints below
Setup
1. Build configuration
Copy secrets.properties.example to secrets.properties and fill in your server details:
api_host=your-api-host.example.com
api_key=your-api-key-here
These values are injected at build time via BuildConfig and used as the default values in the app's settings screen. They are never committed to source control.
2. Build and install
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apkOr open in Android Studio and run directly to a device.
3. Grant permissions
Open the app. The main screen shows the status of every required permission:
| Permission | Purpose |
|---|---|
| Receive / Read / Send SMS | SMS gateway core |
| Read Phone State + Phone Numbers | Device number, call state detection |
| Read Call Log | Outgoing number resolution, CDR |
| Read Media Audio | Access dialer's call recording files |
| Post Notifications | Foreground service notification |
| Draw Over Other Apps | Keeps the service process alive during calls |
Tap Request Permissions to prompt for all runtime permissions at once. Tap Grant Overlay to open the system overlay permission screen.
4. Set as default SMS app
For background MMS sending (no user interaction required), this app must be the device's default SMS app. Tap Set as Default SMS App on the main screen.
If not set as default, MMS will fall back to launching the system messaging app, which requires the user to manually tap Send.
5. Configure your server details
Enter your API host and key in the API Settings section. All endpoint paths are configurable and default to the paths documented below.
Tap Save Settings, then Test API Connection to verify connectivity.
How It Works
SMS/MMS receiving
SmsReceiver is a BroadcastReceiver registered for SMS_DELIVER (when default SMS app) and SMS_RECEIVED. When a message arrives, Android delivers all segments of a multipart SMS in a single broadcast as an array of PDUs. The app concatenates them into a single body before uploading, so your server always receives the complete message.
MMS messages are observed via MmsObserver, which watches the system MMS content provider and forwards new messages to the same /sms/receive endpoint with attachments encoded as base64.
SMS/MMS sending
PollingForegroundService runs a continuous polling loop (configurable interval, default 5 seconds). It calls GET /sms/send on your server, which returns a list of queued messages. Each message is dispatched via MessageSender, which routes to either SmsSender (standard Android SMS API) or MmsSender (WAP-209 PDU encoding). Delivery results are reported back via POST /sms/status.
The polling service is a dataSync foreground service that starts on boot and restarts if killed by the system.
Call Detail Records
CallReceiver is a BroadcastReceiver for ACTION_PHONE_STATE_CHANGED. It runs a state machine:
IDLE → RINGING incoming call arrives, number captured
RINGING → OFFHOOK call answered
IDLE → OFFHOOK outgoing call dialled
OFFHOOK → IDLE call ended → POST CDR
RINGING → IDLE missed call → POST CDR (state: "missed")
For outgoing calls, the number is not available in the broadcast on Android 10+. The app waits ~2.5 seconds after the call ends and resolves the number from the CallLog.
Call recording
Note: Direct audio capture from phone calls (
AudioSource.VOICE_COMMUNICATION) is blocked for non-system apps on stock Android (AOSP, GrapheneOS, Pixel). The Android audio policy manager restricts in-call capture to privileged processes. Any recording made this way will be silent.
The app therefore relies on the device's built-in dialer to produce the recording file. The dialer (a system/privileged app) can record both call legs and saves files to Recordings/CallRecordings/ on shared storage.
Two options for triggering dialer recording:
Option A — Accessibility auto-tap (GrapheneOS dialer, some OEMs):
Enable Record call content and Use Accessibility Services to try to press Record on every call in the app settings, then enable the Call Recording Assistant in Android Accessibility settings.
When a call is answered, CallRecordingAccessibilityService monitors the dialer's window events and performs a programmatic click on any button whose content description contains "record" (but not "stop"). This works reliably with the GrapheneOS dialer. The Google dialer does support a similar Record button but has an unavoidable system announcement to both parties ("This call is being recorded") and requires every contact to be individually opted in or "Record all unknown callers" to be enabled — making the accessibility tap less useful there.
Option B — Manual or automatic dialer recording:
Configure your dialer to record all calls automatically (where supported), then enable only Record call content in the app settings (leave the accessibility option off). The app will still pick up and upload any recording file it finds in CallRecordings/ after the call ends.
In both cases, after the call ends the app waits ~5 seconds for the dialer to finish writing the file, then queries MediaStore for an audio file in Recordings/CallRecordings/ with a DATE_ADDED timestamp at or after the call was answered. If found, it uploads the file via POST /call/recording and the local copy can be deleted.
Tap Upload Existing Recordings on the main screen to manually upload any files already sitting in CallRecordings/ that were never uploaded (e.g. from before the app was installed).
API Reference
All endpoints use HTTPS. All requests include:
Authorization: Bearer <api_key>
Base URL
https://{api_host}
Endpoint paths are fully configurable in the app. Defaults are shown below.
POST /api/v1/public/sms/receive
Called when the device receives an incoming SMS or MMS.
Request Content-Type: application/json
{
"message": {
"from": "+61400000000",
"to": "+61411111111",
"body": "Hello, world!",
"type": "text",
"timestamp": "Tue Jan 01 12:00:00 GMT+10:00 2008",
"attachments": [
{
"filename": "image.jpg",
"contentType": "image/jpeg",
"data": "<base64>"
}
]
},
"metadata": {
"receivedAt": "Tue Jan 01 12:00:00 GMT+10:00 2008",
"sender": "+61400000000",
"recipient": "+61411111111"
}
}type:"text"for SMS,"mms"for MMSattachments: present for MMS only; omitted for plain SMStimestamp: the timestamp from the SMS PDU (sender's carrier time)
Response — any 2xx is treated as success.
GET /api/v1/public/sms/send
Polled by the device on a configurable interval (default every 5 seconds) to retrieve queued outbound messages.
Request — no body; Authorization header only.
Response Content-Type: application/json
[
{
"id": "msg_abc123",
"to": "+61400000000",
"body": "Your verification code is 1234",
"type": "sms"
}
]- Return an empty array
[]when there are no messages queued. type:"sms"or"mms"- For MMS, include an
attachmentsarray matching the same structure as the receive payload. idis echoed back in the status update so you can correlate delivery results.
POST /api/v1/public/sms/status
Called after each send attempt to report delivery status.
Request Content-Type: application/json
{
"id": "msg_abc123",
"status": "sent",
"error": null
}status:"sent"on success,"failed"on errorerror: human-readable error string on failure,nullon success
Response — any 2xx is treated as success.
POST /api/v1/public/call/cdr
Called at the end of every call (answered or missed) when CDR logging is enabled.
Request Content-Type: application/json
{
"call": {
"state": "ended",
"direction": "inbound",
"phoneNumber": "+61400000000",
"startedAt": 1700000000000,
"answeredAt": 1700000060000,
"endedAt": 1700000120000,
"durationSeconds": 60
},
"device": {
"deviceManufacturer": "google",
"deviceModel": "Pixel 8",
"deviceBrand": "google",
"androidSdkVersion": 36,
"androidRelease": "16"
},
"loggedAt": "2024-11-15T10:41:07Z"
}| Field | Notes |
|---|---|
state |
"ended" for answered calls, "missed" for unanswered inbound calls |
direction |
"inbound" or "outbound" |
startedAt |
Epoch ms — RINGING time for inbound, OFFHOOK time for outbound |
answeredAt |
Epoch ms — OFFHOOK ti... |