Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Smsapi Laravel Package

laravel-notification-channels/smsapi

A Laravel notification channel for SMSAPI that lets your application send SMS notifications through the SMSAPI gateway using Laravel’s familiar notification API. Simple to install, configure, and integrate into existing apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire the token

composer require laravel-notification-channels/smsapi

The channel authenticates with an SMSAPI OAuth token (generate one in the SMSAPI panel under API Clients / Tokens; the legacy "API password" from the old panel will not work with the current smsapi/php-client). Put it in config and cache it:

// config/services.php  (check whether your installed version publishes config/smsapi.php instead)
'smsapi' => [
    'token' => env('SMSAPI_TOKEN'),
    // 'from' => env('SMSAPI_FROM'), // if the version you install supports a default sender
],
php artisan config:cache

2. Teach your notifiable where the phone number lives

// app/Models/User.php
public function routeNotificationForSmsapi(): ?string
{
    return $this->phone; // store E.164-ish: 48500100200
}

The channel name is derived from SmsapiChannel → smsapi, so the method name is routeNotificationForSmsapi.

3. First use case — a transactional notification

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use NotificationChannels\Smsapi\SmsapiChannel;
use NotificationChannels\Smsapi\SmsapiMessage;

class OrderShipped extends Notification
{
    public function __construct(public int $orderId) {}

    public function via($notifiable): array
    {
        return [SmsapiChannel::class];
    }

    public function toSmsapi($notifiable): SmsapiMessage
    {
        return (new SmsapiMessage())
            ->from('MyShop')          // must be an approved sender name
            ->content("Order #{$this->orderId} shipped. Track: https://ex.co/t/1");
    }
}
$user->notify(new OrderShipped($order->id));

Where to look first, in order: the README example → src/SmsapiChannel.php (see exactly how it builds the send bag and what it does when the route is empty) → src/SmsapiMessage.php (the fluent surface: content, from, to, template, params, flash, fast, single, normalize, test, notifyUrl, idx) → the smsapi/php-client docs for anything the message class doesn't expose. The message class is a thin DTO over the client's SendSmsBag; if a feature isn't there, you'll be reaching for the client directly.


Implementation Patterns

Multi-channel with a fallback

SMS is expensive and lossy; treat it as the escalation channel, not the primary one.

public function via($notifiable): array
{
    return $notifiable->wantsSms()
        ? ['mail', SmsapiChannel::class]
        : ['mail'];
}

Queue it, always

SMS delivery is a network call inside notify(). Without ShouldQueue, a slow SMSAPI response blocks your HTTP request.

class OrderShipped extends Notification implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 60];
    public $deleteWhenMissingModels = true;
}

implements ShouldQueue on the notification covers all channels, which is usually what you want. If you need SMS queued but mail not, implement ShouldQueue on a custom channel wrapper instead.

On-demand and ad-hoc recipients

For "send an SMS to this number, no user record" (invites, contact-form replies):

Notification::route('smsapi', '+48500100200')
    ->notify(new VerificationCode($code));

Or bypass routing entirely with ->to() on the message, which overrides the route.

Templates over string interpolation

If your account has SMSAPI templates configured, use them — the copy is editable in the panel without a deploy, and params keeps the payload structured:

return (new SmsapiMessage())
    ->template('order_shipped')
    ->params(['order' => $this->orderId, 'url' => $this->trackingUrl]);

Templates also sidestep the diacritics/encoding problem below, because SMSAPI stores them already normalized.

OTP pattern

return (new SmsapiMessage())
    ->content("Your code: {$this->code}. Valid 5 min.")
    ->flash()      // delivered straight to the SIM, not stored — good for codes
    ->single();    // fail loudly rather than split into a multipart message

Store the code server-side with a short TTL and rate-limit sends per number, not just per user — otherwise you're paying for SMS bombing.

Delivery reports via idx

$message = (new SmsapiMessage())
    ->content('...')
    ->idx((string) Str::uuid())
    ->notifyUrl(route('webhooks.smsapi'));

Persist idx → your own model before sending, then reconcile in a webhook controller. The package does not ship a webhook endpoint; you own that, including signature/allow-list validation.

Testing

Channel-level assertions (fast, no HTTP):

Notification::fake();

$user->notify(new OrderShipped(42));

Notification::assertSentTo($user, OrderShipped::class, function ($notification, $channels) {
    return in_array(SmsapiChannel::class, $channels, true);
});

Notification::fake() only proves your code dispatched the notification — it never touches SmsapiChannel. To test the channel itself you have two options:

  • Mock the bound client. Bind a mock of whichever class SmsapiServiceProvider::register() binds in the container, and assert the send bag's recipient/content. This is the tightest test but couples you to the client's internals.
  • HTTP fake. Http::fake() the api.smsapi.com host and assert the outgoing form/JSON body. More brittle across client versions, but it exercises the real mapping code.

There is no SmsapiChannel::fake() equivalent (compare with TwilioChannel::fake() in sibling packages) — this is the single biggest testing gap and worth a small internal helper trait.

Rate limiting

SMSAPI enforces per-second limits per account. Put the throttle on the queue job, not the controller:

public function middleware(): array
{
    return [(new RateLimited('smsapi'))->dontRelease()];
}

Define the limiter in a service provider with Limit::perSecond(config('sms.smsapi_rps', 10)).

Multi-tenant / per-account tokens

The client is bound as a singleton with a single token from config. If you send on behalf of sub-accounts, don't fight the container — instantiate the client yourself and call the SMS feature directly, or write a small channel that resolves a per-tenant token before delegating. Keep the package's SmsapiMessage as the DTO so your notification classes don't change.


Gotchas and Tips

A missing route is a silent no-op. SmsapiChannel::send() returns early when routeNotificationForSmsapi() yields nothing. No exception, no log line, no NotificationFailed event. If "the SMS never arrives," check the route method before anything else. Consider adding a Log::warning wrapper or a custom channel subclass that throws in non-production.

Sender names must be registered. from('MyShop') fails (or silently downgrades to a numeric sender) unless MyShop is an approved sender on the account. Register it in the panel before writing the code, and keep it in config rather than hard-coded per notification so it's consistent across messages.

Encoding doubles your cost. Polish/Czech/Hungarian diacritics push the message from GSM-7 (160 chars/segment) to UCS-2 (70 chars/segment). ->normalize() transliterates to GSM-7 — right call for alerts and URLs, wrong call for anything where the diacritics are load-bearing. Test with your real copy, not "Test message".

Long messages silently become multipart. Over 160 GSM-7 chars, SMSAPI concatenates and bills per segment. URLs plus an order number plus a name is often 170. Use ->single() when you'd rather get a hard failure than a surprise invoice, or count characters in a unit test.

flash() bypasses the inbox. Great for OTP; terrible for anything the user might need to re-read. Also unreliable on some carriers/dual-SIM devices — never make a flash SMS the only delivery path for critical information.

test() doesn't mean "staging". It flags the request so SMSAPI validates but doesn't bill/deliver. Useful in CI against a sandbox account; useless for verifying that real delivery works. Keep a manual smoke-test checklist for the real thing.

Errors arrive as CouldNotSendNotification. In a queued job, an uncaught one retries — and SMS retries are money. Decide explicitly: retry on 5xx/timeouts, fail fast on 4xx (invalid number, unregistered sender, insufficient funds). Wrap send() or use failed() on the job to alert rather than silently burn through $tries.

Phone format is on you. The client mostly tolerates +48… and spaces, but provider-side routing is happiest with a bare MSISDN (48500100200). Normalize on write, store as a string (leading + and zeros die as integers), and validate at the model boundary with a library that knows the country code.

Mocking in tests is not plug-and-play. Because the package binds the client to the container, Notification::fake() tests pass even if the token is garbage. Run at least one integration test against the sandbox with a real token in CI secrets, otherwise your entire SMS suite is a tautology.

Version pinning matters here. 24 stars and zero dependents means a small user base and a thin maintenance surface; a breaking change in smsapi/php-client v3 could land in the channel without much community soak time. Pin exact versions in composer.json, read the changelog before bumping, and keep the surface you depend on narrow — content/from/to/flash will survive client churn better than params or notifyUrl.

Config caching traps. If you call env('SMSAPI_TOKEN') outside a config file, it returns null once config:cache runs — a classic "works locally, dead in production" failure. Keep the token behind config(), and add it to your deploy checklist alongside config:cache.

Extend by wrapping, not forking. The class is small enough that a SmsapiChannel subclass overriding send() (to add logging, metrics, tenant tokens, or throw on empty routes) is cheaper than patching the vendor. If the package goes unmaintained, that subclass plus a direct smsapi/php-client dependency is a two-hour migration — keep your notifications depending on SmsapiMessage, not on anything deeper, to preserve that exit ramp.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core