SMS API vs WhatsApp Business API: When to Use Each (Engineering Decision Guide)

A routing-level decision guide for engineers choosing between SMS API and WhatsApp Business API. Covers the 24-hour session window, template governance under DLT and Meta, real code for both endpoints, fallback orchestration, and a message-class decision matrix.

Featured image for SMS API vs WhatsApp Business API: When to Use Each (Engineering Decision Guide)
  1. Executive Summary
  2. TL;DR
  3. The Question Most Teams Ask Wrong
  4. Capability Comparison at the Protocol Level
  5. The 24-Hour Session Window Is the Real Architectural Difference
  6. Template Governance: DLT and Meta Are Not the Same Problem
  7. Reach and Failure Modes
  8. Sending on Both Channels: Real Code
  9. Building a Channel Router in Practice
  10. Fallback Orchestration: SMS as the Floor, Not the Alternative
  11. Idempotency Across Two Channels
  12. The Decision Matrix by Message Class
  13. Pros and Cons
  14. Migration Anti-Patterns
  15. FAQs

Executive Summary

Use the SMS API when delivery must be guaranteed to any handset regardless of app installation, network type, or user consent state, and use the WhatsApp Business API when the message benefits from rich content, buttons, or a two-way conversation and the recipient is a known WhatsApp user. Most production systems need both, and the correct place to decide is a routing layer that evaluates each individual message against session state and message class, not a one-time platform choice made during procurement.

The failure mode this article exists to prevent is teams treating channel selection as a static configuration value. Once WhatsApp is integrated, product teams tend to route everything through it because it looks better, and then discover that a password reset sent as a template message to a user who uninstalled WhatsApp six months ago never arrives and produces no actionable failure signal in time. The inverse failure is routing a rich order-tracking flow through SMS and paying for four concatenated segments to deliver a link that a WhatsApp card would have carried natively with a tappable button.

Isometric diagram showing a routing node splitting one message stream into an SMS channel and a WhatsApp Business API channel

This article treats the choice as an engineering problem. It covers the session window state machine, the two different template approval regimes you have to satisfy, real send code against both endpoints on the same gateway, error-code-driven fallback, and cross-channel idempotency.


TL;DR

  • SMS reaches every phone. WhatsApp reaches every phone that has WhatsApp installed, registered, and connected to data. These are different guarantees and only one of them is suitable as a system floor.
  • WhatsApp splits messages into two classes: template messages, which you can send at any time but only from pre-approved templates, and free-form session messages, which you can only send inside a 24-hour window opened by the user. SMS has no equivalent state machine.
  • India adds DLT registration on top of SMS. WhatsApp adds Meta template approval. Both are approval queues, but they gate different things and neither substitutes for the other.
  • On SMSGatewayCenter both channels sit behind the same account and the same parameter conventions: POST https://unify.smsgateway.center/SMSApi/send for SMS and POST https://unify.smsgateway.center/WAApi/send for WhatsApp.
  • Do not build channel selection into your application code. Build a router that takes message class plus session state and returns a channel, so the policy can change without a deploy.
  • Fallback should be triggered by specific delivery error codes, not by any failure. Falling back on every failure duplicates messages and inflates cost.

The Question Most Teams Ask Wrong

“Should we use SMS or WhatsApp?” is the wrong question because it presumes a single answer applies to your whole message volume. A typical mid-size e-commerce backend emits at least six distinct message classes in a week: login OTP, order confirmation, shipment dispatch, delivery attempt failure, cart abandonment, and a weekly promotional push. Those six have almost nothing in common in terms of urgency, richness, consent basis, or acceptable failure rate.

The right question is: for this specific message, what is the cost of it not arriving, and what is the cost of it arriving in a lower-fidelity form?

That framing produces a clean split. Messages where non-arrival breaks a user flow (OTP, password reset, transaction alerts, fraud warnings) belong on the channel with the highest unconditional reach, which is SMS. Messages where arrival is desirable but not load-bearing, and where richness increases conversion (cart recovery, catalogue pushes, support threads, appointment rescheduling), belong on WhatsApp.

If you want the business-level view of this comparison covering pricing models, engagement benchmarks, and industry-specific recommendations, read the companion piece WhatsApp Business API vs SMS API: The Ultimate Comparison Guide. This article stays at the implementation layer and assumes you have already decided both channels are on the table.


Capability Comparison at the Protocol Level

DimensionSMS APIWhatsApp Business API
TransportStore-and-forward over the operator SS7 or SMPP pathHTTPS to the Business Platform, then IP delivery to the app
Prerequisite on the handsetA SIM and signalApp installed, number registered, data or Wi-Fi connectivity
Message body limit160 GSM-7 characters per segment, 70 for Unicode, concatenated beyond thatSubstantially longer bodies in a single message, no segment billing
Rich contentNone natively, links onlyImages, video, documents, headers, footers, buttons, list menus
Unsolicited sendsPermitted subject to local regulation and DND rulesOnly via pre-approved templates
Two-wayRequires a long code, short code, or virtual numberNative and bidirectional by default
Sender identityAlphanumeric header, six characters in IndiaA verified business phone number (WABA number)
Content pre-approvalDLT template registration in IndiaMeta template approval, globally
Delivery signalDLR with an operator-sourced cause codeSent, delivered, and read states
Read receiptsNot availableAvailable
Regulatory regimeTRAI and DLT in India, per-country elsewhereMeta Business Platform policy, globally uniform

The line that matters most in that table is the second one. SMS has a single prerequisite that is satisfied by definition for anyone you have a mobile number for. WhatsApp has three, and you cannot verify any of them before you attempt a send. That asymmetry is why SMS remains the correct floor for anything critical, and no amount of WhatsApp engagement advantage changes it.


The 24-Hour Session Window Is the Real Architectural Difference

Most comparison articles list rich media and cost and stop there. The difference that actually changes your code is that WhatsApp is stateful and SMS is not.

WhatsApp messages fall into two categories:

Template messages. Pre-registered message bodies with variable placeholders, approved by Meta before use. You can send these at any time to any opted-in number. This is the only way to initiate contact.

Free-form session messages. Arbitrary content, sendable only inside a 24-hour window that opens when the user sends you a message and closes 24 hours after their most recent inbound message. Any inbound message from the user re-opens or extends the window.

Illustration about The 24-Hour Session Window Is the Real Architectural Difference

This means your WhatsApp integration has to track per-recipient session state. Your SMS integration does not. Concretely:

  • You need a store keyed by recipient number holding the timestamp of the last inbound message.
  • Before every free-form send you check whether now - last_inbound < 24h. If not, you either fall back to a template or fall back to SMS.
  • Inbound webhook processing is not optional for WhatsApp the way it can be for one-way SMS blasts, because inbound events are what maintain your session state.

On the SMSGatewayCenter WhatsApp endpoint this distinction surfaces directly in the request. The documented behaviour is that you include templateName for a template send, and for a session-based send you omit templateName and send the open message body. From the Send WhatsApp Business Message API reference:

If you are sending Session based message, then send open message and remove templateName parameter. If its in session then the message will get delivered.

That single sentence encodes the whole state machine into one optional parameter, which is convenient but also easy to get wrong. If your code omits templateName for a recipient whose window has closed, the send does not become a template send automatically. It fails. Your router has to know the session state before it builds the payload.

A minimal session store

CREATE TABLE wa_session (
  msisdn        VARCHAR(20) PRIMARY KEY,
  last_inbound  TIMESTAMP NOT NULL,
  updated_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_wa_session_last_inbound ON wa_session (last_inbound);

Update last_inbound on every inbound WhatsApp event. Read it before every free-form send. Treat a missing row as a closed window. That is the entire mechanism, and it is the piece teams most often bolt on after their first month of production incidents.


Template Governance: DLT and Meta Are Not the Same Problem

Both channels put an approval queue between you and your users, but they gate different things and the operational shape is different.

AspectIndia DLT (SMS)Meta template approval (WhatsApp)
What is registeredEntity, header (sender ID), and message templateMessage template, tied to a WABA
ScopeIndia onlyGlobal
Variable syntax{#var#}Positional or named placeholders
Enforcement pointOperator scrubbing at submission timeMeta, at template approval and send time
Typical rejection causeContent mismatch against the registered templatePolicy category mismatch, promotional content in a utility template
Change costRe-registration through the DLT portalRe-submission of the template
Failure signalA template-mismatch delivery statusTemplate rejection or send-time error

The practical consequence for a dual-channel system is that the same business message needs two separate approvals with two different sets of rules, and they will not stay in sync. Your marketing team changes the shipment-dispatch wording, and you now have a DLT template and a Meta template that say different things. Whichever channel the router picks, the user sees a different message.

The mitigation is to hold a single canonical message definition in your own system with both external template identifiers attached to it, and to refuse to send if either identifier is missing or stale:

{
  "message_key": "order.dispatched",
  "canonical_body": "Your order no. {{order_id}} has been dispatched. Expected to arrive within {{days}} day. Track: {{url}}",
  "sms": {
    "dltTemplateId": "1207xxxxxxxxxxxxxx",
    "senderid": "SMSGWC",
    "status": "approved"
  },
  "whatsapp": {
    "templateName": "demo_order",
    "wabaNumber": "9170396xxxxx",
    "status": "approved"
  }
}

On the SMS side you can verify the rendered body against the registered DLT template before submitting, using the validateTemplate endpoint at POST https://unify.smsgateway.center/SMSApi/validateTemplate, which returns status code 188 with reason message template mismatch when the body does not match. Running that check in CI against every template in your registry catches drift before it reaches production. There is no exact equivalent pre-flight check on the WhatsApp side, so template state there has to be reconciled from the Create WhatsApp Template API and your own records.

For the full DLT registration walkthrough including entity, header, and template steps, see the DLT SMS overview. For Meta’s own template category rules, the authoritative source is the WhatsApp Business Platform documentation.


Reach and Failure Modes

The channels fail differently, and the difference matters more than the raw delivery percentages that most comparison pages quote.

SMS fails observably and late. A message sits in the SMSC store-and-forward queue, and you get a DLR back with a cause code that tells you something actionable: the subscriber was absent, the operator was unreachable, the number is blacklisted, the template did not match. Those codes map to concrete remediation. ABSENT_SUB means retry later. TEMPLATE_CHECK_FAILED means fix your template and never retry as-is. The delivery report API and the DLR concepts explainer cover how to consume these.

WhatsApp fails silently and early, or not at all. If the number is not on WhatsApp, you find out at send time. If the number is on WhatsApp but the device is offline, the message sits at Meta and is delivered whenever the device reconnects, which could be days. There is no “absent subscriber, give up” equivalent. A message stuck in sent but never delivered is indistinguishable from a message that will be delivered in six hours.

This asymmetry has a direct design implication: you cannot use WhatsApp delivery status as a timeout signal for a time-critical flow. If you send an OTP over WhatsApp and wait for a delivered status before deciding to fall back to SMS, you will either wait too long or fall back too eagerly. Time-critical messages should go to SMS first, full stop. The rich channel is for messages where a multi-hour delivery delay is acceptable.

For the SMS side of this, the cause-code-to-action mapping is covered in depth in SMS API Retry Strategy: How to Handle Failed Message Delivery, and the transport-level equivalents are in SMPP Error Codes: Full Reference and Fixes.


Sending on Both Channels: Real Code

Both endpoints live on the same gateway host and share authentication conventions, which makes a dual-channel abstraction straightforward. The SMS endpoint is POST https://unify.smsgateway.center/SMSApi/send and the WhatsApp endpoint is POST https://unify.smsgateway.center/WAApi/send, both accepting application/x-www-form-urlencoded bodies with userid and password (or an API key header).

cURL: SMS with DLT parameters

curl -X POST 'https://unify.smsgateway.center/SMSApi/send' \
  --data-urlencode 'userid=YourUsername' \
  --data-urlencode 'password=********' \
  --data-urlencode 'sendMethod=quick' \
  --data-urlencode 'msgType=text' \
  --data-urlencode 'mobile=919xxxxxxxxx6' \
  --data-urlencode 'senderid=SMSGWC' \
  --data-urlencode 'msg=Your order no. 88214 has been dispatched. Expected to arrive within 2 day. Click to view status https://exmpl.co/t/88214 - Demo Message. SMS Gateway Center' \
  --data-urlencode 'dltEntityId=1101xxxxxxxxxxxxxx' \
  --data-urlencode 'dltTemplateId=1207xxxxxxxxxxxxxx' \
  --data-urlencode 'duplicatecheck=true' \
  --data-urlencode 'output=json'

cURL: WhatsApp template message

curl -X POST 'https://unify.smsgateway.center/WAApi/send' \
  --data-urlencode 'userid=YourUsername' \
  --data-urlencode 'password=********' \
  --data-urlencode 'sendMethod=quick' \
  --data-urlencode 'msgType=text' \
  --data-urlencode 'mobile=919xxxxxxxxx6' \
  --data-urlencode 'wabaNumber=9170396xxxxx' \
  --data-urlencode 'templateName=demo_order' \
  --data-urlencode 'msg=Your order no. {#var#} has been dispatched. Expected to arrive within {#var#} day. Click to view status {#var#} - Demo Message. SMS Gateway Center' \
  --data-urlencode 'header=Order DEMO' \
  --data-urlencode 'footer=www.smsgatewaycenter.com' \
  --data-urlencode 'output=json'

Note what changed and what did not. sendMethod, msgType, mobile, msg, and output are identical across both. The SMS-specific parameters are senderid, dltEntityId, and dltTemplateId. The WhatsApp-specific ones are wabaNumber, templateName, header, and footer. That overlap is what makes a shared adapter clean.

PHP: a dual-channel sender

<?php

final class MessagingClient
{
    private const SMS_URL = 'https://unify.smsgateway.center/SMSApi/send';
    private const WA_URL  = 'https://unify.smsgateway.center/WAApi/send';

    public function __construct(
        private string $userid,
        private string $password
    ) {}

    public function sendSms(
        string $mobile,
        string $msg,
        string $senderid,
        string $dltEntityId,
        string $dltTemplateId
    ): array {
        return $this->post(self::SMS_URL, [
            'userid'        => $this->userid,
            'password'      => $this->password,
            'sendMethod'    => 'quick',
            'msgType'       => 'text',
            'mobile'        => $mobile,
            'senderid'      => $senderid,
            'msg'           => $msg,
            'dltEntityId'   => $dltEntityId,
            'dltTemplateId' => $dltTemplateId,
            'duplicatecheck'=> 'true',
            'output'        => 'json',
        ]);
    }

    public function sendWhatsAppTemplate(
        string $mobile,
        string $msg,
        string $wabaNumber,
        string $templateName,
        ?string $header = null,
        ?string $footer = null
    ): array {
        $payload = [
            'userid'       => $this->userid,
            'password'     => $this->password,
            'sendMethod'   => 'quick',
            'msgType'      => 'text',
            'mobile'       => $mobile,
            'wabaNumber'   => $wabaNumber,
            'templateName' => $templateName,
            'msg'          => $msg,
            'output'       => 'json',
        ];
        if ($header !== null) { $payload['header'] = $header; }
        if ($footer !== null) { $payload['footer'] = $footer; }

        return $this->post(self::WA_URL, $payload);
    }

    /**
     * Session send. templateName is deliberately absent.
     * Only call this when you have verified the 24h window is open.
     */
    public function sendWhatsAppSession(
        string $mobile,
        string $msg,
        string $wabaNumber
    ): array {
        return $this->post(self::WA_URL, [
            'userid'     => $this->userid,
            'password'   => $this->password,
            'sendMethod' => 'quick',
            'msgType'    => 'text',
            'mobile'     => $mobile,
            'wabaNumber' => $wabaNumber,
            'msg'        => $msg,
            'output'     => 'json',
        ]);
    }

    private function post(string $url, array $fields): array
    {
        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST  => 'POST',
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
            CURLOPT_POSTFIELDS     => http_build_query($fields),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/x-www-form-urlencoded',
            ],
        ]);

        $response = curl_exec($curl);
        $errno    = curl_errno($curl);
        curl_close($curl);

        if ($errno !== 0) {
            throw new RuntimeException("Transport error: {$errno}");
        }

        return json_decode($response, true) ?? [];
    }
}

Node.js: WhatsApp media message with a dynamic button

import { setTimeout as delay } from 'node:timers/promises';

const WA_URL = 'https://unify.smsgateway.center/WAApi/send';

async function sendWhatsAppMedia({
  userid,
  password,
  mobile,
  wabaNumber,
  templateName,
  msg,
  mediaUrl,
  mediaType = 'image',
  buttonUrlPath,
}) {
  const body = new URLSearchParams({
    userid,
    password,
    sendMethod: 'quick',
    msgType: 'media',
    mobile,
    wabaNumber,
    templateName,
    msg,
    mediaUrl,
    mediaType,
    output: 'json',
  });

  if (buttonUrlPath) body.append('buttonUrlPath', buttonUrlPath);

  const res = await fetch(WA_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }

  return res.json();
}

// Example: product image with a tappable deep link on the CTA button.
const result = await sendWhatsAppMedia({
  userid: process.env.SGC_USER,
  password: process.env.SGC_PASS,
  mobile: '919xxxxxxxxx6',
  wabaNumber: '9170396xxxxx',
  templateName: 'demo_order',
  msg: 'Your order no. {#var#} has been dispatched. Expected to arrive within {#var#} day. Click to view status {#var#}',
  mediaUrl: 'https://cdn.example.in/products/88214.jpg',
  mediaType: 'image',
  buttonUrlPath: '/track/88214',
});

console.log(result);

The mediaUrl, mediaType, and buttonUrlPath parameters have no SMS equivalent. This is the concrete shape of the “rich content” advantage: an image, a headline, and a tappable action in one message, against an SMS that would carry the same information as a bare URL split across multiple billed segments.

Python: the router in miniature

import os
import time
from dataclasses import dataclass
from urllib.parse import urlencode

import requests

SMS_URL = "https://unify.smsgateway.center/SMSApi/send"
WA_URL = "https://unify.smsgateway.center/WAApi/send"
SESSION_WINDOW_SECONDS = 24 * 60 * 60

HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}


@dataclass(frozen=True)
class MessageSpec:
    key: str                 # e.g. "auth.otp" or "order.dispatched"
    critical: bool           # non-arrival breaks a user flow
    rich: bool               # benefits from media or buttons
    body: str


def _post(url: str, fields: dict) -> dict:
    resp = requests.post(url, data=urlencode(fields), headers=HEADERS, timeout=20)
    resp.raise_for_status()
    return resp.json()


def send_sms(mobile: str, spec: MessageSpec, senderid: str,
             dlt_entity_id: str, dlt_template_id: str) -> dict:
    return _post(SMS_URL, {
        "userid": os.environ["SGC_USER"],
        "password": os.environ["SGC_PASS"],
        "sendMethod": "quick",
        "msgType": "text",
        "mobile": mobile,
        "senderid": senderid,
        "msg": spec.body,
        "dltEntityId": dlt_entity_id,
        "dltTemplateId": dlt_template_id,
        "duplicatecheck": "true",
        "output": "json",
    })


def send_whatsapp(mobile: str, spec: MessageSpec, waba_number: str,
                  template_name: str | None) -> dict:
    fields = {
        "userid": os.environ["SGC_USER"],
        "password": os.environ["SGC_PASS"],
        "sendMethod": "quick",
        "msgType": "text",
        "mobile": mobile,
        "wabaNumber": waba_number,
        "msg": spec.body,
        "output": "json",
    }
    # Omitting templateName makes this a session send.
    if template_name:
        fields["templateName"] = template_name
    return _post(WA_URL, fields)


def session_open(last_inbound_ts: float | None) -> bool:
    if last_inbound_ts is None:
        return False
    return (time.time() - last_inbound_ts) < SESSION_WINDOW_SECONDS


def route(spec: MessageSpec, mobile: str, last_inbound_ts: float | None,
          registry: dict) -> str:
    """Return the channel to attempt first."""
    if spec.critical:
        return "sms"
    entry = registry.get(spec.key, {})
    wa = entry.get("whatsapp", {})
    if wa.get("status") != "approved":
        return "sms"
    if spec.rich or session_open(last_inbound_ts):
        return "whatsapp"
    return "sms"

Java: HttpClient send

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.stream.Collectors;

public final class GatewayClient {

    private static final String SMS_URL = "https://unify.smsgateway.center/SMSApi/send";
    private static final String WA_URL  = "https://unify.smsgateway.center/WAApi/send";

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    public String send(String url, Map<String, String> fields) throws Exception {
        String form = fields.entrySet().stream()
                .map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
                        + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
                .collect(Collectors.joining("&"));

        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .timeout(Duration.ofSeconds(20))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(form))
                .build();

        HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
        return res.body();
    }

    public String sendWhatsAppTemplate(String mobile, String msg) throws Exception {
        return send(WA_URL, Map.of(
                "userid", System.getenv("SGC_USER"),
                "password", System.getenv("SGC_PASS"),
                "sendMethod", "quick",
                "msgType", "text",
                "mobile", mobile,
                "wabaNumber", "9170396xxxxx",
                "templateName", "demo_order",
                "msg", msg,
                "output", "json"
        ));
    }

    public String sendSms(String mobile, String msg) throws Exception {
        return send(SMS_URL, Map.of(
                "userid", System.getenv("SGC_USER"),
                "password", System.getenv("SGC_PASS"),
                "sendMethod", "quick",
                "msgType", "text",
                "mobile", mobile,
                "senderid", "SMSGWC",
                "msg", msg,
                "dltEntityId", "1101xxxxxxxxxxxxxx",
                "dltTemplateId", "1207xxxxxxxxxxxxxx",
                "output", "json"
        ));
    }
}

Postman

Import both collections rather than hand-building requests. The SMS collection is at Download SMS API Postman Collection and the WhatsApp collection is at Download WhatsApp Business API Postman Collection. Set userid, password, wabaNumber, and senderid as environment variables so you can flip between a sandbox and production account without editing each request.


Building a Channel Router in Practice

Everything above collapses into one function that takes a message and returns a channel. Keep it out of your feature code.

Step 1: Classify the message. Attach two boolean properties to every message key at definition time: critical (non-arrival breaks a user flow) and rich (media or buttons materially improve the outcome). These are product decisions, not runtime ones.

Step 2: Check for hard overrides. Critical messages go to SMS unconditionally. Do not let engagement metrics override this.

Step 3: Check template availability. If the WhatsApp template for this message key is not in an approved state, the WhatsApp path does not exist. Fall through to SMS.

Step 4: Check session state. Look up last_inbound for the recipient. An open window permits free-form sends and is a strong signal the user is an active WhatsApp user.

Step 5: Check recipient preference and opt-in. A stored channel preference beats all heuristics. If the user has explicitly asked for SMS, honour it.

Step 6: Send, and record the attempt against your idempotency key.

Step 7: Evaluate the delivery outcome and decide whether the fallback rule fires.

The route() function in the Python sample above implements steps 1 through 4 in fifteen lines. Steps 5 through 7 are where most of the real complexity lives, and step 7 is the subject of the next section.


Fallback Orchestration: SMS as the Floor, Not the Alternative

The naive fallback is “if WhatsApp fails, send SMS.” That is wrong, and it is expensive.

Consider a WhatsApp send that fails because the template was rejected overnight. Falling back to SMS delivers the message, which is fine. Now consider a WhatsApp send that returns a transient gateway error, gets retried by your queue, succeeds on the retry, and also triggers your fallback. The user gets the message twice and you paid for both.

The fix is to make fallback conditional on specific error codes rather than on failure in general. SMSGatewayCenter exposes this as a configurable behaviour in Multi-Channel Fallback, and the trigger error codes knowledge base entry describes the semantics:

Trigger error codes let you control when the platform moves to the next channel. By default, any failure on a channel (except the last) can trigger the next step. You can narrow this to specific delivery error codes only.

That default is the permissive one. Narrowing it is almost always the right move for a production system.

Illustration about Fallback Orchestration: SMS as the Floor, Not the Alternative

A workable classification of outcomes into fallback decisions:

Outcome classExampleFallback?Retry same channel?
Recipient not reachable on this channelNumber not registered on WhatsAppYes, immediatelyNo
Content rejected by the channelTemplate rejected or mismatchedYes, if the other channel’s template is validNo
Transient gateway or network errorTimeout, 5xxNoYes, with backoff
Rate limitedThrottled by the platformNoYes, after the window
Permanently blocked recipientBlacklisted, opted outNoNo
Delivered but unreadsent with no delivered after N hoursOnly for non-critical messages, and only onceNo

The last row is the one that needs a policy decision from your product team rather than a default. A shipment update that has sat in sent for six hours probably warrants an SMS. A promotional push does not.

Two rules that prevent most fallback incidents:

  1. Fallback fires at most once per message. Record a fallback_used flag against the idempotency key. Never chain.
  2. The final channel in the chain never falls back. This is why SMS belongs at the end, not the beginning, of any chain that includes it.

For the retry mechanics underneath this (backoff, jitter, dead-lettering, and per-code retry eligibility) the deeper treatment is in SMS API Retry Strategy: How to Handle Failed Message Delivery.


Idempotency Across Two Channels

Single-channel idempotency is straightforward: generate a key, deduplicate on it, and use the gateway’s duplicatecheck parameter as a second line of defence. Cross-channel idempotency is harder, because the same logical message now has two possible physical sends and two different gateway-side message identifiers.

Use a two-level key:

logical_key   = sha256(tenant_id + message_key + recipient + business_event_id)
physical_key  = logical_key + ":" + channel + ":" + attempt_number

Deduplicate sends on physical_key. Deduplicate the user-visible outcome on logical_key. Before any send, check whether any physical attempt under this logical key has already reached a terminal success state. If yes, do not send on any channel.

The subtle bug this prevents: a WhatsApp send succeeds, the delivery webhook is delayed, your fallback timer fires and sends the SMS, and then the WhatsApp delivered event arrives. Without a logical-key check the user has already received both. With it, the SMS send is rejected at dispatch time because the logical key is already terminal.

The gateway-side duplicatecheck=true parameter on the SMS endpoint helps but does not solve this, because it only sees SMS traffic. Cross-channel deduplication has to live in your application.

Register a webhook to receive delivery events in near real time rather than polling, using POST https://unify.smsgateway.center/SMSApi/webhook/create as documented in the Create Webhook API. Polling the delivery report endpoint on a schedule is a reasonable reconciliation backstop but a poor primary mechanism, because the polling interval directly becomes your fallback latency floor.


The Decision Matrix by Message Class

This is the table to copy into your own routing configuration and adapt.

Message classPrimary channelFallbackReasoning
Login OTP / 2FASMSNone neededReach is the entire requirement. A WhatsApp OTP that arrives after the code expires is worse than useless. See OTP SMS
Password resetSMSNone neededSame as above, and often the user is locked out of the device the app is on
Payment or fraud alertSMSNone neededRegulatory expectation plus time criticality
Order confirmationWhatsApp templateSMS on unreachableRich receipt with an image performs better, but the user must get it
Shipment dispatchedWhatsApp template with media and buttonSMS on unreachableThe tracking button is the whole value
Delivery attempt failedSMSNoneTime-critical, the courier is at the door
Appointment reminderWhatsApp templateSMS at T minus 2 hours if undeliveredRich reminder with a reschedule button, SMS as the safety net
Cart abandonmentWhatsApp templateNoneNon-critical, do not pay twice for a marketing nudge
Promotional campaignWhatsApp templateNoneNon-critical, and SMS promotional sends carry their own DND and regulatory overhead
Customer support replyWhatsApp session messageNoneThe window is open by definition, since the user just messaged you
Survey / NPSWhatsApp template with buttonsNoneButtons collapse the response friction
Service outage noticeSMSWhatsApp as a supplement, not a fallbackBroadcast reach matters, and outages often mean users are already having connectivity trouble

The pattern in that table: anything where the user is waiting for the message right now goes to SMS. Anything where the user will look at it when convenient goes to WhatsApp. Latency tolerance is a better discriminator than message content.


Pros and Cons

SMS API

Pros

  • Universal reach with no app, no account, and no data connection required
  • Works during degraded connectivity, which is exactly when outage and fraud alerts matter most
  • Delivery codes are operator-sourced and actionable
  • Simple stateless integration, no session tracking
  • Well-understood regulatory framework in India via DLT

Cons

  • No rich content, no buttons, no read receipts
  • Segment-based billing punishes long messages and Unicode
  • Sender identity is a six-character header with limited brand presence in India
  • Two-way requires separate infrastructure (long code or virtual number)
  • Link-based CTAs suffer from link truncation and phishing wariness

WhatsApp Business API

Pros

  • Rich media, headers, footers, and interactive buttons in a single message
  • Native two-way conversation with no extra number to provision
  • Read receipts give real engagement signal
  • Verified business identity rather than a six-character string
  • Long message bodies without per-segment cost

Cons

  • Requires the app installed, registered, and connected
  • Stateful: the 24-hour window has to be tracked and enforced in your code
  • Template approval adds a second, non-substitutable governance queue
  • No reliable timeout signal, which disqualifies it for time-critical flows
  • A rejected or paused template silently removes an entire message class from the channel

Migration Anti-Patterns

Moving OTP to WhatsApp to save cost. The saving is real and the risk is not worth it. A percentage of your users will not have WhatsApp reachable at the moment they need to log in, and those users hit your support queue. Support contact cost per incident dwarfs the per-message saving.

Treating WhatsApp as an SMS replacement rather than an addition. Every team that decommissions SMS entirely rebuilds it within a year, usually after an incident. Keep the SMS path warm even at low volume.

Building channel selection into feature code. Fifteen services each with their own if (user.prefersWhatsApp) branch is fifteen places to change when the policy changes. One router, called by all of them.

Skipping inbound webhook processing on WhatsApp. Without inbound events you cannot maintain session state, which means you can never send a free-form message, which means you have paid for a conversational channel and are using it as a one-way template broadcast.

One template registry per channel. Two registries drift. One canonical definition with two external identifiers attached does not.

Falling back on every failure. Duplicate messages, double cost, and user annoyance. Narrow the trigger set to codes that genuinely mean “this channel cannot deliver this message.”


FAQs

Q: Can I send OTPs over WhatsApp Business API?

Technically yes, using an approved authentication-category template. Architecturally you should not make it the only path. WhatsApp gives no reliable timeout signal, so if the message does not arrive you cannot detect it fast enough to fall back within the OTP validity period. Send the OTP over SMS. If you want WhatsApp OTP as a user-selectable option, offer it as a preference and keep SMS as the default.

Q: What exactly is the 24-hour window and when does it reset?

The window opens when the user sends a message to your WhatsApp business number and stays open for 24 hours from their most recent inbound message. Each new inbound message resets the clock. While it is open you can send free-form content. When it is closed you can only send pre-approved templates.

Q: Do I need DLT registration if I only use WhatsApp?

DLT applies to SMS traffic in India. If you send no SMS you do not need DLT registration for WhatsApp traffic. But if you are running any fallback to SMS, that fallback traffic is SMS traffic and needs DLT-registered entity, header, and template like any other send.

Q: Can the same message body be used for both channels?

You should keep one canonical body and derive both from it, but the registered versions will not be byte-identical. DLT uses {#var#} placeholders and enforces exact-match scrubbing at the operator. Meta templates use their own placeholder convention and category rules. Keep the canonical definition in your system, generate both, and validate the SMS side with the validateTemplate endpoint so drift surfaces in CI.

Q: How do I know whether a number is on WhatsApp before sending?

You generally do not, reliably, ahead of time. Design for the send attempt to be the check. Cache the outcome per recipient so a number that has failed as unreachable on WhatsApp routes straight to SMS on the next attempt, and re-test occasionally, since users do install the app.

Q: Should fallback go WhatsApp to SMS, or SMS to WhatsApp?

Almost always WhatsApp to SMS. SMS is the higher-reach channel, so it belongs last in the chain as the terminal step. Chains ending in WhatsApp have a floor that can fail, which defeats the purpose of having a chain.

Q: How is billing different between the two channels?

SMS is billed per segment, so a 300-character Unicode message costs several times a short GSM-7 one. WhatsApp is billed on a conversation basis rather than per message, so a long rich message and a short one within the same conversation cost the same. For current rates on either channel, see the pricing page or contact the team, since rates vary by destination and volume.

Q: What happens if my WhatsApp template gets rejected after it was approved?

Every message class bound to that template loses its WhatsApp path immediately. This is why your router should check template status at send time rather than assuming an approved template stays approved. Build an alert on template status changes, and make sure the SMS equivalent for that message key is registered and current so the fallback actually works when you need it.

Q: Can I use RCS instead of choosing between these two?

RCS gives you rich cards and branded sender identity over the carrier network rather than an app, which puts it in an interesting middle position. Handset and carrier support are still uneven, so it is best treated as an additional channel in a fallback chain rather than a replacement for either. See RCS Messaging for the current picture.


Run both channels from one account. SMSGatewayCenter serves SMS, WhatsApp Business API, RCS, and voice from a single account with a shared authentication model and a common parameter convention, so your router talks to one gateway instead of three vendors. Start with the SMS API getting started guide, pull both Postman collections, and have a working dual-channel send in an afternoon. Sign up or talk to the team about routing and fallback configuration for your message classes.


Save this interesting page on your favorite Social Media

Blog Author logo

SMS Gateway Center Desk

SMS Gateway Center is one of the largest and leading SMS Provider in India. It is run by a large professional team to cater small companies to large corporate companies. SMS Gateway Center is associated with the best operators in India covering the entire states in India. SMS Gateway Center has been serving through its SMS Resellers in more than 20 states in India. To become our SMS Reseller, kindly contact us

Looking for the best business communication solutions, get in touch!