SMS Signals
Matched messages, straight to your endpoint.
$ composer require smssignals/nativephp-sms-signals
Receive incoming SMS in NativePHP Mobile apps and forward matched messages straight to an HTTPS signals endpoint. For sideloaded and enterprise (MDM) distribution only — RECEIVE_SMS is restricted on Google Play.
- Pattern-matched forwarding to your HTTPS signals endpoint
- Runtime permission flow handled for you
- Enterprise (MDM) and sideload distribution only — not Play Store
Documentation
SMS Signals for NativePHP Mobile
Receive incoming SMS inside a NativePHP Mobile (v4) app and forward matched messages straight to an HTTPS endpoint — evaluated and delivered entirely on-device, in the background, with an offline retry queue.
[!CAUTION] This plugin uses the Android
RECEIVE_SMSpermission, which Google Play heavily restricts. Apps that read SMS are rejected from the Play Store unless they are the user's default SMS handler (which this plugin is not). Ship this only through sideloading or enterprise / MDM distribution (e.g. a managed device fleet, an internal APK, or a private app catalogue). If your app must go on Google Play, do not use this plugin — use a server-side SMS provider webhook (Twilio, Vonage, etc.) instead. Seedocs/DESIGN.mdfor the full policy and threat discussion.
[!NOTE] Android only. iOS provides no public API to intercept SMS; there is no iOS implementation and none is possible.
What it does
-
A
SMS_RECEIVEDBroadcastReceiver(system-delivered, no Activity required) picks up every incoming SMS. -
Optional on-device sender / body regular expressions decide whether the message is relevant. Non-matching messages are dropped on the phone and never leave it.
-
Matched messages are POSTed directly to your configured HTTPS endpoint with a bearer token:
{ "type": "sms", "payload": { "from": "+15550001111", "body": "Your code is 123456", "received_at": "2026-07-23T10:04:00Z" } } -
If the device is offline (or the server errors), the message is persisted to an encrypted on-device retry queue and re-sent by a network-constrained
WorkManagerworker.
It pairs with any Laravel backend — you expose one authenticated POST route that ingests the
envelope. Nothing about the plugin is tied to a particular app.
Requirements
- NativePHP Mobile
^4.0(works today against the4.0.0-rc.1 as 3.99.99alias — see below), or^3.0. - Android
minSdk26+. - PHP 8.2+, Laravel 11 / 12.
Installation
composer require smssignals/nativephp-sms-signals
NativePHP plugins are fail-closed: a plugin is only compiled into your app if your app's allowlist names its service provider. Register it:
php artisan native:plugin:register smssignals/nativephp-sms-signals
That command adds the provider to app/Providers/NativeServiceProvider::plugins(). Confirm it
looks like this:
// app/Providers/NativeServiceProvider.php
public function plugins(): array
{
return [
// ...your other plugins...
\SmsSignals\Sms\SmsServiceProvider::class,
];
}
Then (re)build the native project:
php artisan native:install --force
php artisan native:run
Validate the plugin manifest at any time:
php artisan native:plugin:validate
Configuration
Publish the config if you want to tune timeouts / queue sizes (optional):
php artisan vendor:publish --tag=sms-signals-config
Key options (config/sms-signals.php): connect_timeout_ms, read_timeout_ms, max_queue_size,
max_retry_attempts, show_status_notification, plus optional env-driven endpoint / token /
sender_pattern / body_pattern defaults for single-tenant builds.
In practice you seed the endpoint and a per-user token at runtime (see below), because the
token is user-scoped and secret. The plugin stores it in EncryptedSharedPreferences.
Usage
use SmsSignals\Sms\Facades\Sms;
// 1. Ask for the runtime RECEIVE_SMS permission (and POST_NOTIFICATIONS on Android 13+).
Sms::requestPermission();
// 2. Seed the on-device encrypted config store. Endpoint MUST be https://.
Sms::configure([
'endpoint' => 'https://api.example.com/mobile/signals',
'token' => $user->currentAccessToken()->plainTextToken, // per-user bearer
'sender_pattern' => '^(\\+?1)?(12345|BANKCO)$', // java.util.regex, on-device
'body_pattern' => '\\b\\d{4,8}\\b', // e.g. only messages containing an OTP
]);
// 3. Start forwarding.
Sms::start();
// Later…
Sms::status(); // ['enabled' => true, 'configured' => true, 'encrypted_store' => true, 'queued' => 0, 'permission' => 'granted']
Sms::stop(); // pause forwarding (receiver stays registered, messages dropped)
Sms::flushQueue(); // manually retry anything queued while offline
Listening for events (SuperNative)
use SmsSignals\Sms\Events\SmsReceived;
use SmsSignals\Sms\Events\SmsForwarded;
use SmsSignals\Sms\Events\SmsForwardFailed;
use Native\Mobile\Attributes\OnNative; // provided by nativephp/mobile
#[OnNative(SmsReceived::class)]
public function whenSmsArrives(SmsReceived $e): void
{
// $e->from, $e->body, $e->receivedAt
}
Inbound events are best-effort while the app is foregrounded; the background forward to your endpoint happens regardless of whether the app is open. Events raised while the app is closed are queued and replayed on next launch.
Pairing with your Laravel signals endpoint (generic example)
The forward envelope is small and predictable, so the receiving route is a few lines. Authenticate the bearer token, validate the shape, then hand off to your own domain logic.
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/mobile/signals', function (Request $request) {
$data = $request->validate([
'type' => 'required|in:sms',
'payload.from' => 'required|string|max:64',
'payload.body' => 'required|string|max:2000',
'payload.received_at'=> 'required|date',
]);
// $request->user() resolves from the Bearer token (e.g. Sanctum).
IncomingSignal::create([
'user_id' => $request->user()->id,
'channel' => 'sms',
'sender' => $data['payload']['from'],
'body' => $data['payload']['body'],
'received_at' => $data['payload']['received_at'],
]);
return response()->json(['ok' => true], 202);
})->middleware('auth:sanctum');
Return any 2xx to acknowledge; anything else makes the device queue and retry.
Web-view apps (Livewire / Inertia)
SuperNative apps use the Sms facade directly. If you build a web-view app, a JS wrapper is
provided at resources/js/index.js (configure, start, stop, status, requestPermission,
checkPermission, flushQueue, and an Events map), and an <x-sms-signals::init /> Blade
component that checks the permission on load.
Security notes
- The endpoint is enforced to be
https://in both PHP (config validation) and Kotlin (before the POST). Plaintexthttp://is refused because SMS bodies frequently contain OTPs / PII. - The token and filters live in
EncryptedSharedPreferences(AndroidKeyStore-backed). If the keystore is unavailable the plugin falls back to plaintext storage and flags it instatus()(encrypted_store: false). - Keep your
sender_pattern/body_patternnarrow. Every non-matching message is discarded on-device; only what matches is transmitted.
See docs/DESIGN.md for architecture decisions and what still requires on-device
validation.
License
Proprietary placeholder — the repository owner decides the terms. See LICENSE.