Segments and billing
How SMS SDK estimates GSM-7 and UCS-2 segments, why one emoji changes the count, and why the estimate is not a price.
Providers bill SMS per segment, not per message. estimateSegments() counts segments locally with the standard encoding rules. The count is an estimate, not a price.
Estimate a body
import { estimateSegments } from "@opencoredev/sms-sdk/encoding";
const short = estimateSegments("Your order has shipped.");
console.log(short.encoding, short.units, short.segments); // "gsm7" 23 1
const emoji = estimateSegments("Your package is ready 📦");
console.log(emoji.encoding, emoji.units, emoji.segments); // "ucs2" 24 1
@opencoredev/sms-sdk exports the same function, and sms.validate() and every SmsSendResult include its result.
The rules
A message uses GSM-7 when every character is in the GSM 03.38 basic alphabet or its extension table. Otherwise the whole message uses UCS-2.
| Encoding | Unit | One segment holds | Each part of a longer message holds |
|---|---|---|---|
| GSM-7 | Septet | 160 | 153 |
| UCS-2 | UTF-16 code unit | 70 | 67 |
In a longer message, each segment loses a few units to the header that joins the parts. So 161 GSM-7 septets cost 2 segments.
SMS SDK also applies these rules:
- Extension characters count 2 septets: form feed,
^,{,},\,[,~,],|, and€. Eighty€signs fill one 160-septet segment. - An extension character’s escape and character never split across two segments.
- An emoji counts 2 code units in UCS-2, and surrogate pairs never split across segments.
- One non-GSM character switches the whole message to UCS-2. That changes the per-segment limit, not the count by itself. “Your package is ready 📦” is 24 code units, so it still fits in one segment.
- SMS SDK never transliterates. It does not replace
“with"or drop an emoji to stay in GSM-7.nonGsmCharacterslists what forced UCS-2, so you can change the text yourself.
Why the estimate is not the bill
The estimate is how a standard handset would split the message. Your bill can differ:
- Some providers apply smart encoding, replacing characters such as curly quotes before sending, which can lower the count.
- Some toll-free and international routes concatenate differently or have their own maximum length.
- The provider and carriers set prices for MMS, scheduled and failed messages, carrier fees, and each country.
- Prices vary by country, sender type, and contract.
Use the count to catch surprises, such as a template that grew past 160 characters or a stray emoji. Use your provider’s pricing pages and invoices for money.
Catch long messages before sending
beforeSend receives the segment estimate, so you can block messages longer than you expect:
import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio";
const sms = createSmsClient({
adapters: [
twilio({
accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
from: "+15550100001",
}),
],
beforeSend: ({ segments, encoding }) =>
segments > 3
? { kind: "reject", reason: `Message is ${segments} ${encoding} segments; the limit is 3.` }
: { kind: "allow" },
});
A rejected message throws PolicyRejectedError before any request, so nothing is billed.