beziworld / laravel-smsapi
Laravel integration for SMSAPI: SMS sending, notification channel and delivery reports.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/notifications: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 16:36:45 UTC
README
Laravel integration for SMSAPI (smsapi.pl and smsapi.com): sending SMS, a notification channel, delivery report webhooks and account balance.
Features
- Fluent
SmsMessagebuilder covering scheduling, templates,idxtracking, fast/flash messages and more smsapinotification channel, including on-demand notifications- Delivery report endpoint dispatching a
DeliveryReportReceivedevent per report (batched callbacks are split) SmsSentevent after every accepted message- Typed responses (
SendResult,SentMessage,Profile,DeliveryReport,DeliveryStatusenum) - One exception hierarchy with the SMSAPI error code exposed
- PHP 8.3+, Laravel 12 and 13, PHPStan level 8
Installation
composer require beziworld/laravel-smsapi
php artisan vendor:publish --tag=smsapi-config # optional
SMSAPI_TOKEN=your-oauth-token SMSAPI_FROM=YourBrand # sender name approved in the panel SMSAPI_SERVICE=pl # pl = api.smsapi.pl, com = api.smsapi.com SMSAPI_TEST_MODE=false # true: validate requests without sending or charging
Generate the token in the SMSAPI panel (API Settings > API tokens) and grant only the scopes you need
(typically sms and profile).
Sending messages
use Beziworld\SmsApi\Facades\SmsApi; use Beziworld\SmsApi\Messages\SmsMessage; // Shortcut SmsApi::sendText('500100200', 'Your code is 1234'); // Full builder $result = SmsApi::send( SmsMessage::create('Your order #1001 has shipped') ->to(['48500100200', '600700800']) ->from('Shop') ->idx('order-1001', unique: true) // returned in delivery reports; unique rejects duplicates ->sendAt(now()->addHour()) ->normalize() // replace Polish characters to save parts ); $result->count; // number of accepted messages $result->points(); // total cost $result->ids(); // SMSAPI message ids
Other options: toGroup(), template($name, ...$params), expiresAt(), notifyUrl(), fast(), flash(),
maxParts(), encoding(), test().
Scheduled messages can be cancelled with SmsApi::cancelScheduled($id1, $id2), the balance is available via
SmsApi::balance() and the full account profile via SmsApi::profile().
Notification channel
use Beziworld\SmsApi\Messages\SmsMessage; use Illuminate\Notifications\Notification; class OrderShipped extends Notification { public function via(object $notifiable): array { return ['smsapi']; } public function toSmsApi(object $notifiable): SmsMessage|string { return SmsMessage::create("Order {$this->order->number} has shipped")->idx("order-{$this->order->id}"); } }
The recipient is taken from the notifiable:
public function routeNotificationForSmsapi(): ?string { return $this->phone; }
On-demand: Notification::route('smsapi', '500100200')->notify(new OrderShipped($order));
Messages may also define their own recipients with ->to(); then the route is not consulted. A notifiable without
a phone number is skipped silently.
Delivery reports
-
Enable the endpoint and choose a secret token:
SMSAPI_DELIVERY_REPORTS_ENABLED=true SMSAPI_CALLBACK_TOKEN=long-random-string SMSAPI_CALLBACK_IPS=89.174.81.98,91.185.187.219,213.189.53.211,31.186.83.18,212.91.26.253 # optional
-
Set the callback URL in the SMSAPI panel (or per message with
notifyUrl()):https://your-app.com/smsapi/delivery-report?token=long-random-string -
Listen to the event:
use Beziworld\SmsApi\Events\DeliveryReportReceived; Event::listen(function (DeliveryReportReceived $event) { $report = $event->report; if ($report->isDelivered()) { // $report->messageId, $report->idx, $report->to, $report->doneAt ... } });
SMSAPI does not sign callbacks, hence the token: without a configured token the endpoint rejects every request.
Callbacks may be delivered more than once, so keep listeners idempotent. The token travels in the query string and
therefore appears in web server access logs: use a long random value, restrict the endpoint with
SMSAPI_CALLBACK_IPS and rotate the token if logs are shared. Behind a load balancer configure trusted
proxies so that the IP allow-list sees the real client address.
Error handling
All exceptions extend Beziworld\SmsApi\Exceptions\SmsApiException:
| Exception | When |
|---|---|
ApiException |
SMSAPI rejected the request (errorCode(), httpStatus(), payload()) or could not be reached |
InvalidMessage |
Message without recipients or content |
InvalidConfiguration |
Missing token or unknown service |
CouldNotSendNotification |
Notification without a valid toSmsApi() |
try { SmsApi::sendText($phone, $text); } catch (ApiException $e) { if ($e->errorCode() === 103) { // not enough points } }
Retries
Retries are disabled by default (SMSAPI_RETRIES=0): a request that timed out may still have been accepted, and
repeating it would send the SMS twice. If you enable retries, give every message an idx(..., unique: true) so that
SMSAPI rejects the duplicate (error 53).
Testing your application
Use Laravel's HTTP fake:
Http::fake(['api.smsapi.pl/*' => Http::response(['count' => 1, 'list' => [['id' => '1', 'points' => 0.16, 'number' => '48500100200']]])]);
Usage examples
One-time password (2FA / phone verification)
use Beziworld\SmsApi\Exceptions\ApiException; use Beziworld\SmsApi\Facades\SmsApi; use Beziworld\SmsApi\Messages\SmsMessage; public function sendCode(Request $request): RedirectResponse { $code = (string) random_int(100000, 999999); Cache::put("otp:{$request->user()->id}", Hash::make($code), now()->addMinutes(5)); try { SmsApi::send( SmsMessage::create("Your verification code: {$code}. It expires in 5 minutes.") ->to($request->user()->phone) ->fast() // priority route for time-critical codes ->expiresAt(now()->addMinutes(5)) ); } catch (ApiException $e) { report($e); return back()->withErrors(['phone' => 'The code could not be sent, please try again.']); } return back()->with('status', 'Code sent.'); }
Appointment reminders scheduled in advance
$result = SmsApi::send( SmsMessage::create("Reminder: your visit on {$visit->starts_at->format('d.m H:i')}.") ->to($visit->patient->phone) ->sendAt($visit->starts_at->subDay()) ->idx("visit-{$visit->id}", unique: true) ); $visit->update(['sms_id' => $result->first()?->id]); // Visit cancelled? Cancel the scheduled SMS as well. SmsApi::cancelScheduled($visit->sms_id);
Tracking delivery in the database
use Beziworld\SmsApi\Events\DeliveryReportReceived; class UpdateSmsStatus { public function handle(DeliveryReportReceived $event): void { SmsLog::where('smsapi_id', $event->report->messageId)->update([ 'status' => $event->report->status?->name ?? (string) $event->report->statusCode, 'delivered_at' => $event->report->isDelivered() ? $event->report->doneAt : null, ]); } }
Low balance alert (scheduler)
Schedule::call(function () { if (SmsApi::balance() < 100) { Notification::route('mail', 'admin@example.com')->notify(new SmsBalanceLow()); } })->dailyAt('08:00');
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues and feature requests: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need a custom integration, an SMS gateway setup or a Laravel project? Get in touch through beziworld.eu.
Other packages by the author
- beziworld/laravel-przelewy24: Przelewy24 payments
- beziworld/laravel-tpay: Tpay payments and BLIK
- beziworld/laravel-directadmin: DirectAdmin hosting panel API
License
MIT. See LICENSE.md. This package is not affiliated with SMSAPI / LINK Mobility.