SMS segments and encoding
SMS is the only Orbit channel where a single character can change what a message costs. The billable unit is the segment, and the segment count depends on which of two encodings the body fits into — plus two normalization steps that run before the counting starts. This page explains the decision, the capacity math, why the number you see in the composer is the same number billing uses, and the ceilings and controls that sit around all of it. Read it once before you price a campaign or investigate an unexpected SMS charge; the billing mechanics live in wallets, credits, and charges, and what happens to the segments after you send is in the delivery lifecycle.Two encodings, one decision
Every SMS body leaves Orbit in exactly one of two encodings:- GSM-7 — the 7-bit GSM alphabet (3GPP TS 23.038): ASCII, plus characters like
@,£,€, and a Greek subset. Characters that fit here pack tightly, so each segment holds more. - UCS-2 — 16-bit Unicode. Any character outside the GSM-7 alphabet — an emoji, CJK or Cyrillic, most non-Latin scripts — forces the entire message into UCS-2.
- In GSM-7, ten characters live on an escape sequence (
^,{,},\,[,],~,|,€, form feed) and each consumes 2 septets — the escape byte plus the character. - In UCS-2, count UTF-16 code units, not rendered glyphs: characters outside the basic multilingual plane (emoji, some historic scripts) are surrogate pairs and consume 2 units each.
Capacity: what a segment holds
Two consequences worth internalizing:
- A 1,600-character GSM-7 message is not billed at 160-per-segment; it is billed as
ceil(1600 / 153)= 11 segments at the multi-part rate — which trips the hard cap below. - A message just under the single-part limit that grows past it silently drops to the multi-part limit and picks the segment up from 1 to 2. The 160 → 161 boundary is expensive.
The 10-segment ceiling
Orbit refuses to bill beyond 10 segments — 1,530 GSM-7 septets or 670 UCS-2 code units. A body that exceeds the cap is trimmed to it and the counter reports a truncated flag together with the trimmed body, so the send path can decide to send the shortened form or reject the message; nothing long disappears silently. Extended characters are charged up-front during the trim, so a GSM-7 escape sequence is never split from its character at the boundary.One counter, everywhere: what is billed is what is sent
Segment math lives in exactly one algorithm, shared by every surface that touches it:- The billing path counts segments at send time with that counter. SMS and MMS price per segment — the send-time price gate multiplies your per-segment rate by the same
segmentsvalue you read back on the response. - The compose counter in the dashboard and the cost-preview API run the same counting algorithm, so the number shown while you type is produced by the same math that billing applies at dispatch.
- The encode boundary at the SMPP gateway classifies the body with the same GSM-7 alphabet before submission, so a message billed as GSM-7 is also encoded as GSM-7 on the wire — never the reverse drift where you pay for UCS-2 and send GSM-7, or worse.
The normalization pipeline, before anything counts
Raw input is normalized before encoding detection, in this order:- NFC Unicode normalization. A decomposed
étyped ase+ combining accent (U+0065 U+0301) collapses to the precomposedé(U+00E9). Both forms then count identically — the precomposed codepoint sits in the GSM-7 alphabet, so copy-paste from a source that emits decomposed forms does not blow your message into UCS-2. Without this step, the combining accent (U+0301) is non-GSM and the whole message would flip encoding. - Zero-width character strip. Zero-width spaces, zero-width joiners, and BOM marks (U+200B–U+200D, U+FEFF) are invisible, inflate counts, and are not in the GSM-7 alphabet — they’d otherwise flip encoding for no visual reason. They are removed.
- Smart-punctuation sanitization. Pasted-from-Word punctuation has GSM-7-safe equivalents: curly quotes → straight quotes, en/em dashes → hyphens, ellipsis → three dots, non-breaking space → space, bullets → asterisks. Applying these replacements before counting is what keeps the billed body identical to the on-wire body — the message is counted in the form it will actually travel in, so there is no “billed as X, sent as Y” gap.
é and a precomposed é count identically, and a smart quote you paste in never silently costs you a UCS-2 bill — it is rewritten to an apostrophe, then counted.
Worked examples
Run these against the compose counter to confirm the math before a campaign sends: 1. 300 GSM-7 chars with one extended char. Body: 299 basic characters + one^ (extended = 2 septets) → effective count 301 septets. Encoding: GSM-7. That’s over the 160 single-part limit → multi-part at 153/segment → ceil(301/153) = 2 segments. The ^ quietly consumed 2 of your 153; shuffle paste away from extension-table characters in long copy.
2. A single emoji. Meeting moved to 3pm 👍 — one emoji flips the body to UCS-2. Effective: 22 visible characters, 23 code units (emoji = a surrogate pair, 2 units) → fits one UCS-2 segment → 1 segment, UCS-2. Still cheap. The danger is the next example.
3. Long concatenated marketing copy. A 150-character ASCII blast → 1 GSM-7 segment. Add one emoji → the whole body goes UCS-2; the emoji is a surrogate pair, so 152 UTF-16 units is over the 70 single-part limit → multi-part at 67 → ceil(152/67) = 3 segments. One glyph tripled the segment count — which is exactly the budget line that drives campaign cost at scale.
4. Smart quotes saved. “Limited offer” — today only pasted from a doc: un-normalized this flips to UCS-2 (curly quotes, em dash). After sanitization (", -) it is pure GSM-7 → 1 segment instead of a UCS-2 single segment on the same visible text — and on a 160-char body, the difference is 1 segment vs. 3.
The compose dialog shows the same segments + encoding pair the send response returns — the preview is the counter, not an approximation of it.
Operator controls
Three tenant-side levers wrap the math; none of them changes the encoding decision itself:- The
max_pricesend gate. OnPOST /messages/smsand/messages/mms,max_pricecaps the projected spend: the gate multiplies your per-segment rate by the counted segments and rejects with422 MAX_PRICE_EXCEEDEDwhen the projection exceeds your cap — so a body that just got emoji-flipped cannot burn your wallet on a 3-segment send you never approved. Increasemax_price, shorten the body, or send GSM-7-clean copy. - The compose-time counter. The dashboard composer surfaces
segmentsandencodingas you type, from the same counter billing uses at dispatch — the right place to catch a paste-from-docs mistake before it is a charge. - The 10-segment ceiling. Nothing bills beyond 10 segments; over-cap bodies arrive trimmed with a truncation flag so you see it rather than wonder where the tail went. Split long copy into two sends at a natural break if you need the full text through.
Where next
- Voice & messaging pricing and throughput — the per-segment price mechanics and the SMS segments section this page expands.
- Wallets, credits, and charges — where segment charges debit your balance and how the wallet ledger records them.
- Delivery lifecycle — what happens to your 1–10 segments after the send is accepted, and the statuses each passes through.
- SMS channel guide — the send flow, sender registration gates, and the
segments/encodingresponse fields end to end.