AnvilJSON utilities

Anvil › Docs › /v1/sms-segments

SMS segment calculator API: 160 vs 70 characters, parts, and what forced Unicode

A text message holds 160 plain characters, or 153 per part once it splits. One emoji, curly apostrophe or long dash switches the whole message to Unicode, which holds only 70 (67 per part), so a 100-character message can quietly become two billable texts. This endpoint counts the way the network does, shows each part, names the characters that cost you, and gives a plain version that keeps the meaning.

In one line: GET /v1/sms-segments?text=Don%E2%80%99t%20forget%20%E2%80%93%20the%20meeting%E2%80%99s%20moved%20to%203pm%E2%80%A6&recipients=1000&price=0.04 returns JSON. No key, CORS on, 60 requests a minute.
GET

/v1/sms-segments

SMS segments: how many text parts a message needs (160/153 vs 70/67), what forced Unicode, a plain-character fix and bulk cost. Change the values and press Send request to call the live API.

Parameters

NameTypeWhat it doesDefault
text requiredstringThe message, URL-encoded (up to 2000 characters)—
accentsbooleanIn the plain version, also drop accents GSM-7 can't carry (a with acute to a, c-cedilla to C-cedilla)false
recipientsintegerBulk send: number of recipients (1 to 10,000,000)—
pricenumberBulk send: price per part in your currency (0 to 10)—
partsbooleanInclude the split parts (default true)true

Example

Request:

GET https://api.oddlathe.com/v1/sms-segments?text=Don%E2%80%99t%20forget%20%E2%80%93%20the%20meeting%E2%80%99s%20moved%20to%203pm%E2%80%A6&recipients=1000&price=0.04

Response (generated by the real code when these docs were last built; random or time-based values will differ for you):

{
 "encoding": "UCS-2",
 "characters": 42,
 "units": 42,
 "segments": 1,
 "per_text": 70,
 "per_part": 67,
 "left": 28,
 "next_part_at": 71,
 "extended_chars": 0,
 "forcing_unicode": [
  {
   "char": "’",
   "count": 2,
   "code": "U+2019",
   "name": "curly apostrophe",
   "fix": "'"
  },
  {
   "char": "–",
   "count": 1,
   "code": "U+2013",
   "name": "en dash",
   "fix": "-"
  },
  {
   "char": "…",
   "count": 1,
   "code": "U+2026",
   "name": "ellipsis",
   "fix": "..."
  }
 ],
 "fixable": true,
 "verdict": "Fits in one text, 28 units to spare.",
 "plain": {
 …

Code examples

curl
curl -G 'https://api.oddlathe.com/v1/sms-segments' \
  --data-urlencode 'text=Don’t forget – the meeting’s moved to 3pm…' \
  --data-urlencode 'recipients=1000' \
  --data-urlencode 'price=0.04'
JavaScript (browser or Node 18+)
const params = new URLSearchParams({
  text: "Don’t forget – the meeting’s moved to 3pm…",
  recipients: "1000",
  price: "0.04"
});
const res = await fetch(`https://api.oddlathe.com/v1/sms-segments?${params}`);
if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();
console.log(data);
Python (standard library only)
import json, urllib.parse, urllib.request

params = urllib.parse.urlencode({"text": "Don’t forget – the meeting’s moved to 3pm…", "recipients": "1000", "price": "0.04"})
url = "https://api.oddlathe.com/v1/sms-segments?" + params
# Send a User-Agent: the edge network refuses Python's default one.
req = urllib.request.Request(url, headers={"User-Agent": "my-script/1.0"})
with urllib.request.urlopen(req, timeout=10) as r:
    data = json.load(r)
print(data)

When it's useful

  • Bulk SMS and notification dashboards: warn before a template becomes two parts per recipient
  • Template linting in CI: fail when a message is UCS-2 only because of a pasted smart quote
  • Showing a live parts counter in a form without shipping GSM-7 tables to the browser
  • Estimating a campaign's billable parts and what swapping to plain characters saves

Good to know

  • text is required and URL-encoded (up to 2000 characters; \r\n counts as one new line). Returns encoding (GSM-7 or UCS-2), units (septets or UTF-16 units), segments, per_text and per_part, left before the next part, next_part_at, a verdict and the parts (turn off with parts=false).
  • GSM-7 extra characters € [ ] { } ~ ^ | \ are sent as an escape plus the character, so they count two and extended_chars says how many. Parts never split an escape pair or an emoji's surrogate pair, which is why a part can hold 152 or 66.
  • For UCS-2 messages forcing_unicode lists each culprit with count, code (U+2019), a name (curly apostrophe, en dash, emoji, accented letter...) and its plain fix (or null). plain is the message with those swaps, its segments, and saves_parts; add accents=true to also drop accents GSM-7 can't carry (é, è, ñ, ü stay because they are in the set).
  • Add recipients and price for bulk: billable parts, total, and the plain version's total and saving when it uses fewer parts.
  • Counts follow 3GPP TS 23.038 with the default alphabet and no national language shift tables; a few carriers and handsets (and iMessage or RCS) do their own thing, so treat it as what a standard SMS gateway will bill.
  • Browser UI: Tooltern SMS length counter with the same model, a part-by-part view and a one-click fix (nothing leaves your device). Background: why texts get split and the Wirebook lesson on how a text finds a phone.

How many SMS parts a message is sent as. Plain GSM-7 text fits 160 characters in one text and 153 per part once it splits (the rest carries the hidden part 2 of 3 header); the extra characters € [ ] { } ~ ^ | \ count as two. One character outside that set (an emoji, a curly apostrophe, a long dash, a non-breaking space) switches the whole message to UCS-2: 70 per text, 67 per part, emoji counting as two. Returns the encoding, units, segments, room left, each part, the characters that forced Unicode with their code points and plain swaps, a plain version with its segment count, and with recipients and price the billable parts and what the plain version would save. Same model as the Tooltern SMS length counter. Nothing is stored or logged.

Errors

Errors are JSON like {"error": {"code": "…", "message": "…"}}. The ones this endpoint can return:

  • missing_param (400): a required parameter is missing or empty
  • bad_param (400): a value is the wrong type or out of range (the message says which)
  • too_long (413): text is over 4 KB
  • rate_limited (429): over 60 requests a minute; wait for Retry-After seconds

Questions

Why does one emoji make my text two messages?

Emoji aren't in the GSM-7 alphabet, so the whole message switches to UCS-2: the limit drops from 160 to 70, and the emoji itself counts as two. A 100-character birthday text with one 🎉 becomes 2 parts.

Why 153 and not 160 per part?

Each part of a long message carries a 6-byte header saying which message it belongs to and its number, leaving 153 GSM-7 characters (67 in UCS-2).

Do curly quotes really count?

Yes. ’ and “” from word processors and phone keyboards are outside GSM-7. <code>plain</code> swaps them for ' and " and says how many parts that saves.

Is the message stored?

Never. It is counted in memory from the query string and forgotten; Anvil keeps no request logs.

Related endpoints

Prefer a page to an API?

Published · OpenAPI 3.1 spec · Limits and privacy