SMS billing
Every message sent through this API is priced from your rate card and lands on your invoice. API traffic never consumes the portal's monthly free allowance — it is always priced.
The charge is in the response, so you never have to wait for an invoice:
"billing": {
"billed": true, "currency": "USD", "type": "sms",
"units": 1, "totalPrice": 0.0025, "recipientCount": 1, "skippedCount": 0
}
price = unitPrice × units, always.
The two models
sms | mms | |
|---|---|---|
| When | one recipient, no attachments | two or more recipients, or any attachment |
| Rate | your SMS rate | your MMS rate |
| Charged | per segment | once |
| Body length affects price | yes | no |
| Recipient count affects price | n/a | no |
| Attachment count affects price | n/a | no |
A 6-segment text to one person costs 6 × your SMS rate. The same body to eight people in a group thread costs one MMS rate.
:::caution Attachments alone make it an MMS
A one-line text to a single person is sms. Attach a picture to that same
message and it becomes mms — charged once at your MMS rate instead of per
segment. Nothing about the recipient list changed; the file did it.
This cuts both ways. A long body that would cost 6 SMS segments costs one MMS
rate once you attach something to it, which is sometimes cheaper. Read
billing.type in the response rather than predicting it from to.
:::
:::caution A group thread is a different product, not a bulk send
Two or more numbers in to puts those people in a room together — one
conversation they can all see and reply into. It does not send separate texts.
For N independent one-to-one messages, make N requests. That is also usually cheaper for short bodies. :::
If participants sit on different rates (one US, one UK), the highest applicable MMS rate is charged for the thread.
Segments (sms only)
SMS is billed in segments — the unit the carrier actually charges for. How many you use depends on both length and which characters you use:
| Content | Single segment | Each segment once it splits |
|---|---|---|
| Plain GSM-7 text (Latin letters, digits, common punctuation) | up to 160 chars | 153 chars |
| Anything outside GSM-7 | up to 70 chars | 67 chars |
:::danger The encoding is chosen per message, not per character One character outside GSM-7 anywhere in the body drops the whole message to the 70-character budget. This is the most common surprise on a bill. :::
Always takes the 70/67 path:
- Hebrew, Arabic, Cyrillic, Greek, Chinese, Japanese, Korean — any non-Latin script
- Emoji
- Curly quotes (
’“), en/em dashes (–—),•,…— these arrive invisibly from word processors and CMS fields
Character cost within a non-GSM-7 message differs too:
- A Hebrew or Arabic letter costs 1 unit — 70 Hebrew characters is one segment, 71 is two.
- Most emoji cost 2 units (surrogate pairs) — 69 Hebrew letters plus one emoji is already two segments.
Some accented Latin vowels (é è à ì ò ù) and some Greek capitals are in GSM-7
and do not force the switch. €, [, ], {, }, \, ^, ~, | are
in the GSM-7 extension table and cost 2 characters each while staying on the
160/153 budget.
Hebrew example: a 100-character Hebrew message is 2 segments (100 > 70, then 100 ÷ 67 rounds up to 2). The same 100 characters in English would be 1. Budget Hebrew and Arabic content at roughly 70 characters per segment.
There is no segment cap; a long body just costs more. The hard bound is 2000 bytes on the body.
Skipped recipients
Never sent to, never charged for. reason is a stable token you can branch on:
reason | Meaning |
|---|---|
opted_out | The recipient replied STOP to this number. Carrier-mandated, not overridable. |
duplicate | The number appeared more than once in to, after normalization. |
invalid | Not a dialable number. |
If every recipient is skipped you get 200 — not 201 — with an empty
messages array and totalPrice: 0.
skipped[].to echoes what you sent, so you can match it to your own records.
messages[].to holds the normalized forms that were actually dialled.
Number normalization
Recipients are normalized before anything else happens:
- Formatting stripped:
(347) 555-0123→3475550123 - A 10-digit number gets a US/Canada
1:3475550123→13475550123 - International numbers used as given:
442071234567
Deduplication runs after normalization, so 3475550123 and 13475550123 in
the same request are one recipient — and the second is reported as
duplicate.
Limits
- 20 recipients per request (participants in one group thread).
- 2000 bytes of body. Note bytes: Hebrew, Arabic and other non-Latin scripts take 2–3 bytes per character in UTF-8, so the effective limit is about 1000 characters for those.
- The sender must be a number on your account with SMS enabled — same
requirement as sending from the portal's message center. Anything else is a
403. (Bulk campaigns additionally need 10DLC registration; this endpoint does not.) - 10 attachments, 5 MB each decoded,
image/jpeg|png|bmp|gif. Attachments make the message anmms— see the table above.
Retries and double-billing
There is no idempotency key. If a request times out, the message may or may not have been sent.
| What you got | Safe to retry | Why |
|---|---|---|
4xx | Yes | Rejected before sending. Nothing charged. |
5xx | Yes | Nothing sent. Nothing charged, guaranteed. |
| No response at all | No | Check your SMS history first. |
Errors specific to pricing
| Status | type | Cause |
|---|---|---|
422 | invalid_request | No active rate applies to a destination. The API will not send a message it cannot price — contact support to add it to your rate card. |
5xx | internal_error | The carrier gateway rejected the message or did not answer. Nothing sent, nothing charged. |
Delivery is not confirmation
status: "sent" means the carrier gateway accepted the message. It does not
mean a handset received it. Subscribe to the
AccountSMS webhook for delivery events.