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.
/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./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
| Name | Type | What it does | Default |
|---|---|---|---|
text required | string | The message, URL-encoded (up to 2000 characters) | — |
accents | boolean | In the plain version, also drop accents GSM-7 can't carry (a with acute to a, c-cedilla to C-cedilla) | false |
recipients | integer | Bulk send: number of recipients (1 to 10,000,000) | — |
price | number | Bulk send: price per part in your currency (0 to 10) | — |
parts | boolean | Include 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 -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'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);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
textis required and URL-encoded (up to 2000 characters;\r\ncounts as one new line). Returnsencoding(GSM-7orUCS-2),units(septets or UTF-16 units),segments,per_textandper_part,leftbefore the next part,next_part_at, averdictand theparts(turn off withparts=false).- GSM-7 extra characters
€ [ ] { } ~ ^ | \are sent as an escape plus the character, so they count two andextended_charssays 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_unicodelists each culprit withcount,code(U+2019), aname(curly apostrophe, en dash, emoji, accented letter...) and its plainfix(or null).plainis the message with those swaps, itssegments, andsaves_parts; addaccents=trueto also drop accents GSM-7 can't carry (é, è, ñ, ü stay because they are in the set). - Add
recipientsandpriceforbulk: 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 emptybad_param(400): a value is the wrong type or out of range (the message says which)too_long(413): text is over 4 KBrate_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
/v1/text/inspectText X-ray: code points, graphemes, UTF-8/16 lengths and invisible characters/v1/unitsConvert length, mass, temperature, data-size or data-rate units/v1/slugTurn a title into a clean URL slug/v1/hashHash a short text (MD5, SHA-1, SHA-2, SHA-3, BLAKE2b)
Prefer a page to an API?
- SMS length counter (parts, culprits, one-click fix) on Tooltern: same job, nothing leaves your browser.
Published · OpenAPI 3.1 spec · Limits and privacy