ux2dev / link-mobility
PHP library for the LINK Mobility MessageHub API (SMS, Viber, Push, Voice/TTS, delivery reports, two-way callbacks)
Requires
- php: ^8.1
- ext-json: *
- ext-openssl: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- larastan/larastan: ^3.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- rector/rector: ^2.0
Suggests
- guzzlehttp/guzzle: Supplies PSR-18 client + PSR-17 factories out of the box
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
This package is auto-updated.
Last update: 2026-07-19 11:10:57 UTC
README
A framework-agnostic PHP client for the LINK Mobility MessageHub API with an optional, opinionated Laravel integration. Send SMS, Viber (text + templates), Push and Voice/TTS messages; query delivery status; manage Viber templates; and parse inbound callbacks (delivery reports, two-way/MO messages, template approvals) into typed objects - with optional Eloquent persistence.
- Core (
Ux2Dev\LinkMobility\): no Laravel required, built on PSR-18 / PSR-17. - Laravel layer (
Ux2Dev\LinkMobility\Laravel\): facade, callback route, events, opt-in queued persistence, models, migrations, and Artisan commands.
Requirements
- PHP
^8.1,ext-openssl,ext-json - A PSR-18 HTTP client + PSR-17 factories (e.g.
guzzlehttp/guzzle ^7)
Installation
composer require ux2dev/link-mobility
For Laravel, publish the config (and optionally the migrations / routes):
php artisan vendor:publish --tag=link-mobility-config php artisan vendor:publish --tag=link-mobility-migrations # optional persistence php artisan vendor:publish --tag=link-mobility-routes # optional custom routes
Configuration
Set these in .env:
LINK_MOBILITY_ENV=production # or "testing" LINK_MOBILITY_API_KEY=your-api-key LINK_MOBILITY_API_SECRET=your-api-secret LINK_MOBILITY_SERVICE_ID=1 # optional default service_id LINK_MOBILITY_SC=YourSender # optional default sender / shortcode # Optional webhook protection (see "Callbacks" below) LINK_MOBILITY_WEBHOOK_SECRET= LINK_MOBILITY_WEBHOOK_TOKEN= # Optional persistence LINK_MOBILITY_PERSIST=false
Authentication is automatic: every request is signed with
x-api-sign = HMAC-SHA512(rawJsonBody, api_secret) and sent with your x-api-key.
Sending messages
Laravel (facade)
use Ux2Dev\LinkMobility\Laravel\Facades\LinkMobility; use Ux2Dev\LinkMobility\Message\SmsMessage; $result = LinkMobility::send( SmsMessage::to('359888123456') ->text('Hello from MessageHub') ->callbackUrl(route('link-mobility.callback')) ); $result->smsId; // "light-5b83e6df35fe41.14520017" $result->requestId; $result->isOk(); // meta.code === 200
Framework-agnostic core
use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Psr7\HttpFactory; use Ux2Dev\LinkMobility\Client; use Ux2Dev\LinkMobility\Config\Config; use Ux2Dev\LinkMobility\Enum\Environment; use Ux2Dev\LinkMobility\Message\SmsMessage; $factory = new HttpFactory(); $client = new Client( new Config('api-key', 'api-secret', Environment::Production, defaultServiceId: '1'), new Guzzle(), $factory, // PSR-17 request factory $factory, // PSR-17 stream factory ); $result = $client->send(SmsMessage::to('359888123456')->text('Hi'));
Channels
use Ux2Dev\LinkMobility\Message\{SmsMessage, ViberMessage, PushMessage, TtsMessage}; use Ux2Dev\LinkMobility\Enum\Priority; // SMS SmsMessage::to('359888123456')->text('Hi') ->priority(Priority::High)->delay('+11 hours')->unique(); // Viber text (with SMS fallback) ViberMessage::to('359888123456')->sc('ViberTest')->text('Viber message') ->ttl(120)->platform(1)->fallbackSms('SMS fallback'); // Viber template ViberMessage::to('359888123456')->sc('ViberTest') ->template('6c929cef-29b4-4349-bc9d-2a07bdbb6e43', ['pin' => '123456'], 'bg') ->fallbackSms('Your code: 123456'); // Push PushMessage::create()->sc('PushTest') ->title('Title')->body('Body')->uid('cloud.msghub.example_dfXXXXX') ->redirectUrl('https://example.com')->fallbackSms('SMS fallback'); // Voice / Text-to-Speech TtsMessage::to('359888123456')->sc('VoiceTest')->text('Spoken message') ->fallbackSms('SMS fallback');
For Viber rich content (carousels, surveys, buttons) not yet modelled as typed
builders, use ViberMessage::rich([...]) to merge a structured array into the
payload.
Batch send & delivery status
$results = LinkMobility::sendMany($msgA, $msgB, $msgC); // array<SendResult> $dlr = LinkMobility::dlr('light-5b83e6df35fe41.14520017'); $dlr->status; // DlrStatus enum (Delivered, Undelivered, ...) $dlr->status?->isDelivered();
Viber templates
$templates = LinkMobility::viberTemplates(); $templates->create([...]); // POST /viber_templates/create $templates->get([...]); // POST /viber_templates/get $templates->delete([...]); // POST /viber_templates/delete
Callbacks (delivery reports, two-way messages, template approvals)
Point your callback_url (and LINK Mobility's MO / template webhooks) at the
package route: POST /{prefix}/callback (default prefix link-mobility, so
/link-mobility/callback). The controller parses the payload, dispatches events,
and - for MO - can return a synchronous reply.
Webhook authentication
Verification is opt-in and fail-closed:
- Set
LINK_MOBILITY_WEBHOOK_SECRETto require a validx-api-signHMAC-SHA512 over the raw body on every callback. Missing or invalid ⇒401. - Otherwise set
LINK_MOBILITY_WEBHOOK_TOKENand configure your callback URLs with?token=<value>. - With neither set, the endpoint is unauthenticated (dev only) - secure it before production.
Events
| Event | Fired for |
|---|---|
DeliveryReportReceived |
every DLR callback |
MessageDelivered |
DLR with status = delivered |
MessageUndelivered |
DLR in a final non-delivered state |
InboundMessageReceived |
MO / two-way inbound message |
TemplateStatusChanged |
Viber template approval/decline |
use Ux2Dev\LinkMobility\Laravel\Events\MessageDelivered; Event::listen(MessageDelivered::class, function (MessageDelivered $e) { $e->report->smsId; $e->report->status; // DlrStatus });
Two-way replies
Bind an InboundMessageHandler; its return value becomes the synchronous reply
LINK Mobility sends back to the sender (return null for no reply):
use Ux2Dev\LinkMobility\Callback\Contracts\InboundMessageHandler; use Ux2Dev\LinkMobility\Callback\InboundMessage; $this->app->bind(InboundMessageHandler::class, fn () => new class implements InboundMessageHandler { public function handle(InboundMessage $message): ?string { return "You said: {$message->text}"; } });
Parsing callbacks without Laravel
The parser is part of the framework-agnostic core:
use Ux2Dev\LinkMobility\Callback\CallbackParser; $event = CallbackParser::parse($decodedJsonBody); // => DeliveryReport | InboundMessage | TemplateStatusUpdate
Optional persistence
Ship-and-forget storage for callbacks. Publish + run the migrations, then enable it:
LINK_MOBILITY_PERSIST=true
When enabled, queued listeners write to four tables/models
(OutboundMessage, DeliveryReport, InboundMessage, ViberTemplate).
Delivery reports link back to outbound messages by sms_id. Everything is
optional - the package is fully functional without the database.
Artisan commands
php artisan link-mobility:status # show resolved config + base URL php artisan link-mobility:send-test 359888123456 --text="Hello"
Exceptions
All extend Ux2Dev\LinkMobility\Exception\LinkMobilityException:
| Exception | Meaning |
|---|---|
ApiException |
Non-2xx HTTP or meta.code >= 400 (exposes metaCode, metaText, requestId, httpStatus) |
TransportException |
PSR-18 client / network failure |
SignatureException |
Empty signing secret |
ConfigurationException |
Missing/invalid credentials |
InvalidResponseException |
Malformed JSON |
UnrecognizedCallbackException |
Callback payload not identifiable as DLR / MO / template |
Testing & static analysis
composer install composer test # Pest test suite composer stan # PHPStan (level max, via Larastan) composer rector # Rector - preview refactors (dry-run) composer check # stan + test
The shipped code under src/ is analysed at PHPStan level max and is clean.
License
MIT. See LICENSE.