Table of Contents
- The Short Answer
- TL;DR
- The Six Variables That Decide Your Bill
- Variable One: The Credit, Not the Message
- The Encoding Cliff: One Character Can Triple Your Bill
- Counting Segments Correctly in Code
- Pricing a Message Before You Send It
- Variable Two: Route Class
- What the Published Slabs Actually Say
- Variable Three: Volume Slabs and the Commitment Trap
- Variable Four: Where the Deduction Actually Happens
- Variable Five: Retry Amplification
- Variable Six: DLT and the Costs That Are Not Per-SMS
- DND Scrubbing and Who Pays for a Blocked Message
- International SMS: A Different Cost Model Entirely
- Reconciling Your Bill Against the DLR API
- How to Model Your Monthly Bulk SMS Cost
- A Worked Example
- Cost Reduction Checklist
- Channel Cost Decision Matrix
- Ten Costly Mistakes
- Frequently Asked Questions
The Short Answer
Your bulk SMS bill in India is the per-credit rate multiplied by the number of credits consumed, and almost every unpleasant surprise comes from the second term, not the first. A message is billed in credits, a credit covers 160 characters of GSM-7 text or only 70 characters of Unicode, and a single character outside the GSM-7 set flips the whole message to Unicode and can turn a one-credit send into three. On top of that, credits are deducted when a message is successfully submitted to the operator rather than when it is delivered, route class changes the rate by a third or more, DLT registration adds fixed costs outside the per-SMS rate, and a naive retry loop multiplies everything. Comparing providers on the advertised paise figure alone tells you almost nothing about what you will actually spend.

TL;DR
- You are billed per credit, not per message. One credit equals 160 GSM-7 characters or 70 Unicode characters. Concatenated messages drop to 153 and 67 per segment because the User Data Header eats space.
- One non-GSM-7 character re-encodes the entire message. A curly apostrophe, an em dash, an emoji or a rupee sign pushes a 155-character message from one credit to three.
- Route class matters more than negotiation. On the published SMSGatewayCenter slabs, transactional at 50,000 units is INR 0.15 per SMS while promotional at the same volume is INR 0.13, and the routes have different delivery windows, sender ID types and DND behaviour.
- Deduction happens at successful submission to the operator, not at delivery. The published terms state credits are non-refundable once the SMS is successfully submitted. Undelivered does not mean unbilled.
- Retries are the most common source of unexplained overspend. Retrying a permanently failed message three times triples the cost of that message and buys nothing.
- DLT is a fixed overhead layer, not a per-SMS charge, but template rejections and mismatches cause failed sends that still consume operational time and can cause resend cost.
- Price the message before you send it.
POST https://unify.smsgateway.center/SMSApi/info/msgreturns encoding, length, remaining characters and credit count for any message string. - Reconcile against delivery reports. The DLR API returns per-message
costandlengthfields, which is the only way to audit a bill against reality.
The Six Variables That Decide Your Bill
Most bulk SMS pricing articles are a table of provider names and paise figures. That is a comparison of list prices, not a cost model. Here is the actual structure of the bill:
| # | Variable | What it controls | Typical magnitude of effect |
|---|---|---|---|
| 1 | Credits per message | How many billable units one logical message consumes | 1x to 3x or worse, entirely under your control |
| 2 | Route class | The per-credit rate and the delivery rules attached to it | Roughly 15 to 30 percent between promotional and transactional at the same volume |
| 3 | Volume slab | The per-credit rate you qualify for | Roughly 30 percent from the entry slab to the top published slab |
| 4 | Billing trigger point | Whether failures cost you money | Up to your full failure rate, often 2 to 8 percent |
| 5 | Retry policy | How many times you pay for the same logical message | 1x to 4x on the retried subset |
| 6 | Fixed compliance overhead | DLT registration, sender ID and template costs, amortised | Fixed, matters enormously at low volume and rounds to nothing at high volume |
Variables 1, 4 and 5 are engineering decisions. Variables 2 and 3 are commercial decisions. Variable 6 is a regulatory floor. Teams almost always negotiate hard on 2 and 3 and completely ignore 1, 4 and 5, which is backwards, because the engineering variables have a larger multiplier and cost nothing to fix.
Variable One: The Credit, Not the Message
A GSM SMS carries a 140-octet payload. With 7-bit GSM-7 packing that is 160 characters. With 16-bit UCS-2, needed for any character outside the GSM-7 alphabet, it is 70 characters.
When a message exceeds one segment, the sender splits it and prefixes each part with a User Data Header so the handset can reassemble it in order. That header consumes payload. The result is the well-known asymmetry: the first segment of a concatenated message holds 153 GSM-7 characters, not 160, and 67 Unicode characters, not 70.
SMSGatewayCenter publishes this mapping directly on its Text Messages Chart, and the numbers line up exactly with the GSM specification:
| Credits billed | GSM-7 text characters | Unicode characters |
|---|---|---|
| 1 | 160 | 70 |
| 2 | 306 | 134 |
| 3 | 459 | 201 |
| 4 | 612 | 268 |
| 5 | 765 | 335 |
| 6 | 918 | 402 |
| 7 | 1071 | 469 |
| 8 | 1224 | 536 |
| 9 | 1377 | 603 |
| 10 | 1530 | 670 |
Read the second row carefully. Two credits buy 306 characters, not 320. Three buy 459, not 480. Every segment after the first costs you seven characters of GSM-7 capacity or three characters of Unicode capacity to the header.
The practical consequence: a message of exactly 161 GSM-7 characters costs the same as a message of 306 characters. If your template lands at 161 to 170 characters, you are paying for a second segment and using almost none of it. Trimming eight characters halves that message’s cost. Across a million-message campaign that is not a rounding error.
The 160 / 161 boundary in plain terms
| Message length (GSM-7) | Segments | Credits | Wasted capacity |
|---|---|---|---|
| 158 | 1 | 1 | 2 characters |
| 160 | 1 | 1 | 0 |
| 161 | 2 | 2 | 145 characters |
| 306 | 2 | 2 | 0 |
| 307 | 3 | 3 | 152 characters |
The 161-character message is the single most expensive length in bulk SMS. It costs double and delivers one extra character of value.
The Encoding Cliff: One Character Can Triple Your Bill
This is the failure mode that produces the “why did our SMS bill jump 40 percent last month” conversation, and it is almost always caused by a copy-paste from a word processor.
GSM-7 has a fixed alphabet. Anything outside it forces the entire message, not just the offending character, into UCS-2. The capacity per credit drops from 160 to 70, a 56 percent reduction.
Characters that look harmless and are not in GSM-7:
| Character | Name | Where it comes from |
|---|---|---|
' U+2019 | Right single quotation mark | Microsoft Word and Google Docs autocorrect on apostrophes |
" " U+201C/D | Curly double quotes | Same autocorrect |
– U+2013 | En dash | Word autocorrects a hyphen surrounded by spaces |
… U+2026 | Horizontal ellipsis | Word autocorrects three dots |
₹ U+20B9 | Indian rupee sign | Typed directly instead of using “Rs.” or “INR” |
• U+2022 | Bullet | Pasted from a bulleted list |
| Any emoji | Marketing copy | |
| Any Devanagari, Tamil, Telugu, Bengali, Gujarati, Kannada, Malayalam or Odia character | Regional language campaigns, which is intentional and unavoidable |
A 155-character promotional message written in plain ASCII costs one credit. The same message where an autocorrect turned two apostrophes into U+2019 becomes Unicode, needs 155 characters of UCS-2 capacity, and at 67 characters per concatenated segment that is three segments. One credit becomes three. The message is visually identical to the person who wrote it.
Worse, the two characters that caused it will not show up in a visual review. They render as apostrophes.
Characters that are in GSM-7 but cost two units
There is a second, subtler trap. Ten characters live in the GSM-7 extension table and are encoded as an escape sequence followed by the character, consuming two of your 160 units each:
{ } [ ] ~ ^ \ | and the euro sign €
A URL containing square brackets, or a message using a tilde, silently costs more than its character count suggests. A message that a naive strlen() reports as 158 characters can be 164 GSM-7 units and therefore two segments.
This is why counting characters with strlen(), .length or len() and dividing by 160 gives the wrong answer in two separate directions.

Regional language campaigns are not a mistake, they are a budget line
If you are sending in Hindi, Tamil or Marathi, Unicode is correct and unavoidable. The point is not to avoid it, it is to budget for it separately. A Unicode campaign at the same message length costs roughly 2.3 times what the English version costs. Model the two message classes separately in your forecast rather than applying a single blended rate to your whole volume. Transactional and OTP routes on SMSGatewayCenter both list Unicode and regional language support, so the capability is there; the cost model just needs to reflect it.
Counting Segments Correctly in Code
A correct counter must do three things: detect whether every character is in the GSM-7 alphabet, count extension-table characters as two units, and apply the 160/153 and 70/67 rules. It must also iterate by Unicode code point rather than by UTF-16 code unit, or emoji outside the Basic Multilingual Plane will be counted twice.
PHP
<?php
final class SmsSegmentCounter
{
/** Base GSM 03.38 alphabet, one unit each. */
private const GSM_BASIC =
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?"
. "¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
/** GSM 03.38 extension table, two units each (escape + char). */
private const GSM_EXTENDED = "^{}\\[~]|€";
/**
* @return array{encoding:string,units:int,segments:int,perSegment:int}
*/
public static function count(string $message): array
{
$chars = preg_split('//u', $message, -1, PREG_SPLIT_NO_EMPTY);
$units = 0;
$isGsm = true;
foreach ($chars as $char) {
if (mb_strpos(self::GSM_BASIC, $char, 0, 'UTF-8') !== false) {
$units += 1;
} elseif (mb_strpos(self::GSM_EXTENDED, $char, 0, 'UTF-8') !== false) {
$units += 2;
} else {
$isGsm = false;
break;
}
}
if (!$isGsm) {
// UCS-2: count UTF-16 code units, so non-BMP characters count as 2.
$units = mb_strlen(mb_convert_encoding($message, 'UTF-16BE', 'UTF-8'), '8bit') / 2;
$single = 70;
$multi = 67;
$encoding = 'UNICODE';
} else {
$single = 160;
$multi = 153;
$encoding = 'TEXT';
}
$segments = $units <= $single
? 1
: (int) ceil($units / $multi);
return [
'encoding' => $encoding,
'units' => (int) $units,
'segments' => max(1, $segments),
'perSegment' => $units <= $single ? $single : $multi,
];
}
}
// A message that looks like one segment but is not.
$a = "Your order has been shipped and will arrive by Friday. Track it at "
. "https://ex.co/t/9F3K2 or call 1800-000-000 for help. Thank you for shopping with us!";
var_dump(SmsSegmentCounter::count($a));
// encoding TEXT, units 151, segments 1
// The same copy after a word processor touched the apostrophe.
$b = str_replace("'", "\u{2019}", $a . " Don't reply.");
var_dump(SmsSegmentCounter::count($b));
// encoding UNICODE, segments 3
This is the same counter used in the SMS API in PHP integration tutorial, where it sits in the message-building layer rather than being bolted on at send time.
Node.js
const GSM_BASIC = new Set(
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" +
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà"
);
const GSM_EXTENDED = new Set("^{}\\[~]|€");
export function countSegments(message) {
let units = 0;
let isGsm = true;
// Spread iterates by code point, not UTF-16 code unit.
for (const char of message) {
if (GSM_BASIC.has(char)) units += 1;
else if (GSM_EXTENDED.has(char)) units += 2;
else { isGsm = false; break; }
}
if (!isGsm) {
units = message.length; // .length IS UTF-16 code units, which is what UCS-2 bills.
}
const single = isGsm ? 160 : 70;
const multi = isGsm ? 153 : 67;
const segments = units <= single ? 1 : Math.ceil(units / multi);
return {
encoding: isGsm ? "TEXT" : "UNICODE",
units,
segments: Math.max(1, segments),
perSegment: units <= single ? single : multi,
};
}
console.log(countSegments("Hello world"));
// { encoding: 'TEXT', units: 11, segments: 1, perSegment: 160 }
console.log(countSegments("नमस्ते, आपका ऑर्डर भेज दिया गया है।"));
// { encoding: 'UNICODE', ... }
Note the deliberate asymmetry: in the GSM branch we iterate code points, and in the Unicode branch we use .length precisely because UCS-2 billing counts 16-bit units, so an astral-plane emoji correctly counts as two. The full version, with the surrounding client and queue, is in the SMS API in Node.js integration tutorial.
Python
from math import ceil
GSM_BASIC = set(
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?"
"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà"
)
GSM_EXTENDED = set("^{}\\[~]|€")
def count_segments(message: str) -> dict:
units = 0
is_gsm = True
for char in message:
if char in GSM_BASIC:
units += 1
elif char in GSM_EXTENDED:
units += 2
else:
is_gsm = False
break
if not is_gsm:
# UCS-2 bills 16-bit units, so encode and halve the byte count.
units = len(message.encode("utf-16-be")) // 2
single, multi = (160, 153) if is_gsm else (70, 67)
segments = 1 if units <= single else ceil(units / multi)
return {
"encoding": "TEXT" if is_gsm else "UNICODE",
"units": units,
"segments": max(1, segments),
"per_segment": single if units <= single else multi,
}
if __name__ == "__main__":
print(count_segments("Pay Rs. 1,499 by 5 PM to avoid late fees."))
# {'encoding': 'TEXT', 'units': 40, 'segments': 1, 'per_segment': 160}
print(count_segments("Pay ₹1,499 by 5 PM to avoid late fees."))
# {'encoding': 'UNICODE', ...} <- the rupee sign alone did this
The last two lines of that example are the entire article in miniature. Writing Rs. instead of ₹ keeps the message in GSM-7. It is a two-character copy change with a permanent effect on unit economics.
Java
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.stream.Collectors;
public final class SmsSegmentCounter {
private static final String BASIC =
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?"
+ "¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
private static final Set<Integer> GSM_BASIC =
BASIC.codePoints().boxed().collect(Collectors.toSet());
private static final Set<Integer> GSM_EXTENDED =
"^{}\\[~]|€".codePoints().boxed().collect(Collectors.toSet());
public record Result(String encoding, int units, int segments, int perSegment) {}
public static Result count(String message) {
int units = 0;
boolean isGsm = true;
for (int cp : message.codePoints().toArray()) {
if (GSM_BASIC.contains(cp)) units += 1;
else if (GSM_EXTENDED.contains(cp)) units += 2;
else { isGsm = false; break; }
}
if (!isGsm) {
units = message.getBytes(StandardCharsets.UTF_16BE).length / 2;
}
int single = isGsm ? 160 : 70;
int multi = isGsm ? 153 : 67;
int segments = units <= single ? 1 : (int) Math.ceil((double) units / multi);
return new Result(isGsm ? "TEXT" : "UNICODE",
units, Math.max(1, segments),
units <= single ? single : multi);
}
}
Pricing a Message Before You Send It
You do not have to trust your own counter. SMSGatewayCenter exposes a server-side length and credit calculator that returns the platform’s own verdict on any message string, which is the number that will actually be billed.
Endpoint: POST https://unify.smsgateway.center/SMSApi/info/msg
Parameters: userid and password, or an apiKey HTTP header, plus msg (the message text) and output (json or plain, default plain).
cURL
curl --request POST \
--url https://unify.smsgateway.center/SMSApi/info/msg \
--header 'content-type: application/x-www-form-urlencoded' \
--data-urlencode 'userid=YourUsername' \
--data-urlencode 'password=YourPassword' \
--data-urlencode 'msg=Hello World' \
--data-urlencode 'output=json'
Response
{
"response": {
"api": "info",
"action": "msg",
"status": "success",
"msg": "success",
"code": "200",
"count": 1,
"msgList": {
"encoding": "TEXT",
"length": 11,
"remaining": 149,
"credit": 1
}
}
}
Four fields, and every one of them is operationally useful:
| Field | Meaning | What to do with it |
|---|---|---|
encoding | TEXT for GSM-7, otherwise Unicode | Alert if a template you expect to be English comes back as Unicode. This is your autocorrect detector. |
length | Units consumed by the message | Compare against your local counter in CI; a mismatch means your counter has a bug |
remaining | Characters left in the current segment | Surface this in your template editor so copywriters see the cliff approaching |
credit | Credits that will be billed | The number to multiply by your slab rate |
The endpoint is documented at Get SMS Length and Cost and it is a POST-only HTTP API.
The one place every team should call this
Not at send time. At template save time.
Calling it per outbound message adds a network round trip to your hot path for no benefit, because your local counter should already be correct. Call it once when a template is created or edited, store the returned encoding and credit, and block the save if a template that is flagged English comes back as Unicode. That single guard catches the autocorrect problem permanently, at the only moment when a human is present to fix it.
Wire it into CI as well, alongside the pre-send template validation via SMSApi/validateTemplate described in the DLT registration guide. One check confirms the message is compliant, the other confirms it is affordable.
Variable Two: Route Class
India does not have one bulk SMS product, it has several regulatory categories with different rules and different rates. Choosing the wrong one costs money in both directions: overpaying for a transactional route to send marketing, or having marketing on a promotional route blocked because it went to a DND number at 10 PM.
| Route class | What it carries | Delivery window | DND handling | Sender ID |
|---|---|---|---|---|
| Transactional | Order confirmations, bank alerts, ticket details, service updates tied to an existing relationship | 24×7 | Delivered to DND numbers | Static alphanumeric |
| Promotional | Offers, discounts, campaign blasts to prospects | 10 AM to 9 PM | Filtered against DND | Numeric |
| OTP | One-time passwords and verification codes only | 24×7 | Delivered to DND numbers | Alphanumeric |
| Service Implicit | Messages arising from an existing transaction or relationship where consent is implied | Per TRAI rules | Per registered consent | Registered header |
| Service Explicit | Messages requiring recorded explicit consent from the subscriber | Per TRAI rules | Requires explicit consent record | Registered header |
The implicit and explicit distinction is the one that trips up most teams, because it is a consent classification rather than a content classification, and misclassifying it is a compliance problem before it is a cost problem. SMSGatewayCenter maintains dedicated explainers for service implicit SMS and service explicit SMS, and a broader overview at SMS types.
The cost angle: routing a message on the wrong class does not usually save money, it causes it to be scrubbed or blocked, at which point you have paid for a message nobody received. That is variable four in action, and it is why classification is a cost control, not just a compliance control.
What the Published Slabs Actually Say
These are the rates published on the SMSGatewayCenter pricing pages at the time of writing. All are for India, all carry one year validity, and all are quoted plus GST at the prevailing rate. Check the live pages before budgeting, since published rates change.
Transactional SMS
Source: Transactional SMS Pricing. 24×7 delivery, DND numbers delivered, static alphanumeric sender ID.
| SMS units | Rate per SMS | Plan price |
|---|---|---|
| 10,000 | INR 0.17 | INR 1,650 + GST |
| 50,000 | INR 0.15 | INR 7,500 + GST |
| 1,00,000 | INR 0.14 | INR 14,000 + GST |
| 5,00,000 | INR 0.12 | INR 60,000 + GST |
Promotional SMS
Source: Promotional SMS Pricing. 10 AM to 9 PM delivery, DND numbers filtered, numeric sender ID.
| SMS units | Rate per SMS | Plan price |
|---|---|---|
| 50,000 | INR 0.13 | INR 6,500 + GST |
| 1,00,000 | INR 0.12 | INR 12,000 + GST |
| 5,00,000 | INR 0.11 | INR 55,000 + GST |
| 10,00,000 | INR 0.10 | INR 1,00,000 + GST |
OTP SMS
Source: OTP SMS Pricing. 24×7 delivery.
| SMS units | Rate per SMS | Plan price |
|---|---|---|
| 10,000 | INR 0.17 | INR 1,650 + GST |
| 50,000 | INR 0.15 | INR 7,500 + GST |
| 1,00,000 | INR 0.14 | INR 14,000 + GST |
| 5,00,000 | INR 0.13 | INR 65,000 + GST |
What these tables tell you that a price comparison does not
Three things are worth extracting.
First, the transactional-to-promotional spread narrows as volume rises. At 50,000 units the gap is INR 0.15 against INR 0.13, about 15 percent. At 5,00,000 it is INR 0.12 against INR 0.11, about 9 percent. If you are large, splitting traffic across two route classes to chase the promotional rate buys you less than the operational complexity costs. If you are small, the entry barrier matters more: the transactional route starts at a 10,000-unit slab while promotional starts at 50,000.
Second, OTP diverges from transactional only at the top slab. The two are identical at 10,000, 50,000 and 1,00,000 units. At 5,00,000 units OTP is INR 0.13 against transactional INR 0.12. Below half a million messages, OTP and transactional pricing are the same product economically, so route the traffic by what it is rather than by what you think it costs.
Third, the entry slab has a rounding artefact worth noticing. INR 1,650 divided by 10,000 is INR 0.165, advertised as INR 0.17. That is in your favour, marginally, and it is a good reminder to divide plan price by units yourself rather than reading the headline rate.
Credits do not expire in twelve months, but they do expire
All three routes carry one year validity, and the pricing pages note that units carry forward if you renew within the due date. That structure has a cost implication most teams miss: buying a large slab to get the low rate and then under-sending means the effective rate is plan price divided by messages actually sent, not by units purchased.
| Slab bought | List rate | Messages actually sent in the year | Effective rate |
|---|---|---|---|
| 5,00,000 transactional at INR 60,000 | INR 0.12 | 5,00,000 | INR 0.12 |
| 5,00,000 transactional at INR 60,000 | INR 0.12 | 3,00,000 | INR 0.20 |
| 1,00,000 transactional at INR 14,000 | INR 0.14 | 1,00,000 | INR 0.14 |
Under-utilising a 5,00,000 slab by 40 percent produces a worse effective rate than buying the 1,00,000 slab at list. Buy the slab that matches your forecast, not the slab with the best headline number, and renew within the due date so unused units carry forward rather than lapsing.
Variable Three: Volume Slabs and the Commitment Trap
The slab structure rewards volume, which is normal and rational. The trap is treating a slab as a discount rather than as a commitment.
Two questions decide whether a bigger slab is worth it:
- What is your forecast confidence? If your annual volume estimate has a plus or minus 30 percent error bar, and the slab above requires you to be at the top of that range to break even, buy the smaller one. The maths above shows that under-utilisation destroys the discount faster than the discount creates value.
- Is your volume seasonal or steady? One year validity plus carry-forward on renewal makes seasonality survivable, but only if you actually renew on time. A festive-season retailer sending 70 percent of annual volume in one quarter should size the slab to the annual total and diarise the renewal date, not size it to peak-quarter run rate.
There is also a floor effect nobody mentions. Below roughly 50,000 messages a year, the per-SMS rate barely matters. At the 10,000-unit transactional slab, the entire annual spend is INR 1,650 plus GST. The DLT registration overhead and the engineering hours spent on integration dominate that figure by an order of magnitude. Small senders should optimise for integration simplicity and support quality, not for two paise.
Variable Four: Where the Deduction Actually Happens

This is the variable that produces the largest gap between expected and actual spend, and it is stated plainly in the published pricing terms:
“The applicable per-SMS rate will be deducted from your wallet while sending SMS.”
and
“Credits are non-refundable once SMS is successfully submitted to the operator.”
Read that as a state machine. Here is the lifecycle of a message and where money leaves:
| Stage | What it means | Billed? |
|---|---|---|
| 1. API request received | Your call reached SMSApi/send | No |
| 2. Validation | Auth, sender ID, DLT template match, mobile number format | No, a rejection here is free |
| 3. Submission to operator accepted | The operator has taken the message | Yes, deduction happens here |
| 4. Operator attempts delivery | Handset reachable? Subscriber active? | Already billed |
| 5. DLR returned | DELIVERED, UNDELIV, EXPIRED, ABSENT_SUB and so on | Already billed |
Everything from stage 3 onward is sunk cost. A message that reaches an absent subscriber, or is undelivered because the operator is unreachable, has already cost you a credit.
What this means practically
Your effective per-delivered-message rate is your slab rate divided by your delivery rate.
| Slab rate | Delivery rate | Effective cost per delivered message |
|---|---|---|
| INR 0.14 | 98% | INR 0.143 |
| INR 0.14 | 94% | INR 0.149 |
| INR 0.14 | 88% | INR 0.159 |
| INR 0.14 | 80% | INR 0.175 |
A provider at INR 0.12 with an 85 percent delivery rate costs INR 0.141 per delivered message. A provider at INR 0.14 with a 98 percent delivery rate costs INR 0.143. Those are functionally the same price, and the second one is a far better product because the messages arrive. This is precisely why comparing providers on the advertised rate is close to meaningless, and why the recommendation to compare actual delivery reports across providers, made in the guide on choosing bulk SMS providers in India, is the correct methodology.
Failures that are free
Not everything costs money. Rejections at stage 2 do not reach the operator, so they do not consume a credit. That includes:
- Authentication failures
- Invalid mobile number format, reported in the
invalidMobilefield of the send response - Template mismatch, which returns status code 188 from
SMSApi/validateTemplateand is rejected before submission - Blocked sender ID and invalid sender ID
- Duplicate rejection when
duplicatecheckis enabled
The last one is a genuine cost control. Enabling duplicatecheck on the send call stops a re-fired job from paying twice for the same message. It is not a substitute for idempotency in your own code, but it is a useful second line of defence, and it is free to turn on.
The engineering implication is direct: push every failure you can into stage 2. Validate mobile numbers with a proper library before submitting, validate templates against validateTemplate in CI, and check sender ID and encoding at template save time. Every failure you move from stage 4 to stage 2 converts a billed loss into a free rejection.
Variable Five: Retry Amplification
This is the cheapest problem to fix and the most expensive one to ignore.
A retry loop that does not distinguish between permanent and transient failures pays full price for every attempt. Consider a modest campaign:
| Scenario | Messages | Failures | Retry policy | Total credits billed |
|---|---|---|---|---|
| No retries | 1,00,000 | 6,000 | none | 1,00,000 |
| Blind retry, 3 attempts | 1,00,000 | 6,000 | retry all failures 3x | 1,18,000 |
| Classified retry | 1,00,000 | 6,000 (4,000 permanent, 2,000 transient) | retry transient only, 2x | 1,04,000 |
Blind retrying costs 18 percent more than not retrying at all, and the extra 18 percent buys you recovery only on the 2,000 genuinely transient failures. The classified policy recovers the same messages for 4 percent.
The classification rule is simple and the delivery error codes make it mechanical:
| Class | Examples | Retry? | Reason |
|---|---|---|---|
| Permanent | Invalid number, ABSENT_SUB on a dead number, blocked sender ID, template mismatch, DND block on promotional | Never | The next attempt will fail identically and cost the same |
| Transient | OPERATOR_DOWN, SYSTEM_FAIL, network timeout, HTTP 5xx | Yes, with backoff | The condition is time-limited |
| Ambiguous | Request timed out with no response | Yes, but only with an idempotency key | You do not know whether the platform accepted it, and retrying without a key risks paying twice for a delivered message |
That third row is the expensive one. A client-side timeout does not mean the message was not submitted. If it was submitted, you were already billed; retrying bills you again and the recipient gets the message twice. The fix is a business-level idempotency key plus duplicatecheck, covered in depth in the SMS API retry strategy guide.
You can fetch the authoritative code lists programmatically rather than hardcoding them, via POST https://unify.smsgateway.center/SMSApi/info/deliverycodes for delivery error codes and POST https://unify.smsgateway.center/SMSApi/info/responsecodes for API response codes. Build your permanent-versus-transient map from those lists and keep it in version control.
Suppression lists are a cost control
If a number returns a permanent failure three campaigns running, stop sending to it. A stale list of 40,000 dead numbers on a monthly campaign is 4,80,000 wasted credits a year, which at INR 0.12 is INR 57,600 spent on messages that cannot arrive. Suppression is boring, unglamorous and one of the highest-return things you can build.
Variable Six: DLT and the Costs That Are Not Per-SMS
DLT registration is mandatory for A2P messaging in India under TRAI’s Telecom Commercial Communications Customer Preference Regulations, and the SMSGatewayCenter pricing terms state it directly: DLT registration is required to send transactional SMS, and the sender ID and template must be pre-approved for delivery to happen.
The costs here sit outside the per-SMS rate:
| Cost item | Nature | Notes |
|---|---|---|
| Principal Entity registration on an operator DLT portal | One-time | Charged by the DLT portal operator, not by the messaging provider |
| Sender ID (header) registration | One-time per header | Alphanumeric, up to six characters for India |
| Template registration | One-time per template | Every content variant needs its own approved template |
| Template maintenance | Ongoing | Copy changes require re-approval, which means lead time |
| PE-TM binding | Operational | Binding your Principal Entity to a Telemarketer, which is what breaks when you switch providers |
The per-SMS impact is indirect but real. Template rigidity forces message design decisions that affect segment count. If your approved template is 168 characters and you cannot change it without re-approval, you are paying two credits per message until the next approval cycle. Getting the template under 160 GSM-7 units before submitting it for approval is a one-time act with a permanent recurring saving.
The other DLT cost is switching friction. PE-TM binding means your registrations are tied to your current telemarketer. Moving providers is not free, which weakens your negotiating position at renewal. Budget for the transition if you plan to move; the DLT SMS hub and the PE-TM binding chain explainer cover the mechanics.
We are deliberately not publishing DLT portal registration fees here. They are set by the individual operator portals, they differ between them, and they change. Check the portal you are registering with.
DND Scrubbing and Who Pays for a Blocked Message
DND is a route-class question, and the two behaviours are explicitly different in the published plan features.
| Route | DND behaviour per published plan features |
|---|---|
| Transactional | “DND Numbers Delivered” |
| OTP | 24×7 delivery |
| Promotional | “DND Numbers Filtered” |
For promotional traffic, scrubbing happens against the DND registry before or at submission. A number filtered out at that point has not been submitted to the operator, so it does not consume a credit under the “billed on successful submission” rule.
The cost implication is not the scrubbing itself, it is what it does to your campaign economics. If 25 percent of your promotional list is DND-registered, a campaign you budgeted as 1,00,000 messages reaches 75,000 people. You have not overpaid, but your cost per reached contact is a third higher than your model assumed, and your cost per conversion is higher still.
Scrub your list before you plan the campaign, not after. Then size the slab against the reachable list, not the raw list.
International SMS: A Different Cost Model Entirely
Everything above describes India-domestic pricing. The published terms are explicit that the domestic rates apply for India only.
International SMS runs on a different economic model:
- Per-destination pricing. There is no single international rate. Each destination country and often each operator within it has its own termination cost, and the spread between the cheapest and most expensive destinations is very large.
- Different sender ID rules. Some countries mandate alphanumeric sender IDs, some mandate numeric, some require pre-registration comparable to DLT, and some forbid sender ID overrides entirely.
- Different regulatory overhead. There is no global equivalent of DLT, so compliance work is per-country.
- Higher absolute rates. SMSGatewayCenter’s international SMS pricing plans start at INR 20,000 for high-volume campaigns.
If you are budgeting a mixed domestic and international programme, model them as two separate cost lines with separate rates and separate delivery-rate assumptions. Blending them into one average rate produces a forecast that is wrong in both directions.
Reconciling Your Bill Against the DLR API
Everything above is a model. This section is how you check the model against reality, and it is the part almost nobody does.
The delivery report API returns per-message records that include both a cost field and a length field. That is enough to audit your spend message by message.
Endpoint: GET or POST https://unify.smsgateway.center/SMSApi/reports/status with method=getDlr, fromdate, todate, pageLimit, and optionally mobileNo, uuId or startCursor, plus output=json.
Each record in reports_dlrList carries: country, amount, msgType, cost, deliveryTime, length, channel, msgId, cause, mobileNo, uuId, dltTemplateId, globalErrorCode, cursorId, network, senderName, flashMsg, submitTime, text, status.
A reconciliation script
import os
from collections import defaultdict
from decimal import Decimal
import httpx
BASE = "https://unify.smsgateway.center/SMSApi/reports/status"
def fetch_dlr(from_date: str, to_date: str, page_limit: int = 500):
"""Yield DLR records, following the cursor until exhausted."""
cursor = None
with httpx.Client(timeout=httpx.Timeout(30.0, connect=10.0)) as client:
while True:
data = {
"method": "getDlr",
"userid": os.environ["SGC_USERID"],
"password": os.environ["SGC_PASSWORD"],
"fromdate": from_date,
"todate": to_date,
"pageLimit": page_limit,
"output": "json",
}
if cursor:
data["startCursor"] = cursor
resp = client.post(BASE, data=data)
resp.raise_for_status()
payload = resp.json()
records = payload.get("reports_dlrList") or []
if not records:
return
for record in records:
yield record
cursor = records[-1].get("cursorId")
if not cursor:
return
def reconcile(from_date: str, to_date: str) -> None:
spend_by_status = defaultdict(Decimal)
spend_by_encoding = defaultdict(Decimal)
count_by_status = defaultdict(int)
wasted = Decimal("0")
total = Decimal("0")
for rec in fetch_dlr(from_date, to_date):
cost = Decimal(str(rec.get("cost") or "0"))
status = (rec.get("status") or "UNKNOWN").upper()
encoding = (rec.get("msgType") or "unknown").lower()
total += cost
spend_by_status[status] += cost
spend_by_encoding[encoding] += cost
count_by_status[status] += 1
# Anything that was billed but not delivered is pure loss.
if status not in ("DELIVERED", "DELIVRD"):
wasted += cost
print(f"Total billed : {total:.4f}")
print(f"Billed, not delivered: {wasted:.4f} "
f"({(wasted / total * 100) if total else 0:.2f}% of spend)")
print()
print("Spend by delivery status")
for status, amount in sorted(spend_by_status.items(),
key=lambda kv: kv[1], reverse=True):
print(f" {status:<16} {count_by_status[status]:>8,} {amount:>12.4f}")
print()
print("Spend by encoding")
for encoding, amount in sorted(spend_by_encoding.items(),
key=lambda kv: kv[1], reverse=True):
print(f" {encoding:<16} {amount:>12.4f}")
if __name__ == "__main__":
reconcile("2026-07-01", "2026-07-31")
Run this monthly. Three numbers come out of it and each one drives a decision:
- Percentage of spend on undelivered messages. If this is above five percent, your list hygiene or your route selection is the problem, not your rate.
- Spend by encoding. If
unicodespend is materially higher than your intended regional-language volume, you have an encoding leak somewhere in a template. - Average
lengthper message. If it sits just above a segment boundary, there is copy to trim.
The text field in the DLR record also lets you find the specific offending templates. Group by dltTemplateId, sum cost, sort descending, and you have a ranked list of exactly which templates are consuming your budget. Fix the top three and you have usually found the whole problem. For background on interpreting the status values, see the knowledge base entry on understanding delivery reports.
How to Model Your Monthly Bulk SMS Cost
Six steps. Do them in this order.
Step 1: Enumerate your message classes. Not campaigns, classes. An OTP is a class. An order confirmation is a class. A festive offer is a class. Each has a distinct template, route and volume pattern.
Step 2: For each class, get the true credit count. Run the template through SMSApi/info/msg and record the credit value. Do not estimate from character count.
Step 3: Multiply volume by credits, not by messages. This is the step teams skip. A class sending 50,000 messages at 2 credits each consumes 1,00,000 units.
Step 4: Assign each class to a route and apply the correct slab rate. Sum the total units across all classes first to determine which slab you qualify for, then apply that rate.
Step 5: Divide by your expected delivery rate. Use the actual rate from your DLR reconciliation, not an aspirational one. This converts cost-per-submitted into cost-per-delivered.
Step 6: Add the retry factor and the fixed compliance overhead. Retry factor is 1.0 if you retry nothing and rises with your transient-failure rate times your attempt count. Compliance overhead is amortised annually.
The formula:
Monthly cost = Σ (volume_class × credits_class) × slab_rate
÷ delivery_rate
× retry_factor
+ (annual_compliance_cost ÷ 12)
+ GST at prevailing rate
A Worked Example
A mid-size Indian e-commerce company, using the published transactional and promotional slabs above.
Message classes
| Class | Monthly volume | Template length | Encoding | Credits each | Units consumed | Route |
|---|---|---|---|---|---|---|
| OTP for login | 120,000 | 92 chars | GSM-7 | 1 | 120,000 | OTP |
| Order confirmation | 65,000 | 148 chars | GSM-7 | 1 | 65,000 | Transactional |
| Shipping update | 65,000 | 171 chars | GSM-7 | 2 | 130,000 | Transactional |
| Delivery confirmation | 60,000 | 118 chars | GSM-7 | 1 | 60,000 | Transactional |
| Festive offer (English) | 200,000 | 139 chars | GSM-7 | 1 | 200,000 | Promotional |
| Festive offer (Hindi) | 80,000 | 96 chars | Unicode | 2 | 160,000 | Promotional |
Naive model, the one most teams build
510,000 messages per month. Apply a blended INR 0.13. Result: INR 66,300 per month.
Actual model
Transactional and OTP units: 120,000 + 65,000 + 130,000 + 60,000 = 375,000 units. At an annualised 4.5 million units, this clears the 5,00,000 slab comfortably, so apply INR 0.12 for transactional and INR 0.13 for OTP at that tier.
- OTP: 120,000 × INR 0.13 = INR 15,600
- Transactional: 255,000 × INR 0.12 = INR 30,600
Promotional units: 200,000 + 160,000 = 360,000 units. Annualised well past 10,00,000, so INR 0.10.
- Promotional: 360,000 × INR 0.10 = INR 36,000
Subtotal: INR 82,200.
Now apply delivery rate. Assume 96 percent on transactional and OTP, 89 percent on promotional after DND filtering and handset unreachability.
- Transactional and OTP effective: INR 46,200 ÷ 0.96 = INR 48,125 worth of value delivered per INR 46,200 spent, so the true cost per delivered message is 4.2 percent higher than list.
- Promotional at 89 percent is 12.4 percent higher than list.
Add a retry factor of 1.03 (a well-classified retry policy on a 4 percent transient failure rate).
Total: roughly INR 84,700 per month plus GST, against a naive estimate of INR 66,300. The naive model is 28 percent low, and every rupee of the gap comes from the two rows highlighted below.
The two lines worth fixing
Shipping update at 171 characters. Eleven characters over the boundary, costing a second credit on 65,000 messages a month. Trimming it to 160 characters or fewer saves 65,000 units a month, which at INR 0.12 is INR 7,800 a month, INR 93,600 a year. The change is deleting eleven characters of copy and getting the template re-approved once.
Hindi festive offer at 96 Unicode characters. Unavoidable encoding, but 96 characters needs two segments at 67 each, using 134 of a possible 134 units. Trimming to 70 characters would make it one credit and save 80,000 units a month, INR 8,000 a month at the promotional rate. Whether 70 Unicode characters is enough to carry the offer is a marketing judgement, but it should be a conscious one, made with the number in front of you.
Those two changes together are roughly INR 190,000 a year and require zero engineering work. That is the entire argument for treating segment counting as a first-class concern.
Want to know what your current templates actually cost? Sign in and run each one through the SMS Length and Cost API, or talk to our team and we will audit your message mix against your delivery reports.
Cost Reduction Checklist
Ordered by return on effort, highest first.
| # | Action | Effort | Typical saving |
|---|---|---|---|
| 1 | Audit every template’s credit count via SMSApi/info/msg and trim anything one to fifteen characters over a boundary | Hours | 5 to 20 percent |
| 2 | Block Unicode on templates intended to be English at save time | Hours | Highly variable, occasionally enormous |
| 3 | Classify failures and stop retrying permanent ones | Days | 3 to 15 percent |
| 4 | Build a suppression list from repeated permanent failures | Days | 2 to 8 percent |
| 5 | Enable duplicatecheck on the send call | Minutes | Insurance against re-fired jobs |
| 6 | Move number validation client-side, before submission | Days | Converts billed failures into free rejections |
| 7 | Scrub promotional lists against DND before sizing the campaign | Hours | Improves cost-per-reach, not raw cost |
| 8 | Size your slab against a realistic annual forecast, not peak run rate | Hours | Up to 40 percent effective rate |
| 9 | Reconcile monthly against DLR cost and length | Hours per month | Finds everything above |
| 10 | Replace ₹ with Rs., straight quotes for curly, hyphens for en dashes in every template | Minutes | Prevents the single worst failure mode |
Item 10 takes ten minutes and is the highest-leverage line in the table. Do it today.
Channel Cost Decision Matrix
SMS is not always the cheapest channel for a given message, and it is not always the right one. Cost per message is the wrong comparison; cost per successful outcome is the right one.
| Message type | SMS | WhatsApp Business API | RCS | Voice | Recommendation |
|---|---|---|---|---|---|
| OTP for login | Reliable, universal, low unit cost | Fails silently when the user has no WhatsApp or no data, no timeout signal | Requires RCS-capable handset | Expensive, slow | SMS, always as primary |
| Order confirmation | Cheap, universal | Richer, needs an active session or a template | Rich, patchy reach | Inappropriate | SMS, with WhatsApp as an enhancement |
| Shipping tracking with a map | Two credits and a bare URL | Native rich card, better experience | Native rich card | No | WhatsApp or RCS if reach allows |
| Festive promotional blast | Lowest unit cost, DND-filtered | Per-conversation pricing, template approval, opt-in required | Rich but limited reach | No | SMS for reach and cost |
| Regional-language campaign | 2 to 3x cost from Unicode | Same content, different pricing model | Rich | No | Model both, the crossover point moves with volume |
| Payment reminder needing acknowledgement | One-way, no read receipt | Two-way, read receipts, buttons | Two-way | High cost, high attention | WhatsApp for engagement, SMS fallback |
| Critical alert to an unknown-capability device | Universal | Uncertain | Uncertain | Universal | SMS, with voice escalation |
The asymmetry worth internalising: SMS fails observably and late, WhatsApp fails silently with no timeout signal. That difference is why WhatsApp should not be the primary OTP channel regardless of what the per-message arithmetic says. The full engineering comparison is in the SMS API versus WhatsApp Business API guide, and if you want to run channels in sequence rather than choosing one, multi-channel fallback lets you specify which delivery error codes trigger a step to the next channel rather than falling through on any failure.
Ten Costly Mistakes
1. Counting characters with strlen() and dividing by 160. Wrong twice: it misses the extension-table characters that cost two units, and it misses the 153-character concatenation rule.
2. Letting copywriters draft templates in a word processor. Autocorrect converts apostrophes and dashes to non-GSM-7 characters that render identically and triple the cost. Draft in a plain-text field with a live credit counter.
3. Comparing providers on advertised rate alone. A rate is meaningless without a delivery rate. Ask for delivery reports, run a paid pilot, and compute cost per delivered message.
4. Retrying every failure the same way. Permanent failures never succeed and always bill. Classify before you retry.
5. Assuming undelivered means unbilled. Deduction happens at successful submission to the operator. Your bill and your delivery report are different documents.
6. Buying the largest slab for the headline rate. Under-utilisation produces a worse effective rate than a smaller slab at list price. Size to forecast.
7. Submitting a DLT template without checking its credit count first. Once approved, you are stuck with the length until you re-approve. Optimise before submission, not after.
8. Applying one blended rate across English and regional-language traffic. They differ by more than 2x. Model them separately.
9. Never reconciling. If you have never run the DLR cost and length fields into a report, you do not know what you are actually spending money on. Almost every team that does this for the first time finds a surprise.
10. Optimising the rate before optimising the credits. Negotiating two paise off INR 0.14 is a 14 percent saving on a variable you barely control. Fixing a template that is 11 characters over the boundary is a 50 percent saving on that class, available immediately, with no negotiation.
Frequently Asked Questions
What is the actual per-SMS cost of bulk SMS in India? It depends on route class and volume. On SMSGatewayCenter’s published slabs, transactional SMS ranges from INR 0.17 per SMS at 10,000 units down to INR 0.12 at 5,00,000 units, promotional from INR 0.13 at 50,000 units down to INR 0.10 at 10,00,000 units, and OTP from INR 0.17 at 10,000 units down to INR 0.13 at 5,00,000 units, all plus GST with one year validity. Your effective cost is higher than any of these figures because messages over 160 GSM-7 characters consume multiple credits and because credits are consumed on submission rather than delivery.
Why did my SMS bill increase without my volume changing? The most common cause by far is an encoding change. A template edit that introduced a curly apostrophe, an en dash, an ellipsis or a rupee sign converts the message from GSM-7 to Unicode, cutting per-credit capacity from 160 characters to 70 and often tripling the cost of that message class. Run each template through POST https://unify.smsgateway.center/SMSApi/info/msg and check the encoding field. The second most common cause is a template edit that pushed length past 160 characters, doubling the credit count.
Is one SMS always one credit? No. One credit covers 160 characters in GSM-7 encoding or 70 characters in Unicode. Beyond that, messages are split into concatenated segments of 153 GSM-7 characters or 67 Unicode characters each, because the User Data Header consumes payload space. A 300-character English message is two credits, a 300-character Hindi message is five.
Do I pay for SMS that are not delivered? Yes, in most cases. SMSGatewayCenter’s published pricing terms state that credits are non-refundable once the SMS is successfully submitted to the operator. A message that fails after submission, for example because the handset is switched off or the subscriber is unreachable, has already consumed a credit. Failures caught before submission, such as invalid number format, template mismatch or an invalid sender ID, do not consume a credit.
Does a promotional message to a DND number cost me money? On a promotional route, DND numbers are filtered before submission to the operator, so a filtered number does not consume a credit. What it does affect is your reachable audience size, so scrub your list before sizing a campaign rather than after.
Why does sending in Hindi cost more than sending in English? Devanagari characters are outside the GSM-7 alphabet, so the message must be encoded in UCS-2, which fits 70 characters per credit instead of 160, or 67 per segment in a concatenated message. That is roughly 2.3 times the cost for the same character count. This is an inherent property of the GSM standard, not a provider charge.
How can I check what an SMS will cost before I send it? Call POST https://unify.smsgateway.center/SMSApi/info/msg with your credentials and the msg parameter. The response returns encoding, length, remaining and credit, where credit is the number of billable units. Documented at the Get SMS Length and Cost page. Call it when a template is saved rather than on every send.
Which characters silently increase SMS cost? Two groups. First, characters outside GSM-7 that force the whole message to Unicode: curly quotes, en and em dashes, the ellipsis character, the rupee symbol, bullets, emoji and all Indic scripts. Second, ten characters that are in GSM-7 but consume two units each via the extension table: {, }, [, ], ~, ^, \, | and the euro sign.
Is transactional or promotional SMS cheaper? Promotional is cheaper at every comparable volume, but the routes are not interchangeable. Promotional delivers only between 10 AM and 9 PM, filters DND numbers and uses a numeric sender ID. Transactional delivers 24×7 to DND numbers with a static alphanumeric sender ID. Sending transactional content on a promotional route is a compliance failure, and the saving is not worth it.
Does GST apply to bulk SMS in India? The published plan prices are quoted plus GST at the prevailing rate. Budget the tax on top of the plan price, and check the current rate with your finance team rather than assuming.
What happens to unused SMS credits? Published plans carry one year validity, and the pricing pages note that units are carried forward if you renew within the due date. Credits that lapse are lost, which is why buying a slab significantly larger than your forecast is a false economy: your effective rate is plan price divided by messages actually sent.
Does DLT registration cost extra per message? No. DLT costs are fixed and one-time: Principal Entity registration, sender ID registration and template registration, all charged by the operator DLT portals rather than as a per-SMS surcharge. The indirect per-message cost comes from template rigidity, since an approved template that is 11 characters over the 160-character boundary costs a second credit on every message until you re-approve a shorter version.
How do I audit what I actually spent on SMS? Call the delivery report API at https://unify.smsgateway.center/SMSApi/reports/status with method=getDlr. Each record in reports_dlrList includes cost, length, msgType, status and dltTemplateId. Group by status to find spend on undelivered messages, group by msgType to find unexpected Unicode spend, and group by dltTemplateId to rank templates by total cost. That last grouping usually identifies the entire problem in three rows.
Is SMS cheaper than WhatsApp Business API? They use different pricing models, SMS per message segment and WhatsApp per conversation window, so a direct unit comparison is misleading. The right comparison is cost per successful outcome, which depends on your reach: WhatsApp reaches only users who have the app and data connectivity, and it fails silently when they do not, whereas SMS reaches any active handset and reports its failures. For OTP and critical alerts, SMS wins on reliability regardless of unit price. For rich, engagement-driven messaging to an opted-in audience, WhatsApp often wins on outcome.
Ready to size a plan against a real forecast rather than a headline rate? Review the current transactional, promotional and OTP slabs, or get in touch for a quote built from your actual message classes. Check out our fair pricing policy.