laravel-notification-channels/46elks
Send SMS notifications from your Laravel app with 46elks, the Swedish messaging provider. This channel plugs 46elks into Laravel's notification system, letting you deliver text messages to users through a familiar notification class backed by the 46elks API.
Architecture fit. The package is a native Laravel notification channel, not a standalone SDK. It plugs into the Notification contract via via() and a to46elks() mapper method, so SMS becomes one more channel alongside mail, database, and chat rather than a new messaging subsystem. Internally the footprint is minimal: a service provider, a config block, a channel class that performs a single authenticated HTTP POST to https://api.46elks.com/a1/sms, and a fluent message DTO. That is the correct architecture for the stated use case (one-way transactional SMS) and the wrong one for anything resembling campaigns, batching, or aggregation — none of which it claims to support.
Integration feasibility. Low effort, with one long-lead dependency. Engineering work is roughly a composer require, a credentials block, a routeNotificationFor46elks() method on the notifiable, and a to46elks() method on existing notifications. The long-lead item is not code: 46elks alphanumeric sender IDs require dashboard approval, and the API user is a separate credential from the account login. Start that provisioning before the first sprint, not during it. On-demand notifications (Notification::route('46elks', $phone)->notify(...)) cover the pre-model case — signup OTPs, where no notifiable record exists yet.
Technical risk.
Http::withBasicAuth(...)->asForm()->post(...).notify() does not throw when an SMS fails. The notification is "delivered" as far as Laravel is concerned. Without try/catch, NotificationFailed listeners, or structured logging, credential, balance, and sender-approval failures all look identical to success.id and nothing more. Confirming handset receipt requires a DLR callback registered in the 46elks dashboard plus a receiver route you build, allowlist, and treat as untrusted input. That is separate scope and should be costed as such.Key questions for the team.
$tries = 1 plus ShouldBeUnique; status pings tolerate retries.Stack fit. Laravel 12 is explicitly supported as of v1.10.12; earlier releases track Laravel ≤11. The package assumes a standard notification-driven application — existing mail/database channels, queue workers already running, config/services.php or a published package config. If those hold, integration is additive and touches no existing send path. If the application does not already use queued notifications, add that prerequisite to the plan rather than absorbing it silently into the SMS estimate.
Migration path.
composer require laravel-notification-channels/46elks:^1.10.12 — pinned, not floated.'46elks' block to config/services.php sourced from env (username, password, from).via() returns a channel class the notifier cannot instantiate, and the failure is silent.routeNotificationFor46elks() returning E.164, and to46elks() on the notification classes being extended.PhoneVerification::send($phone)) so the channel name appears in exactly one place and a provider swap is a one-file change.Compatibility notes. Phone numbers must be E.164 (+46701234567) — the channel does not defensively reformat, so garbage in produces a delivery failure with a thin error message. Message bodies are GSM-7; 160 characters per segment, dropping to 70 with any non-GSM character (emoji, curly quotes, some accents) and billing per segment. An alphanumeric sender approved in Sweden may be dropped or substituted in other countries — verify per market before committing SMS to a new region.
Sequencing. Four phases, ordered by blast radius:
tinker before touching a user-facing flow.ShouldBeUnique, a "didn't get the code?" escape hatch, and staging gated so no real SMS leaves non-production.Local and staging safety. Either Notification::fake() in a local service provider or a feature flag that returns ['mail'] instead of the SMS channel. A staging OTP reaching a real handset costs money and confuses whoever owns the number.
Maintenance. Community-maintained with no SLA and a very small contributor base. The mitigation is architectural, not contractual: keep to46elks() a pure mapper so the channel is replaceable in one file, and keep the 20-line direct-HTTP fallback documented and known to more than one engineer. Do not fork the package for metrics or custom retry logic — wrap notify() in an internal SmsNotifier service instead.
Support. Expect no upstream support channel. Defects surface as our own tickets and are triaged by our team. Two recurring categories deserve runbook entries: (a) SMS failures that are actually billing failures — add a low-balance alert on the 46elks account and check balance before debugging queue workers; (b) credential resolution returning null because config:cache ran at image build before env injection — run it at boot and assert non-empty credentials at startup. In-product, provide a self-serve "didn't get the code?" path so credential and balance issues do not all convert into support tickets.
Scaling. Throughput is provider-bound and linear in message count; there is no batching, aggregation, or bulk semantics. Cost scales per segment, so message length is a cost and deliverability lever — keep OTP and status messages under one GSM-7 segment and sanitize smart quotes out of interpolated user strings. Queue workers carry the load; anything slow inside to46elks() (DB queries, HTTP calls, conditionals on app state) becomes queue latency for every other job on that worker, so resolve everything in the constructor or via(). Multi-tenant or multi-brand sender strategies should resolve from a config map keyed by tenant, never hardcoded in notification classes.
Failure modes and detection.
| Failure | Symptom | Detection |
|---|---|---|
| Wrong credentials / cached config | Auth error, no send | Startup assertion on config; error logging |
| Unapproved sender ID | API error, no send | Error logging; sender verified in dashboard pre-launch |
| Non-E.164 number | Silent non-delivery | Normalize at input boundary; test the accessor |
| Account out of credit | Send failures that look like code bugs | Low-balance alert on 46elks account |
| Duplicate send on retry | Second billable, user-visible message | $tries = 1 + ShouldBeUnique on OTP-class notifications |
| Queue exhaustion | Delayed or dropped notifications | NotificationFailed listener; queue depth monitoring |
Instrument at both layers: Notification::fake() for unit assertions on channel selection, and Http::fake(['api.46elks.com/*' => ...]) for integration tests that catch payload-shape regressions when the package version bumps.
Ramp-up. Low. The package is a handful of files; a Laravel engineer can read the source and have the complete public API in fifteen minutes — which is also the most reliable way to learn what the README leaves implicit (config key names, whether from is required, error behavior). Plan for one engineer-day to first production send, plus non-engineering lead time for sender-ID approval and any market-by-market verification. The larger ramp-up cost sits outside the package: deciding retry semantics per notification, establishing a phone-normalization convention, and agreeing who owns the 46elks account operationally.
Bottom line. This is a defensible tactical dependency for one-way transactional SMS in a Laravel app where 46elks is already the committed provider. It is not appropriate where SMS is a revenue-critical path without a written fallback, where delivery confirmation is required at launch, or where the provider decision is still open. The gating item for a go/no-go is provider fit and account provisioning, not the package itself.
How can I help you explore Laravel packages today?