Laravel SMS
SMS channels for Unifonic, Twilio, Jazz, Zong and Etisalat.
composer require devix-labs/laravel-sms
Live demo coming soon
An SMS holds 160 characters only if every one of them is in the GSM 03.38 alphabet. One that is not — a curly apostrophe out of a word processor, an emoji, a single Arabic letter — and the whole message goes as UCS-2 and the limit drops to 70, so a one-part message becomes three and you are billed for three on every send until somebody notices. The de facto counter, instasent/sms-counter-php, has 1.5 million downloads and was last released in June 2019 at version 0.5.2; we installed it and checked ours against it on ten cases including every boundary and they agree exactly, so the claim here is not that it counts wrongly but that counting is all it does. This says which character forced Unicode, what it would be replaced with and what that would save; normalises numbers to E.164 against the country you send from; checks sender IDs against the eleven-character limit and the countries where registration is required before anything is delivered; and sends through a gateway you describe in five lines of config rather than one somebody had to write a package for — there is effectively nothing on Packagist for Pakistani operators.
What you get
One character can triple the bill
Outside the GSM 03.38 alphabet the limit falls from 160 characters to 70. A curly quote a CMS inserted turns a one-part message into three, on every send, forever. This counts it correctly before anything is sent.
It says which character did it
Counting is the easy half. It names the offending characters, offers the GSM-7 equivalents, and tells you what the message would then cost — so somebody can fix their template rather than being told a number they cannot act on.
Checked against the counter everybody uses
instasent/sms-counter-php has 1.5 million downloads. We installed it and ran both over the 160 and 161 boundaries, the extension characters, Arabic at 70 and 71, emoji at 35 and 36, the euro sign and a non-breaking space. They agree every time, and those vectors are a test here.
153, not 160
A multipart message spends seven bytes of every part on the header that joins them, so a long GSM-7 message is billed at 153 characters per part and a Unicode one at 67. Counting long messages at 160 is the commonest way to under-estimate a campaign.
Numbers the gateway will accept
0300-1234567 reaches the gateway as +923001234567, and 00971, +971 and ٠٥٠ all arrive at the same place. A malformed number costs exactly as much as a good one and delivers nothing.
Sender IDs, before the delivery report
Eleven characters, and in the UAE, Saudi Arabia, Pakistan, India and Turkey an alphanumeric sender must be registered with the operator or nothing from it is delivered at all. Reason codes, checked before the send rather than found out afterwards.
A gateway you configure, not one you install
Packagist has effectively nothing for Pakistani operators — developifynet/telenor-php has 224 downloads in its lifetime. The http driver is declarative: a URL, the parameter names your operator happens to use, and how it signals success. Five lines you can check against their documentation without reading any PHP.
Assert what you would have sent
The array driver remembers every message, including what it would have been billed as — so a template that grows by a sentence and quietly becomes two parts fails a test instead of doubling a bill.
Laravel SMS — overview
The expensive fact
An SMS holds 160 characters only if every character is in the GSM 03.38 alphabet. One that is not — a curly apostrophe a CMS inserted, an emoji, a single Arabic or Urdu letter — and the whole message goes as UCS-2 and the limit drops to 70.
A 160-character message that was one part becomes three. You are billed for three. On every send, forever, until somebody notices.
Nine characters are in the alphabet and still cost two septets, because they
are reached through an escape: ^ { } \ [ ~ ] | and €. A message of 159
characters with one { in it is two parts.
And a long message is billed at 153 characters per part, not 160, because seven bytes of each part are the header that joins them. Counting at 160 is the commonest way to under-estimate a bill.
What we say about the incumbent, honestly
instasent/sms-counter-php has 1,526,849 downloads and was last released in
June 2019, at version 0.5.2.
We installed it and ran both over ten cases — the 160 and 161 boundaries, the extension characters, a curly quote, Arabic at 70 and 71, emoji at 35 and 36, the euro sign, a non-breaking space. The encoding, the length and the part count matched every time.
So the claim here is not that it counts wrongly. It does not, and those vectors are now a test in this package so ours cannot drift from them.
The claim is that counting is all it does. It will tell you the message is three parts. It will not tell you which character did it, what to replace it with, what that would save, whether the number is in a shape the gateway will accept, whether the sender ID will be silently dropped, or send anything.
The part nobody has written
For the region this catalogue serves, Packagist has:
| Downloads, lifetime | |
|---|---|
| multicaret/laravel-unifonic | 197,124 |
| developifynet/telenor-php | 224 |
| itechpk/brandedsms | 16 |
| mudassar1/telenor-sms-php | 1 |
Writing a class per operator means a package has to exist for every operator, and for Pakistan effectively none do. So the http driver is declarative: a URL, the parameter names the gateway happens to use, and how it signals success. Five lines you can check against the operator's own documentation without reading any PHP.
:encoding matters more than it looks — most gateways must be told a message
is Unicode. Send Arabic without the flag and the recipient gets question marks,
and you are still billed.
Sender IDs
Eleven characters, and in the UAE, Saudi Arabia, Pakistan, India, Turkey and elsewhere an alphanumeric sender must be registered with the operator before anything from it is delivered at all.
Both are normally found out from a delivery report, after a campaign. Here
anything that would be rejected outright stops the send, and
needs-registration is returned as advice — because this package cannot know
what you have registered and does not pretend to.
What we do not claim
That we have run this against live accounts. We have not — not Jazz, not Zong, not Unifonic, not Etisalat. Those credentials belong to a customer, and a package claiming tested support for a gateway nobody here has authenticated against would be dishonest.
So it is built the other way round: the http driver shows you exactly what it
will send, array lets you assert it, and log lets you watch it. Everything
before the request is ours and is tested.
That we replace the Twilio channel. If you are on Twilio, use the official one — 303,601 downloads a month, maintained, no reason to change.
How it compares
Questions
Is instasent/sms-counter-php wrong?
No, and we checked rather than assuming. We installed it and ran both counters over ten cases including every boundary — the encoding, the length and the part count matched every time, and those vectors are now a test here so ours cannot drift. What it is: a counter, last released in June 2019 at version 0.5.2. It will tell you a message is three parts. It will not tell you which character did it, what to replace it with, whether the number will be accepted, whether the sender ID will be dropped, or send anything.
Have you tested this against Jazz, Zong, Unifonic or Etisalat?
No, and it would be dishonest to imply otherwise — those credentials belong to a customer and we do not have accounts. That is why the http driver is declarative rather than a class per operator: you can read exactly what it will send and check it against the operator's documentation. Everything before the request — counting, the encoding flag, the E.164 number, the sender ID — is ours and is tested.
Should I use this instead of the Twilio channel?
No. laravel-notification-channels/twilio is maintained and takes 303,601 downloads a month; if you are on Twilio there is no reason to change. Use this for what happens before the send — the counting, the sanitising, the numbers, the sender ID — and for the operators nobody has written a package for.
Why does it rewrite my message?
Only if you leave sanitise on, only for characters with an unambiguous GSM-7 equivalent — curly quotes, dashes, ellipses, non-breaking spaces — and it returns exactly what it changed so you can show somebody. Arabic and emoji are never touched, because there is nothing honest to turn them into. Set 'sanitise' => false and nothing is altered.
Why is an emoji two characters?
Because UCS-2 counts code units, and anything outside the basic multilingual plane — which is most emoji — takes a surrogate pair. Thirty-five emoji is a full message, not half of one. Counting them as one is a way to be surprised by a bill.
More from Devix
All resources →Laravel Phone
Laravel package
Phone validation, casts and a Blade component that agree with Phone Input exactly.
Laravel Validators
Laravel package
Validation rules for regional IDs, tax numbers and IBANs.
Laravel Cookie Consent
Laravel package
Consent banner middleware with a proof-of-consent log.