Search by

beziworld / laravel-smsplanet

kamcw

Laravel integration for SMSPlanet.pl: SMS and MMS sending, notification channel, signed delivery webhooks and multiple accounts.

Package info

github.com/Beziworld/laravel-smsplanet

Homepage

Issues

pkg:composer/beziworld/laravel-smsplanet

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 16:23 UTC

This package is auto-updated.

Last update: 2026-11-04 16:36:47 UTC


README

tests Latest Version License

Laravel integration for SMSPlanet: SMS and MMS sending, personalised bulk messages, a notification channel, signed delivery webhooks, incoming two-way messages and multiple accounts.

Polska wersja dokumentacji

Features

  • Fluent SmsMessage (up to 10 000 recipients, personalisation with [%parametr1%]..[%parametr4%]) and MmsMessage
  • Scheduling, sender names, test mode, clear_polish, transactional channel, dispatch name
  • Polish numbers normalised to +48..., SmsCounter helper for GSM-7 / UCS-2 parts (and partsCount() via the API)
  • smsplanet notification channel, including on-demand notifications and per-message account selection
  • Delivery webhook (MESSAGE_NOTIFICATION_WEBHOOK) with HMAC-SHA256 signature verification and a Laravel event
  • Incoming messages of two-way numbers forwarded to a token-protected endpoint
  • Dispatch details, balance, sender names, blacklist, contacts, groups, link shortener and webhook management
  • Multiple accounts: SmsPlanet::connection('name') and SmsPlanet::using([...]) for tenant credentials
  • One exception hierarchy exposing the SMSPlanet error code
  • PHP 8.3+, Laravel 12 and 13, PHPStan level 8

Installation

composer require beziworld/laravel-smsplanet
php artisan vendor:publish --tag=smsplanet-config   # optional
SMSPLANET_TOKEN=your-api-token       # generated in the customer panel
# or the API key and password instead of the token:
# SMSPLANET_KEY=your-api-key
# SMSPLANET_PASSWORD=your-api-password
SMSPLANET_FROM=YourBrand             # approved sender name; "TEST" works for testing
SMSPLANET_TEST_MODE=false            # true: validate requests without sending

Sending messages

use Beziworld\SmsPlanet\Facades\SmsPlanet;
use Beziworld\SmsPlanet\Messages\SmsMessage;

// Shortcut
SmsPlanet::sendText('600 111 222', 'Your code is 1234');

// Full builder
$result = SmsPlanet::send(
    SmsMessage::create('Your order #1001 has shipped')
        ->to(['600111222', '+48700333444'])
        ->from('Shop')
        ->sendAt(now()->addHour())
        ->name('Order 1001')     // visible in the panel history
        ->clearPolish()          // ą to a, ł to l, ... (160 characters per part)
);

$result->messageId;    // id of the dispatch
$result->isBuffered(); // "B-..." ids are queued for 10-30 s and cannot be cancelled

// Personalised: one request, up to 100 recipients
SmsPlanet::send(
    SmsMessage::create('Hi [%parametr1%], your visit is at [%parametr2%].')
        ->toPersonalized('600111222', 'Jan', '10:00')
        ->toPersonalized('700333444', 'Ewa', '11:30')
);

Other options: test(), transactional() (one-time codes, must be enabled for the account), companyId().

use Beziworld\SmsPlanet\Messages\MmsMessage;

SmsPlanet::sendMms(
    MmsMessage::create('Promo')                       // subject
        ->from('48664079876')                         // MMS number shown in the panel
        ->to('600111222')
        ->content('-20% today')
        ->attachment('https://example.com/promo.jpg') // URL or Base64, 300 kB in total
);

SmsPlanet::cancel($result->messageId);

Account and data

SmsPlanet::messageInfo($messageId);      // MessageInfo with per-recipient delivery (max once per 3 minutes)
SmsPlanet::balance();                    // PrePaid points
SmsPlanet::partsCount($text);            // parts as calculated by SMSPlanet
SmsPlanet::senderFields('SMS');          // ['TEST', 'Shop', ...]
SmsPlanet::addSenderField('MY BRAND');
SmsPlanet::addToBlacklist('600111222', now()->addYear()); SmsPlanet::removeFromBlacklist('600111222');
SmsPlanet::findContacts(msisdn: '600111222'); SmsPlanet::saveContact(['msisdn' => '600111222', 'name' => 'Jan'], ['Clients']);
SmsPlanet::groups(); SmsPlanet::saveGroup('VIP'); SmsPlanet::addContactToGroup($contactId, $groupId);
SmsPlanet::shortenUrl('https://example.com/offer');
SmsPlanet::post('/generateReport', ['from' => '01-05-2026 00:00:00', 'to' => '31-05-2026 23:59:59']); // any other endpoint

Notification channel

use Beziworld\SmsPlanet\Messages\SmsMessage;
use Illuminate\Notifications\Notification;

class AppointmentReminder extends Notification
{
    public function via(object $notifiable): array
    {
        return ['smsplanet'];
    }

    public function toSmsPlanet(object $notifiable): SmsMessage|string
    {
        return SmsMessage::create("Reminder: your visit on {$this->visit->starts_at->format('d.m H:i')}.");
    }
}

The recipient is taken from the notifiable:

public function routeNotificationForSmsplanet(): ?string
{
    return $this->phone;
}

On-demand: Notification::route('smsplanet', '600111222')->notify(new AppointmentReminder($visit));

A message may define its own recipients with ->to() and its account with ->connection('clinic'). A notifiable without a phone number is skipped silently.

Delivery reports (webhook)

  1. Enable the endpoint and set the Signature Key from the panel (API > Webhooki):

    SMSPLANET_WEBHOOK_ENABLED=true
    SMSPLANET_SIGNATURE_KEY=key-from-the-panel
  2. Register the webhook once (for example in a deploy script or Tinker):

    SmsPlanet::createWebhook(route('smsplanet.webhook'));
  3. Listen to the event:

    use Beziworld\SmsPlanet\Events\DeliveryReportReceived;
    
    Event::listen(function (DeliveryReportReceived $event) {
        $report = $event->report;
    
        $report->messageId;       // id of the dispatch
        $report->to;              // recipient
        $report->isDelivered();
        $report->deliveredAt;
        $report->error();         // DeliveryError enum when the error is a documented status code
    });

Every request must carry a valid Signature header (Base64 HMAC-SHA256 of the raw body); requests without a valid signature, or any request when no key is configured, are rejected with 403. With several accounts each connection may define its own signature_key; the matching connection is exposed as $event->report->connection. SMSPlanet retries a notification for about 16 hours until it receives HTTP 200, so keep listeners idempotent.

Incoming messages (two-way numbers)

SMSPLANET_INCOMING_ENABLED=true
SMSPLANET_CALLBACK_TOKEN=long-random-string
SMSPLANET_CALLBACK_IPS=        # optional allow-list

Set the forwarding URL in the panel (Odbieranie wiadomości > Ustawienia) to https://your-app.com/smsplanet/incoming?token=long-random-string and listen to IncomingMessageReceived. SMSPlanet does not document the parameters of the forwarding, so the endpoint reads the common names (from, msisdn, phone for the sender, msg, message, text for the content, to, date) and keeps every parameter in $event->message->parameters.

Multiple accounts

// config/smsplanet.php
'default' => env('SMSPLANET_CONNECTION', 'default'),
'connections' => [
    'default' => ['token' => env('SMSPLANET_TOKEN'), 'from' => env('SMSPLANET_FROM'), 'signature_key' => env('SMSPLANET_SIGNATURE_KEY')],
    'clinic' => ['token' => env('SMSPLANET_CLINIC_TOKEN'), 'from' => 'Clinic', 'signature_key' => env('SMSPLANET_CLINIC_SIGNATURE_KEY')],
],
SmsPlanet::connection('clinic')->sendText($phone, 'Hi');

// Credentials stored per tenant
SmsPlanet::using(['token' => $tenant->smsplanet_token, 'from' => $tenant->sms_sender])->send($message);

Error handling

All exceptions extend Beziworld\SmsPlanet\Exceptions\SmsPlanetException:

Exception When
ApiException SMSPlanet returned an error (errorCode(), httpStatus(), payload(), isAuthenticationError()) or could not be reached
InvalidMessage Message without recipients, content, sender or attachment, inconsistent personalisation
InvalidConfiguration Missing credentials or unknown connection
CouldNotSendNotification Notification without a valid toSmsPlanet()

SMSPlanet answers errors with HTTP 200 and {"errorMsg", "errorCode"}, for example 101 invalid key, 103 invalid sender, 109 insufficient funds, 201 invalid token. Some endpoints return only errorMsg (then errorCode() is null).

try {
    SmsPlanet::sendText($phone, $text);
} catch (ApiException $e) {
    if ($e->errorCode() === 109) {
        // top up the account
    }
}

Limits and retries

SMSPlanet accepts 1000 send requests and 300 other requests per minute and blocks the API for 60 seconds when the limit is exceeded; send many recipients in one request instead of one request per number. Retries are disabled by default (SMSPLANET_RETRIES=0) because a timed-out request may still have been accepted.

Testing your application

Http::fake(['api2.smsplanet.pl/*' => Http::response(['messageId' => '191919'])]);

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need a custom integration, an SMS gateway setup or a Laravel project? Get in touch through beziworld.eu.

License

MIT. See LICENSE.md. This package is not affiliated with SMSPlanet.