ux2dev/link-mobility

PHP library for the LINK Mobility MessageHub API (SMS, Viber, Push, Voice/TTS, delivery reports, two-way callbacks)

Maintainers

Package info

github.com/ux2dev/link-mobility

pkg:composer/ux2dev/link-mobility

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-07-18 06:09 UTC

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_SECRET to require a valid x-api-sign HMAC-SHA512 over the raw body on every callback. Missing or invalid ⇒ 401.
  • Otherwise set LINK_MOBILITY_WEBHOOK_TOKEN and 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.