Most SMS dashboards show one large number, usually "delivered", and it is usually green. That number is the least useful one in your report. It tells you a carrier accepted the message. It doesn't tell you whether a person saw it, clicked it, bought anything, or whether the send cost more than it earned.
This post covers the four layers of SMS reporting worth tracking, in order: what happened to the message at the carrier, what the recipient did, what that action was worth, and what the whole thing cost per segment. It includes the formulas, a worked example with stated assumptions, and the places where the numbers can mislead you.
Layer 1: delivery, and why "delivered" overstates it
Every outbound text ends in one of a few states. The names vary by platform, but the categories stay the same:
- Refused before send. The number opted out, is blocklisted, or is a known landline. On Ready, opt-outs and numbers on your Ban list are refused up front and not charged.
- Held. The text was queued during the recipient's quiet hours. On Ready it sends later and is charged only when it sends.
- Sent / accepted. It left the platform and the carrier took it.
- Delivered. The carrier returned a delivery receipt.
- Failed / undelivered. The carrier rejected it or couldn't reach the handset.
Delivery rate should be delivered ÷ messages that actually left the platform. Don't count refused or held messages in the denominator. Refusals tell you how clean your list is. They say nothing about how carriers treat your traffic, and mixing the two makes both numbers useless.
The bigger catch is that a delivery receipt is not proof of a read, and in some cases not proof the handset got the message at all. Carrier filtering can drop a message after the network accepts it, and that doesn't always come back as a clean failure. We covered that gap in detail in Your 98% 'Delivered' Rate Is Lying to You. The practical rule: treat delivery rate as a floor check, not a success metric. If it drops, something is wrong. If it stays high, that alone doesn't mean anything is right.
Reading carrier error data
Failures carry an error code or reason. You don't need to memorize codes. Group them into buckets, because each bucket has a different fix:
| Failure bucket | What it usually means | What to do |
|---|---|---|
| Invalid / unallocated number | Bad data at capture (typos, fake numbers) | Validate at signup; fix the form |
| Landline or non-texting line | List includes numbers that can't receive SMS | Turn on landline validation or clean the list |
| Unreachable / handset off | Temporary; phone off or out of coverage | Don't resend in a loop; one later retry at most |
| Blocked as spam / content filtered | Carrier flagged the content or the link | Rewrite the content, check the link domain |
| Unregistered or campaign mismatch | Traffic doesn't match the registered 10DLC campaign | Fix registration or send within your registered use case |
The last two buckets are the ones to watch. A spike in content-filter failures right after a new template goes out almost always traces back to that template: a public link shortener, all-caps urgency, or wording that reads like the wrong use case. If your campaign is approved and you're still getting filtered, this breakdown of content-level blocking walks through the usual causes.
Track failures per template and per send, not just account-wide. A 2% failure rate across the account can hide a single template failing at 20%.
Layer 2: engagement (clicks, replies, opt-outs)
Click tracking without getting filtered
SMS has no open tracking, so clicks are your main engagement signal. There are two ways to measure them:
- UTM parameters on your own domain.
yourstore.com/sale?utm_source=sms&utm_campaign=june_restock. Your web analytics counts the sessions. This is free and transparent, and recipients see a domain they recognize. - Per-recipient unique links. Each contact gets a distinct token (
yourstore.com/s/a8k2), so you know exactly who clicked. It takes more setup, and it's the only way to tie a click to a specific contact.
Avoid generic public URL shorteners. Carriers see the same shortener domains in a lot of spam, and links on them are a common reason for content filtering. A branded short domain you control is fine.
Click-through rate = unique clickers ÷ delivered. Use unique clickers, not total clicks, because link previews and people who tap twice inflate the total.
Replies and opt-outs
Reply rate matters most for two-way and conversational sends. Warm, opted-in lists reply most to conversational messages. Promotional sends get far fewer replies, and that's normal.
Opt-out rate = STOP replies ÷ delivered, measured per send. It's the earliest signal that you're sending too often or that a segment of your list didn't really expect your texts. On Ready, a STOP is honored automatically and the opt-out carries across every campaign. The point of tracking it is the trend, not the cleanup. If opt-outs climb two sends in a row, cut frequency before carriers notice the complaints. You can pull opt-outs with GET /v1/opt-outs (see the opt-outs API docs).
Layer 3: conversion attribution
This is where most SMS reporting gets too generous. Three methods, from weakest to strongest:
- Last click. Count orders from sessions that arrived via the SMS link. Easy to set up, and it undercounts: plenty of people read the text, then open the app or type the URL themselves.
- Attribution window. Count any order from a texted contact within X hours (24 to 72 is common). It catches the people who didn't click, and it overcounts, because some of them would have bought anyway.
- Holdout group. Randomly hold back 5 to 10% of the eligible list and don't text them. Compare conversion rates. The difference is the lift the text actually caused. This is the only method that answers "did the text make money?"
If you only do one thing from this post, run a holdout on your next promo. A campaign that shows a big window-attributed number but almost no lift over the holdout is reaching customers who were going to buy anyway. That's still useful to know, and it's a reason to send to them less often. For the full revenue side, see how to calculate SMS marketing ROI.
Layer 4: cost, measured in segments
Carriers bill segments, not messages, so cost reporting has to count segments too. A GSM-7 message fits 160 characters in one segment. Longer messages split into 153-character segments. One emoji switches the whole message to unicode: 70 characters for one segment, 67 per segment once it's multipart.
Worked example (assumptions stated)
This is an illustration, not a customer result. Assume:
- 2,000 opted-in contacts on a restock promo
- A 150-character message
- 90 unique clicks, 18 orders, 60 one-segment replies
- Ready Standard: $0.0075/segment plus a $0.0045 carrier fee per segment, sent or received
Version A, plain text (1 segment):
- Outbound: 2,000 × 1 = 2,000 segments → $15 in rate + 2,000 × $0.0045 in carrier fees
- Replies: 60 segments → $0.45 + 60 × $0.0045
- Total segments: 2,060
Version B, the same 150 characters plus one emoji (unicode, 150 ÷ 67 rounds up to 3 segments):
- Outbound: 2,000 × 3 = 6,000 segments → $45 in rate + 6,000 × $0.0045
- Replies: the same 60 segments
- Total segments: 6,060
One emoji roughly triples the cost of the send. If Version B got the same 18 orders, your cost per order roughly tripled too, and only segment-level reporting shows that. These totals exclude Ready's optional compliance surcharge: scrubbing and landline validation are each $0.003 extra per segment when switched on for an account, and both are off by default.
The metrics that come out of this:
- Cost per delivered message = total send cost ÷ delivered
- Cost per click = total cost ÷ unique clickers
- Cost per incremental order = total cost ÷ (orders in texted group − expected orders from holdout rate)
The last one is the number to compare across channels.
The report worth building
A short reporting table per send:
| Metric | Formula | Watch for |
|---|---|---|
| Refusal rate | refused ÷ attempted | List hygiene; rising = capture problem |
| Delivery rate | delivered ÷ sent | Sudden drops = filtering or registration issue |
| Failure mix | failures by bucket | Content-filter spikes after template changes |
| Segments per message | segments ÷ messages | Anything above 1 on a short promo |
| CTR | unique clickers ÷ delivered | Compare templates, not sends months apart |
| Opt-out rate | STOPs ÷ delivered | Two rising sends in a row = cut frequency |
| Incremental conversion | texted rate − holdout rate | The real answer |
| Cost per incremental order | cost ÷ incremental orders | Compare with email and paid channels |
If you send through an API, you can pull message history with GET /v1/messages (paginated with before_id) and join it to your order data in your own warehouse. Details are in the API docs. If you resell texting to customers and your segment counts don't match the invoice, the segment-count audit covers where the gap usually comes from.
A note on throughput when you read timing data
If clicks arrive slowly, check whether the send itself took a long time before you blame the copy. A fresh standard 10DLC campaign often sends at only about 1 to 4 segments per second. At an assumed 2 segments per second, the 6,000-segment Version B above takes roughly 50 minutes to go out, so the last recipients get it close to an hour after the first. T-Mobile also applies a daily brand cap by trust score, starting around 2,000 messages a day at the lowest tier. Compare click timing against when each message was actually sent, not when you pressed send.
The practical takeaway
Treat delivery rate as a health check, not a result. Group failures by cause and track them per template. Measure clicks on your own domain, run a holdout so you know what the text actually caused, and count cost in segments, because one emoji can triple it.
If you want to see these numbers on your own traffic, the first 20 texts on Ready are free one-to-one sends (a card on file is required). Use them to check your segment counts and delivery before you register, then build your reporting from your real sends. Pricing is on the pricing page, and you can create an account here.