Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

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. nonGsmCharacters lists 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.

Next steps