---
title: "Segments and billing"
description: "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

```ts
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:

```ts
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

- [Validation and encoding](/sending/validation-and-encoding)
- [Encoding and utilities reference](/reference/encoding-and-utilities)
