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

Smspoh Laravel Package

laravel-notification-channels/smspoh

Laravel notification channel for SMSPoh that lets you send SMS messages through the SMSPoh gateway using Laravel's native notification system. Configure your API credentials and sender, then deliver SMS notifications via a dedicated channel.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the upgrade path before you install

v2.0.0 is a major release that moves the package onto SMSPoh API v3. If you already ship v1, read UPGRADE.md first — the config location, the credential shape, and the sender method all changed. If you're starting fresh, the steps below are the current (v2) shape.

  1. Get SMSPoh credentials — v3 uses a key + secret pair, not the single API token v1 used. Grab both from the dashboard.
  2. Register/whitelist your sender name as before. In Myanmar, sender IDs are approved per account; an unapproved from is still the most common cause of a "successful" install that never delivers.
  3. Check the Composer constraint. The release notes call out Laravel 13.x compatibility — confirm the package's illuminate/support range still covers the Laravel version you're pinned to before bumping the major.

Install and configure

composer require laravel-notification-channels/smspoh

The provider is auto-discovered. In v2 the config block lives in config/services.php, not a published config/smspoh.php:

// config/services.php
'smspoh' => [
    'key'    => env('SMSPOH_KEY'),
    'secret' => env('SMSPOH_SECRET'),
    'from'   => env('SMSPOH_FROM'),
],
SMSPOH_KEY=your-api-key
SMSPOH_SECRET=your-api-secret
SMSPOH_FROM=YourApprovedSender

If you publish the package config and it drops files into config/, be careful it doesn't overwrite your services.php — merge by hand instead. And clear cached config after any of this, or you'll authenticate with stale/empty credentials.

Where to look first (the package is small — read it, don't guess)

  • src/SmspohChannel.php — now hitting the v3 endpoint; check what status codes and body-level errors throw, and how the new payload fields (schedule, encryption, unicode, webhook, reference) are serialized.
  • src/SmspohMessage.php — the fluent builder. v2 adds scheduledAt(), encrypt(), encryptKey(), unicode(), deliveryReceiptUrl(), clientReference(), and renames sender() → from().
  • src/SmspohServiceProvider.php — confirms exactly which config key it reads (services.smspoh) and under what names.
  • UPGRADE.md — short, but the only authoritative list of what breaks.

First use case: an OTP notification

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use NotificationChannels\Smspoh\SmspohChannel;
use NotificationChannels\Smspoh\SmspohMessage;

class SendOtp extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public string $code) {}

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

    public function toSmspoh($notifiable): SmspohMessage
    {
        return SmspohMessage::create()
            ->to($notifiable->phone)
            ->content("Your verification code is {$this->code}. It expires in 5 minutes.")
            ->unicode(); // required if the copy includes Burmese script
    }
}

Dispatch with $user->notify(new SendOtp('482913'));.

Implementation Patterns

Route the phone number on the model, not in every notification

// app/Models/User.php
public function routeNotificationForSmspoh($notification)
{
    return $this->phone; // E.164: +959xxxxxxxxx
}

Then toSmspoh() can omit ->to() entirely, and swapping numbers or adding a second contact is a model change, not a notification change. Use explicit ->to() only for one-off messages.

On-demand notifications for non-models (invites, magic links, unverified leads)

Notification::route('smspoh', '+959771234567')
    ->notify(new SendOtp($code));

If that route key doesn't resolve for your version, the deterministic escape hatch is ->to($phone) on the message, which bypasses notifiable routing. Wrap it in a helper so the whole app uses one code path.

Make it queued and rate-limited by default

SMS is a side effect, not part of the request. Always implements ShouldQueue, and throttle per user:

public function middleware($notifiable)
{
    return [(new ThrottleRequests('sms:' . $notifiable->id, 3, 1))];
}

This stops an OTP-resend button, a retry storm, or a webhook replay from draining credits and getting your sender blocked.

Scheduling — new in v2, and the most useful addition

scheduledAt('Y-m-d H:i:s') lets you hand reminders to SMSPoh instead of holding a queue job open for hours or days:

return SmspohMessage::create()
    ->content('Your appointment is tomorrow at 09:00.')
    ->scheduledAt($appointment->starts_at->copy()->subDay()->utc()->format('Y-m-d H:i:s'));

This decouples you from delay() on a queue job — which is exactly what you want for appointment reminders, trial-expiry nudges, and payment due dates, since it survives a queue restart and doesn't consume a delayed-job slot.

Encryption for OTP and sensitive alerts — new in v2

return SmspohMessage::create()
    ->content($text)
    ->encrypt()
    ->encryptKey(config('services.smspoh.encrypt_key'));

Treat the encryption key as a real secret: keep it in .env/a secret manager, never in the message class. Before you ship this for OTP, verify the recipient experience end to end (how the message is decrypted/unlocked on the handset) — an encrypted OTP nobody can read is worse than a plaintext one, and it's the kind of thing you only find out from a real device, not from a passing test.

Delivery receipts via webhook — new in v2

deliveryReceiptUrl($url) lets you set a per-message callback, so you finally get delivery confirmation instead of guessing from a 200:

return SmspohMessage::create()
    ->content($text)
    ->clientReference($notificationId)
    ->deliveryReceiptUrl(route('webhooks.smspoh'));

Route that to a public, verified endpoint. Verify the request (signature/shared secret), keep the handler idempotent — providers retry callbacks — acknowledge fast with a 200, and do the DB write in a queued job. Use clientReference() to correlate the callback back to your notification/order row; it's the cleanest join key you'll get.

Use clientReference() as your idempotency handle

Give every send a stable reference (notification UUID, order id, "{$template}:{$user->id}"). Combined with a short Cache::lock(), it's how you stop a queue retry from producing a second billable SMS, and it gives support a searchable key on the provider side. Same idea as v1, but now first-class.

Localize and template centrally

Burmese copy is long; keep it in lang/my/notifications.php and render in toSmspoh(). This keeps the script handling and segment length in one place — and now makes it trivial to decide per template whether ->unicode() is required.

Multi-tenant / per-purpose senders

Override the sender on the message when different brands share an account — note the method rename:

return SmspohMessage::create()
    ->content($text)
    ->from($tenant->sms_from ?? config('services.smspoh.from'));

->sender() is deprecated in v2. Sweep your codebase for it now rather than waiting for it to disappear in v3.

Testing — two levels

// Feature tests: never hit the network
Notification::fake();
$user->notify(new SendOtp('123456'));
Notification::assertSentTo($user, SendOtp::class, function ($n, $channels) use ($user) {
    return in_array(SmspohChannel::class, $channels)
        && str_contains($n->toSmspoh($user)->content, '123456');
});

// Channel unit tests: Http::fake() the v3 endpoint and assert the payload,
// including the new fields — scheduled_at, encrypted, unicode,
// delivery_receipt_url, client_reference. Those are the ones most likely
// to regress silently when you bump the package.

Observability

Listen to NotificationSent / NotificationFailed, filter channel === 'smspoh', and log the notification class plus notifiable id. Store the provider message id and your clientReference on your own table — with v2's delivery receipts you can now actually close the loop from queued → sent → delivered.

Isolate the dependency

Wrap SmspohChannel::class behind App\Notifications\Channels\SmsChannel or a base notification class. A package that shipped a breaking major with a config-path move and a method rename is exactly the case for this — a future v3 becomes one file's work.

Fallback channel

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

Gotchas and Tips

  • The upgrade is not a token rename. Three things break at once: config moved to config/services.php, token split into key + secret, and sender became from. Miss any one and you get a 401 or a rejected sender that looks like a network problem. Diff your services.php against the release notes, not against a blog post.
  • Stale cached config is the classic post-upgrade failure. config:cache freezes old keys. After upgrading, run php artisan config:clear and then config:cache again, and confirm config('services.smspoh.key') is populated — never read env('SMSPOH_KEY') from application code, it returns null in production.
  • env() names changed too. If .env still has SMSPOH_TOKEN, the new keys resolve empty and you'll chase a phantom auth error. Rename the env vars in the same commit as the Composer bump.
  • Unicode is now explicit. v2 exposes ->unicode(). If your templates contain Burmese script and you don't set it, expect garbled text or a provider-side rejection depending on how the gateway sniffs encoding. Set it deliberately per template, and keep in mind UCS-2 segments are ~70 characters versus 160 for GSM-7 — a "short" Burmese notification can bill as two or three segments. Never truncate mid-codepoint.
  • scheduledAt() takes 'Y-m-d H:i:s', which carries no timezone. It's interpreted in whatever zone the provider assumes. Convert explicitly from your stored UTC timestamps (->utc()->format(...)) and test one scheduled send against the real API before trusting reminders at scale — an off-by-hours reminder is a support ticket per user.
  • Encryption key lifecycle. Rotating encryptKey() may make previously queued-but-unsent messages undecryptable. Don't rotate while a backlog of scheduled encrypted messages is in flight, and never log the key or the plaintext alongside the request.
  • Delivery-receipt endpoints are public and retried. Verify authenticity, make the handler idempotent (upsert on the provider message id / your clientReference), and return 200 fast. Also confirm the URL is reachable from the internet — a route() pointing at localhost will silently never fire, and you'll conclude receipts are broken.
  • HTTP 200 still doesn't mean delivered. Gateways return 200 with a body-level failure status. Read SmspohChannel in v2 to see whether it throws on logical errors; if it doesn't, check the response yourself and throw, so the queue retries and NotificationFailed fires. Delivery receipts reduce but don't remove this need.
  • Phone normalization. SMSPoh expects international format. Normalize 09xxxxxxxxx → +959xxxxxxxxx once (model mutator or a libphonenumber cast) and validate before dispatch. A malformed number is a failed send you paid for.
  • Don't log the credentials. With key and secret now in play there are two headers/secrets to redact. Http::fake() tests are safe; dump() in a controller is not.
  • Set a timeout. Add a 5–10s HTTP timeout in config. A hanging gateway call stalls queue workers and cascades into backlog.
  • Guard missing numbers. If routeNotificationForSmspoh() returns null and the message has no ->to(), the channel throws. Filter in via() rather than catching after the fact.
  • Deduplication is still yours. clientReference() gives you a correlation key, not a guarantee. Keep the Cache::lock("sms:{$user->id}:{$template}", 30) guard around OTP and payment alerts.
  • Verify method names against src/ after the bump. Deprecated-but-working methods (sender()) hide the problem until removal. Grep for sender( and SMSPOH_TOKEN and smspoh.php as an upgrade checklist.
  • Pin the version exactly. A single-maintainer package shipping a breaking major with new payload fields means the hardening is on you. Use an exact constraint, read the src/ diff and UPGRADE.md before bumping, and re-run your Http::fake() payload assertions — those will catch a silently changed field name faster than staging will.
  • Extension points. Subclass SmspohMessage to add helpers (e.g. ->template()), and point the endpoint at a sandbox via config. For retries with backoff or provider failover, write a decorator channel — don't patch vendor/.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package