Skip to content
Devix Open Source

Reference

Counting, numbers and sender IDs

Message

use Devix\Sms\Message;

$cost = Message::of('It’s ready — now');

$cost->encoding;    // 'GSM_7BIT' | 'GSM_7BIT_EX' | 'UTF16'
$cost->length;      // septets for GSM-7, UTF-16 code units for UCS-2
$cost->parts;       // what you are billed for
$cost->remaining;   // how much room is left in this part
$cost->perPart;     // 160, 153, 70 or 67
$cost->offenders;   // ['’' => 1, '—' => 1]
$cost->isUnicode();
$cost->ifSanitised(); // the same message, cleaned up — or null if it would not help

The numbers, and why they are those numbers

Single Concatenated
GSM-7 160 153
UCS-2 70 67

A multipart message spends seven bytes of each part on the header that joins them, which is six septets — so 153, not 160. Counting long messages at 160 is the commonest way to under-estimate a bill.

An emoji counts as two, because UCS-2 counts code units and anything outside the basic multilingual plane takes a surrogate pair. Thirty-five emoji is a full message.

These figures are checked against instasent/sms-counter-php — 1.5 million downloads, and the de facto answer — on ten cases including every boundary. They agree, and the vectors are a test here so ours cannot drift.

Gsm

use Devix\Sms\Gsm;

Gsm::fits('Plain text');       // true
Gsm::fits('رمز');              // false
Gsm::isExtended('{');          // true — two septets

[$clean, $changed] = Gsm::sanitise('It’s “here” — now…');
// "It's \"here\" - now..."
// ['’' => "'", '“' => '"', '”' => '"', '—' => '-', '…' => '...']

sanitise() returns what it changed, so you can show somebody "we replaced your curly quotes" rather than silently altering their message. It only ever touches characters with an unambiguous GSM-7 equivalent; Arabic is left alone, because there is nothing honest to turn it into.

Number

use Devix\Sms\Number;

Number::e164('0300-1234567', 'PK');   // '+923001234567'
Number::e164('050 123 4567', 'AE');   // '+971501234567'
Number::e164('00923001234567');       // '+923001234567'
Number::e164('٠٥٠١٢٣٤٥٦٧', 'AE');     // '+971501234567'

Number::isE164('+923001234567');      // true

It throws rather than guessing when it cannot make sense of something, because a malformed number costs the same as a good one and delivers nothing.

SenderId

use Devix\Sms\SenderId;

SenderId::problems($sender, $country);     // reason codes, or []
SenderId::explain('needs-registration');   // the sentence

empty · too-long · bad-characters · all-digits · needs-registration

A numeric sender is a phone number and none of the alphanumeric rules apply to it.

Sender

$sender->send(string $to, string $text, ?string $from = null, array $options = []);
$sender->cost(string $text): Message;
$sender->driver(): Driver;

Configuration it reads: country, from, sanitise, max_parts, check_registration.

max_parts refuses a message longer than you are willing to pay for, and when the length is due to Unicode it says which characters did it.

Result

$result->sent;    // bool
$result->to;      // the E.164 number it went to
$result->id;      // the gateway's own id, when it gave one
$result->parts;   // what you were billed
$result->error;   // the gateway's own words, not a paraphrase
$result->raw;     // everything it returned

A gateway that is down produces a failed Result, not an exception. A single message failing should not take down a queue worker.

Updated 15 Sep 2026