Encoding & pricing
Pages, not messages
Section titled “Pages, not messages”SMS is billed per page (or “segment”), per recipient. How many pages your message costs depends entirely on which characters are in it.
| Encoding | Chars per page | Used when |
|---|---|---|
| GSM-7 | 160 | Every character is in the GSM alphabet |
| UCS-2 | 70 | Any character isn’t |
That second row is where the money goes.
What’s in GSM-7
Section titled “What’s in GSM-7”Safe: A–Z, a–z, 0–9, space, and
@ £ $ ¥ è é ù ì ò Ç Ø ø Å å Δ _ Φ Γ Λ Ω Π Ψ Σ Θ Ξ Æ æ ß É ! " # ¤ % & ' ( ) * + , - . / : ; < = > ? ¡ Ä Ö Ñ Ü § ¿ ä ö ñ ü à
These count as two characters each: ^ { } \ [ ] ~ | €
Everything else — emoji, “smart quotes”, – en-dashes, … ellipses, most
non-Latin scripts — forces UCS-2.
The invisible culprits
Section titled “The invisible culprits”The characters that catch people out aren’t emoji, which are at least visible. They’re the ones a word processor or a designer’s copy-deck silently inserts:
| Looks like | Actually is | Safe? |
|---|---|---|
' | ' curly apostrophe (U+2019) | ❌ UCS-2 |
' | ' straight apostrophe | ✅ GSM-7 |
"…" | "…" curly quotes | ❌ UCS-2 |
- | – en-dash (U+2013) | ❌ UCS-2 |
... | … ellipsis (U+2026) | ❌ UCS-2 |
Copy a message out of Word, Google Docs, or Notion and it will almost certainly contain curly quotes. Your 158-character message just became three pages, and nothing about it looks different.
Normalise before you send:
const toGSM = (s) => s.replace(/[‘’]/g, "'") // curly single → straight .replace(/[“”]/g, '"') // curly double → straight .replace(/[–—]/g, '-') // en/em dash → hyphen .replace(/…/g, '...'); // ellipsis → three dots
toGSM('We'll see you at 5–6pm…'); // → "We'll see you at 5-6pm..."That one function can cut a campaign’s cost by two thirds.
Multi-page messages
Section titled “Multi-page messages”Longer messages are split and reassembled on the handset — the recipient sees one message. But each page carries a header, so the usable space per page shrinks:
| Encoding | Single page | Per page when concatenated |
|---|---|---|
| GSM-7 | 160 | 153 |
| UCS-2 | 70 | 67 |
So a 320-character GSM-7 message is three pages (153 + 153 + 14), not two.
| Message | Encoding | Chars | Pages |
|---|---|---|---|
Your code is 481920. | GSM-7 | 20 | 1 |
| 160 chars, plain ASCII | GSM-7 | 160 | 1 |
| 161 chars, plain ASCII | GSM-7 | 161 | 2 |
Your code is 481920. 👋 | UCS-2 | 22 | 1 |
| 71 chars with one emoji | UCS-2 | 71 | 2 |
| 150 chars with one curly quote | UCS-2 | 150 | 3 |
Check the cost before you commit
Section titled “Check the cost before you commit”Every accepted batch returns a cost_estimate. pages is the page count for
one message — multiply by recipients for the batch:
"cost_estimate": { "total_cost": 19.2, "currency": "NGN", "pages": 3, "accepted_recipients": 2, "sufficient": true}pages: 3 on a message you thought was short is your signal that something
re-encoded it to UCS-2.
const batch = await sendSMS({ to, content });
if (batch.cost_estimate.pages > expectedPages) { logger.warn( { reference_id: batch.reference_id, pages: batch.cost_estimate.pages }, 'message segmented more than expected — check for non-GSM characters', );}Practical advice
Section titled “Practical advice”- Keep OTPs under 160 GSM-7 characters. They’re your highest-volume, most cost-sensitive traffic. No emoji, ever.
- Run marketing copy through a normaliser. Marketing copy comes from documents; documents contain curly quotes.
- Emoji are a deliberate choice. They lift engagement — sometimes worth 3× the cost. Just make it a decision, not an accident.
- Watch
pagesin your logs. It’s the cheapest early warning you have.