{"id":2536,"date":"2026-07-31T16:10:19","date_gmt":"2026-07-31T10:40:19","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2536"},"modified":"2026-07-31T16:10:22","modified_gmt":"2026-07-31T10:40:22","slug":"sms-api-vs-whatsapp-business-api-when-to-use-each","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-vs-whatsapp-business-api-when-to-use-each\/","title":{"rendered":"SMS API vs WhatsApp Business API: When to Use Each (Engineering Decision Guide)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#executive-summary\" data-type=\"internal\" data-id=\"#executive-summary\">Executive Summary<\/a><\/li>\n\n\n\n<li><a href=\"#tldr\" data-type=\"internal\" data-id=\"#tldr\">TL;DR<\/a><\/li>\n\n\n\n<li><a href=\"#the-question-most-teams-ask-wrong\" data-type=\"internal\" data-id=\"#the-question-most-teams-ask-wrong\">The Question Most Teams Ask Wrong<\/a><\/li>\n\n\n\n<li><a href=\"#capability-comparison-at-the-protocol-level\" data-type=\"internal\" data-id=\"#capability-comparison-at-the-protocol-level\">Capability Comparison at the Protocol Level<\/a><\/li>\n\n\n\n<li><a href=\"#the-24-hour-session-window-is-the-real-architectural-difference\" data-type=\"internal\" data-id=\"#the-24-hour-session-window-is-the-real-architectural-difference\">The 24-Hour Session Window Is the Real Architectural Difference<\/a><\/li>\n\n\n\n<li><a href=\"#template-governance-dlt-and-meta-are-not-the-same-problem\" data-type=\"internal\" data-id=\"#template-governance-dlt-and-meta-are-not-the-same-problem\">Template Governance: DLT and Meta Are Not the Same Problem<\/a><\/li>\n\n\n\n<li><a href=\"#reach-and-failure-modes\" data-type=\"internal\" data-id=\"#reach-and-failure-modes\">Reach and Failure Modes<\/a><\/li>\n\n\n\n<li><a href=\"#sending-on-both-channels-real-code\" data-type=\"internal\" data-id=\"#sending-on-both-channels-real-code\">Sending on Both Channels: Real Code<\/a><\/li>\n\n\n\n<li><a href=\"#building-a-channel-router-in-practice\" data-type=\"internal\" data-id=\"#building-a-channel-router-in-practice\">Building a Channel Router in Practice<\/a><\/li>\n\n\n\n<li><a href=\"#fallback-orchestration-sms-as-the-floor-not-the-alternative\" data-type=\"internal\" data-id=\"#fallback-orchestration-sms-as-the-floor-not-the-alternative\">Fallback Orchestration: SMS as the Floor, Not the Alternative<\/a><\/li>\n\n\n\n<li><a href=\"#idempotency-across-two-channels\" data-type=\"internal\" data-id=\"#idempotency-across-two-channels\">Idempotency Across Two Channels<\/a><\/li>\n\n\n\n<li><a href=\"#the-decision-matrix-by-message-class\" data-type=\"internal\" data-id=\"#the-decision-matrix-by-message-class\">The Decision Matrix by Message Class<\/a><\/li>\n\n\n\n<li><a href=\"#pros-and-cons\">Pros and Cons<\/a><\/li>\n\n\n\n<li><a href=\"#migration-anti-patterns\" data-type=\"internal\" data-id=\"#migration-anti-patterns\">Migration Anti-Patterns<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\" data-type=\"internal\" data-id=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Executive Summary<\/h2>\n\n\n\n<a id=\"executive-summary\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-vs-whatsapp-business-api-routing-decision.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"480\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-vs-whatsapp-business-api-routing-decision.webp\" alt=\"Isometric diagram showing a routing node splitting one message stream into an SMS channel and a WhatsApp Business API channel\" class=\"wp-image-2537\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-vs-whatsapp-business-api-routing-decision.webp 800w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-vs-whatsapp-business-api-routing-decision-300x180.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-vs-whatsapp-business-api-routing-decision-768x461.webp 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<a id=\"tldr\"><\/a>\n\n\n\n<ul class=\"wp-block-list\">\n<li>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.<\/li>\n\n\n\n<li>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.<\/li>\n\n\n\n<li>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.<\/li>\n\n\n\n<li>On SMSGatewayCenter both channels sit behind the same account and the same parameter conventions: <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/send<\/code> for SMS and <code>POST https:\/\/unify.smsgateway.center\/WAApi\/send<\/code> for WhatsApp.<\/li>\n\n\n\n<li>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.<\/li>\n\n\n\n<li>Fallback should be triggered by specific delivery error codes, not by any failure. Falling back on every failure duplicates messages and inflates cost.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Question Most Teams Ask Wrong<\/h2>\n\n\n\n<a id=\"the-question-most-teams-ask-wrong\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Should we use SMS or WhatsApp?&#8221; 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you want the business-level view of this comparison covering pricing models, engagement benchmarks, and industry-specific recommendations, read the companion piece <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/whatsapp-business-api-vs-sms-api-2025\/\">WhatsApp Business API vs SMS API: The Ultimate Comparison Guide<\/a>. This article stays at the implementation layer and assumes you have already decided both channels are on the table.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Capability Comparison at the Protocol Level<\/h2>\n\n\n\n<a id=\"capability-comparison-at-the-protocol-level\"><\/a>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Dimension<\/th><th>SMS API<\/th><th>WhatsApp Business API<\/th><\/tr><\/thead><tbody><tr><td>Transport<\/td><td>Store-and-forward over the operator SS7 or SMPP path<\/td><td>HTTPS to the Business Platform, then IP delivery to the app<\/td><\/tr><tr><td>Prerequisite on the handset<\/td><td>A SIM and signal<\/td><td>App installed, number registered, data or Wi-Fi connectivity<\/td><\/tr><tr><td>Message body limit<\/td><td>160 GSM-7 characters per segment, 70 for Unicode, concatenated beyond that<\/td><td>Substantially longer bodies in a single message, no segment billing<\/td><\/tr><tr><td>Rich content<\/td><td>None natively, links only<\/td><td>Images, video, documents, headers, footers, buttons, list menus<\/td><\/tr><tr><td>Unsolicited sends<\/td><td>Permitted subject to local regulation and DND rules<\/td><td>Only via pre-approved templates<\/td><\/tr><tr><td>Two-way<\/td><td>Requires a long code, short code, or virtual number<\/td><td>Native and bidirectional by default<\/td><\/tr><tr><td>Sender identity<\/td><td>Alphanumeric header, six characters in India<\/td><td>A verified business phone number (WABA number)<\/td><\/tr><tr><td>Content pre-approval<\/td><td>DLT template registration in India<\/td><td>Meta template approval, globally<\/td><\/tr><tr><td>Delivery signal<\/td><td>DLR with an operator-sourced cause code<\/td><td>Sent, delivered, and read states<\/td><\/tr><tr><td>Read receipts<\/td><td>Not available<\/td><td>Available<\/td><\/tr><tr><td>Regulatory regime<\/td><td>TRAI and DLT in India, per-country elsewhere<\/td><td>Meta Business Platform policy, globally uniform<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The 24-Hour Session Window Is the Real Architectural Difference<\/h2>\n\n\n\n<a id=\"the-24-hour-session-window-is-the-real-architectural-difference\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp messages fall into two categories:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Template messages.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Free-form session messages.<\/strong> 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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/24-hour-session-window-real-architectural-difference.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"480\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/24-hour-session-window-real-architectural-difference.webp\" alt=\"Illustration about The 24-Hour Session Window Is the Real Architectural Difference\" class=\"wp-image-2539\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/24-hour-session-window-real-architectural-difference.webp 800w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/24-hour-session-window-real-architectural-difference-300x180.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/24-hour-session-window-real-architectural-difference-768x461.webp 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This means your WhatsApp integration has to track per-recipient session state. Your SMS integration does not. Concretely:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You need a store keyed by recipient number holding the timestamp of the last inbound message.<\/li>\n\n\n\n<li>Before every free-form send you check whether <code>now - last_inbound &lt; 24h<\/code>. If not, you either fall back to a template or fall back to SMS.<\/li>\n\n\n\n<li>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.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">On the SMSGatewayCenter WhatsApp endpoint this distinction surfaces directly in the request. The documented behaviour is that you include <code>templateName<\/code> for a template send, and for a session-based send you omit <code>templateName<\/code> and send the open message body. From <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-whatsapp-business-message\/\">the Send WhatsApp Business Message API reference<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">If you are sending Session based message, then send open message and remove <code>templateName<\/code> parameter. If its in session then the message will get delivered.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>templateName<\/code> 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A minimal session store<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE TABLE wa_session (\n  msisdn        VARCHAR(20) PRIMARY KEY,\n  last_inbound  TIMESTAMP NOT NULL,\n  updated_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX idx_wa_session_last_inbound ON wa_session (last_inbound);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Update <code>last_inbound<\/code> 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Template Governance: DLT and Meta Are Not the Same Problem<\/h2>\n\n\n\n<a id=\"template-governance-dlt-and-meta-are-not-the-same-problem\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">Both channels put an approval queue between you and your users, but they gate different things and the operational shape is different.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Aspect<\/th><th>India DLT (SMS)<\/th><th>Meta template approval (WhatsApp)<\/th><\/tr><\/thead><tbody><tr><td>What is registered<\/td><td>Entity, header (sender ID), and message template<\/td><td>Message template, tied to a WABA<\/td><\/tr><tr><td>Scope<\/td><td>India only<\/td><td>Global<\/td><\/tr><tr><td>Variable syntax<\/td><td><code>{#var#}<\/code><\/td><td>Positional or named placeholders<\/td><\/tr><tr><td>Enforcement point<\/td><td>Operator scrubbing at submission time<\/td><td>Meta, at template approval and send time<\/td><\/tr><tr><td>Typical rejection cause<\/td><td>Content mismatch against the registered template<\/td><td>Policy category mismatch, promotional content in a utility template<\/td><\/tr><tr><td>Change cost<\/td><td>Re-registration through the DLT portal<\/td><td>Re-submission of the template<\/td><\/tr><tr><td>Failure signal<\/td><td>A template-mismatch delivery status<\/td><td>Template rejection or send-time error<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The practical consequence for a dual-channel system is that <strong>the same business message needs two separate approvals with two different sets of rules, and they will not stay in sync<\/strong>. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"message_key\": \"order.dispatched\",\n  \"canonical_body\": \"Your order no. {{order_id}} has been dispatched. Expected to arrive within {{days}} day. Track: {{url}}\",\n  \"sms\": {\n    \"dltTemplateId\": \"1207xxxxxxxxxxxxxx\",\n    \"senderid\": \"SMSGWC\",\n    \"status\": \"approved\"\n  },\n  \"whatsapp\": {\n    \"templateName\": \"demo_order\",\n    \"wabaNumber\": \"9170396xxxxx\",\n    \"status\": \"approved\"\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On the SMS side you can verify the rendered body against the registered DLT template before submitting, using the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/validateTemplate\/\">validateTemplate endpoint<\/a> at <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/validateTemplate<\/code>, which returns status code <code>188<\/code> with reason <code>message template mismatch<\/code> 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 <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/create-whatsapp-template\/\">Create WhatsApp Template API<\/a> and your own records.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the full DLT registration walkthrough including entity, header, and template steps, see the <a href=\"https:\/\/www.smsgatewaycenter.com\/dlt-sms\/\">DLT SMS overview<\/a>. For Meta&#8217;s own template category rules, the authoritative source is the <a href=\"https:\/\/developers.facebook.com\/docs\/whatsapp\/cloud-api\/\">WhatsApp Business Platform documentation<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Reach and Failure Modes<\/h2>\n\n\n\n<a id=\"reach-and-failure-modes\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">The channels fail differently, and the difference matters more than the raw delivery percentages that most comparison pages quote.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SMS fails observably and late.<\/strong> 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. <code>ABSENT_SUB<\/code> means retry later. <code>TEMPLATE_CHECK_FAILED<\/code> means fix your template and never retry as-is. The <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-delivery-report\/\">delivery report API<\/a> and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/understanding-delivery-reports-dlr\/\">DLR concepts explainer<\/a> cover how to consume these.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>WhatsApp fails silently and early, or not at all.<\/strong> 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 &#8220;absent subscriber, give up&#8221; equivalent. A message stuck in <code>sent<\/code> but never <code>delivered<\/code> is indistinguishable from a message that will be delivered in six hours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This asymmetry has a direct design implication: <strong>you cannot use WhatsApp delivery status as a timeout signal for a time-critical flow.<\/strong> If you send an OTP over WhatsApp and wait for a <code>delivered<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the SMS side of this, the cause-code-to-action mapping is covered in depth in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-retry-strategy-handling-failed-messages\/\">SMS API Retry Strategy: How to Handle Failed Message Delivery<\/a>, and the transport-level equivalents are in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/smpp-error-codes-full-reference-and-fixes\/\">SMPP Error Codes: Full Reference and Fixes<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Sending on Both Channels: Real Code<\/h2>\n\n\n\n<a id=\"sending-on-both-channels-real-code\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">Both endpoints live on the same gateway host and share authentication conventions, which makes a dual-channel abstraction straightforward. The SMS endpoint is <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/send<\/code> and the WhatsApp endpoint is <code>POST https:\/\/unify.smsgateway.center\/WAApi\/send<\/code>, both accepting <code>application\/x-www-form-urlencoded<\/code> bodies with <code>userid<\/code> and <code>password<\/code> (or an API key header).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL: SMS with DLT parameters<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST 'https:\/\/unify.smsgateway.center\/SMSApi\/send' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'password=********' \\\n  --data-urlencode 'sendMethod=quick' \\\n  --data-urlencode 'msgType=text' \\\n  --data-urlencode 'mobile=919xxxxxxxxx6' \\\n  --data-urlencode 'senderid=SMSGWC' \\\n  --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' \\\n  --data-urlencode 'dltEntityId=1101xxxxxxxxxxxxxx' \\\n  --data-urlencode 'dltTemplateId=1207xxxxxxxxxxxxxx' \\\n  --data-urlencode 'duplicatecheck=true' \\\n  --data-urlencode 'output=json'\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">cURL: WhatsApp template message<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST 'https:\/\/unify.smsgateway.center\/WAApi\/send' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'password=********' \\\n  --data-urlencode 'sendMethod=quick' \\\n  --data-urlencode 'msgType=text' \\\n  --data-urlencode 'mobile=919xxxxxxxxx6' \\\n  --data-urlencode 'wabaNumber=9170396xxxxx' \\\n  --data-urlencode 'templateName=demo_order' \\\n  --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' \\\n  --data-urlencode 'header=Order DEMO' \\\n  --data-urlencode 'footer=www.smsgatewaycenter.com' \\\n  --data-urlencode 'output=json'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note what changed and what did not. <code>sendMethod<\/code>, <code>msgType<\/code>, <code>mobile<\/code>, <code>msg<\/code>, and <code>output<\/code> are identical across both. The SMS-specific parameters are <code>senderid<\/code>, <code>dltEntityId<\/code>, and <code>dltTemplateId<\/code>. The WhatsApp-specific ones are <code>wabaNumber<\/code>, <code>templateName<\/code>, <code>header<\/code>, and <code>footer<\/code>. That overlap is what makes a shared adapter clean.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP: a dual-channel sender<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\nfinal class MessagingClient\n{\n    private const SMS_URL = 'https:\/\/unify.smsgateway.center\/SMSApi\/send';\n    private const WA_URL  = 'https:\/\/unify.smsgateway.center\/WAApi\/send';\n\n    public function __construct(\n        private string $userid,\n        private string $password\n    ) {}\n\n    public function sendSms(\n        string $mobile,\n        string $msg,\n        string $senderid,\n        string $dltEntityId,\n        string $dltTemplateId\n    ): array {\n        return $this-&gt;post(self::SMS_URL, &#91;\n            'userid'        =&gt; $this-&gt;userid,\n            'password'      =&gt; $this-&gt;password,\n            'sendMethod'    =&gt; 'quick',\n            'msgType'       =&gt; 'text',\n            'mobile'        =&gt; $mobile,\n            'senderid'      =&gt; $senderid,\n            'msg'           =&gt; $msg,\n            'dltEntityId'   =&gt; $dltEntityId,\n            'dltTemplateId' =&gt; $dltTemplateId,\n            'duplicatecheck'=&gt; 'true',\n            'output'        =&gt; 'json',\n        ]);\n    }\n\n    public function sendWhatsAppTemplate(\n        string $mobile,\n        string $msg,\n        string $wabaNumber,\n        string $templateName,\n        ?string $header = null,\n        ?string $footer = null\n    ): array {\n        $payload = &#91;\n            'userid'       =&gt; $this-&gt;userid,\n            'password'     =&gt; $this-&gt;password,\n            'sendMethod'   =&gt; 'quick',\n            'msgType'      =&gt; 'text',\n            'mobile'       =&gt; $mobile,\n            'wabaNumber'   =&gt; $wabaNumber,\n            'templateName' =&gt; $templateName,\n            'msg'          =&gt; $msg,\n            'output'       =&gt; 'json',\n        ];\n        if ($header !== null) { $payload&#91;'header'] = $header; }\n        if ($footer !== null) { $payload&#91;'footer'] = $footer; }\n\n        return $this-&gt;post(self::WA_URL, $payload);\n    }\n\n    \/**\n     * Session send. templateName is deliberately absent.\n     * Only call this when you have verified the 24h window is open.\n     *\/\n    public function sendWhatsAppSession(\n        string $mobile,\n        string $msg,\n        string $wabaNumber\n    ): array {\n        return $this-&gt;post(self::WA_URL, &#91;\n            'userid'     =&gt; $this-&gt;userid,\n            'password'   =&gt; $this-&gt;password,\n            'sendMethod' =&gt; 'quick',\n            'msgType'    =&gt; 'text',\n            'mobile'     =&gt; $mobile,\n            'wabaNumber' =&gt; $wabaNumber,\n            'msg'        =&gt; $msg,\n            'output'     =&gt; 'json',\n        ]);\n    }\n\n    private function post(string $url, array $fields): array\n    {\n        $curl = curl_init();\n        curl_setopt_array($curl, &#91;\n            CURLOPT_URL            =&gt; $url,\n            CURLOPT_RETURNTRANSFER =&gt; true,\n            CURLOPT_CUSTOMREQUEST  =&gt; 'POST',\n            CURLOPT_TIMEOUT        =&gt; 20,\n            CURLOPT_HTTP_VERSION   =&gt; CURL_HTTP_VERSION_1_1,\n            CURLOPT_POSTFIELDS     =&gt; http_build_query($fields),\n            CURLOPT_HTTPHEADER     =&gt; &#91;\n                'Content-Type: application\/x-www-form-urlencoded',\n            ],\n        ]);\n\n        $response = curl_exec($curl);\n        $errno    = curl_errno($curl);\n        curl_close($curl);\n\n        if ($errno !== 0) {\n            throw new RuntimeException(\"Transport error: {$errno}\");\n        }\n\n        return json_decode($response, true) ?? &#91;];\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js: WhatsApp media message with a dynamic button<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import { setTimeout as delay } from 'node:timers\/promises';\n\nconst WA_URL = 'https:\/\/unify.smsgateway.center\/WAApi\/send';\n\nasync function sendWhatsAppMedia({\n  userid,\n  password,\n  mobile,\n  wabaNumber,\n  templateName,\n  msg,\n  mediaUrl,\n  mediaType = 'image',\n  buttonUrlPath,\n}) {\n  const body = new URLSearchParams({\n    userid,\n    password,\n    sendMethod: 'quick',\n    msgType: 'media',\n    mobile,\n    wabaNumber,\n    templateName,\n    msg,\n    mediaUrl,\n    mediaType,\n    output: 'json',\n  });\n\n  if (buttonUrlPath) body.append('buttonUrlPath', buttonUrlPath);\n\n  const res = await fetch(WA_URL, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n    body,\n  });\n\n  if (!res.ok) {\n    throw new Error(`HTTP ${res.status}`);\n  }\n\n  return res.json();\n}\n\n\/\/ Example: product image with a tappable deep link on the CTA button.\nconst result = await sendWhatsAppMedia({\n  userid: process.env.SGC_USER,\n  password: process.env.SGC_PASS,\n  mobile: '919xxxxxxxxx6',\n  wabaNumber: '9170396xxxxx',\n  templateName: 'demo_order',\n  msg: 'Your order no. {#var#} has been dispatched. Expected to arrive within {#var#} day. Click to view status {#var#}',\n  mediaUrl: 'https:\/\/cdn.example.in\/products\/88214.jpg',\n  mediaType: 'image',\n  buttonUrlPath: '\/track\/88214',\n});\n\nconsole.log(result);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>mediaUrl<\/code>, <code>mediaType<\/code>, and <code>buttonUrlPath<\/code> parameters have no SMS equivalent. This is the concrete shape of the &#8220;rich content&#8221; 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: the router in miniature<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\nimport time\nfrom dataclasses import dataclass\nfrom urllib.parse import urlencode\n\nimport requests\n\nSMS_URL = \"https:\/\/unify.smsgateway.center\/SMSApi\/send\"\nWA_URL = \"https:\/\/unify.smsgateway.center\/WAApi\/send\"\nSESSION_WINDOW_SECONDS = 24 * 60 * 60\n\nHEADERS = {\"Content-Type\": \"application\/x-www-form-urlencoded\"}\n\n\n@dataclass(frozen=True)\nclass MessageSpec:\n    key: str                 # e.g. \"auth.otp\" or \"order.dispatched\"\n    critical: bool           # non-arrival breaks a user flow\n    rich: bool               # benefits from media or buttons\n    body: str\n\n\ndef _post(url: str, fields: dict) -&gt; dict:\n    resp = requests.post(url, data=urlencode(fields), headers=HEADERS, timeout=20)\n    resp.raise_for_status()\n    return resp.json()\n\n\ndef send_sms(mobile: str, spec: MessageSpec, senderid: str,\n             dlt_entity_id: str, dlt_template_id: str) -&gt; dict:\n    return _post(SMS_URL, {\n        \"userid\": os.environ&#91;\"SGC_USER\"],\n        \"password\": os.environ&#91;\"SGC_PASS\"],\n        \"sendMethod\": \"quick\",\n        \"msgType\": \"text\",\n        \"mobile\": mobile,\n        \"senderid\": senderid,\n        \"msg\": spec.body,\n        \"dltEntityId\": dlt_entity_id,\n        \"dltTemplateId\": dlt_template_id,\n        \"duplicatecheck\": \"true\",\n        \"output\": \"json\",\n    })\n\n\ndef send_whatsapp(mobile: str, spec: MessageSpec, waba_number: str,\n                  template_name: str | None) -&gt; dict:\n    fields = {\n        \"userid\": os.environ&#91;\"SGC_USER\"],\n        \"password\": os.environ&#91;\"SGC_PASS\"],\n        \"sendMethod\": \"quick\",\n        \"msgType\": \"text\",\n        \"mobile\": mobile,\n        \"wabaNumber\": waba_number,\n        \"msg\": spec.body,\n        \"output\": \"json\",\n    }\n    # Omitting templateName makes this a session send.\n    if template_name:\n        fields&#91;\"templateName\"] = template_name\n    return _post(WA_URL, fields)\n\n\ndef session_open(last_inbound_ts: float | None) -&gt; bool:\n    if last_inbound_ts is None:\n        return False\n    return (time.time() - last_inbound_ts) &lt; SESSION_WINDOW_SECONDS\n\n\ndef route(spec: MessageSpec, mobile: str, last_inbound_ts: float | None,\n          registry: dict) -&gt; str:\n    \"\"\"Return the channel to attempt first.\"\"\"\n    if spec.critical:\n        return \"sms\"\n    entry = registry.get(spec.key, {})\n    wa = entry.get(\"whatsapp\", {})\n    if wa.get(\"status\") != \"approved\":\n        return \"sms\"\n    if spec.rich or session_open(last_inbound_ts):\n        return \"whatsapp\"\n    return \"sms\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Java: HttpClient send<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic final class GatewayClient {\n\n    private static final String SMS_URL = \"https:\/\/unify.smsgateway.center\/SMSApi\/send\";\n    private static final String WA_URL  = \"https:\/\/unify.smsgateway.center\/WAApi\/send\";\n\n    private final HttpClient http = HttpClient.newBuilder()\n            .connectTimeout(Duration.ofSeconds(10))\n            .build();\n\n    public String send(String url, Map&lt;String, String&gt; fields) throws Exception {\n        String form = fields.entrySet().stream()\n                .map(e -&gt; URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)\n                        + \"=\" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))\n                .collect(Collectors.joining(\"&amp;\"));\n\n        HttpRequest req = HttpRequest.newBuilder()\n                .uri(URI.create(url))\n                .timeout(Duration.ofSeconds(20))\n                .header(\"Content-Type\", \"application\/x-www-form-urlencoded\")\n                .POST(HttpRequest.BodyPublishers.ofString(form))\n                .build();\n\n        HttpResponse&lt;String&gt; res = http.send(req, HttpResponse.BodyHandlers.ofString());\n        return res.body();\n    }\n\n    public String sendWhatsAppTemplate(String mobile, String msg) throws Exception {\n        return send(WA_URL, Map.of(\n                \"userid\", System.getenv(\"SGC_USER\"),\n                \"password\", System.getenv(\"SGC_PASS\"),\n                \"sendMethod\", \"quick\",\n                \"msgType\", \"text\",\n                \"mobile\", mobile,\n                \"wabaNumber\", \"9170396xxxxx\",\n                \"templateName\", \"demo_order\",\n                \"msg\", msg,\n                \"output\", \"json\"\n        ));\n    }\n\n    public String sendSms(String mobile, String msg) throws Exception {\n        return send(SMS_URL, Map.of(\n                \"userid\", System.getenv(\"SGC_USER\"),\n                \"password\", System.getenv(\"SGC_PASS\"),\n                \"sendMethod\", \"quick\",\n                \"msgType\", \"text\",\n                \"mobile\", mobile,\n                \"senderid\", \"SMSGWC\",\n                \"msg\", msg,\n                \"dltEntityId\", \"1101xxxxxxxxxxxxxx\",\n                \"dltTemplateId\", \"1207xxxxxxxxxxxxxx\",\n                \"output\", \"json\"\n        ));\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Postman<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Import both collections rather than hand-building requests. The SMS collection is at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/download-sms-postman\/\">Download SMS API Postman Collection<\/a> and the WhatsApp collection is at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/download-whatsapp-postman\/\">Download WhatsApp Business API Postman Collection<\/a>. Set <code>userid<\/code>, <code>password<\/code>, <code>wabaNumber<\/code>, and <code>senderid<\/code> as environment variables so you can flip between a sandbox and production account without editing each request.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Building a Channel Router in Practice<\/h2>\n\n\n\n<a id=\"building-a-channel-router-in-practice\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">Everything above collapses into one function that takes a message and returns a channel. Keep it out of your feature code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 1: Classify the message.<\/strong> Attach two boolean properties to every message key at definition time: <code>critical<\/code> (non-arrival breaks a user flow) and <code>rich<\/code> (media or buttons materially improve the outcome). These are product decisions, not runtime ones.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 2: Check for hard overrides.<\/strong> Critical messages go to SMS unconditionally. Do not let engagement metrics override this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 3: Check template availability.<\/strong> If the WhatsApp template for this message key is not in an approved state, the WhatsApp path does not exist. Fall through to SMS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 4: Check session state.<\/strong> Look up <code>last_inbound<\/code> for the recipient. An open window permits free-form sends and is a strong signal the user is an active WhatsApp user.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 5: Check recipient preference and opt-in.<\/strong> A stored channel preference beats all heuristics. If the user has explicitly asked for SMS, honour it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 6: Send, and record the attempt against your idempotency key.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 7: Evaluate the delivery outcome and decide whether the fallback rule fires.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>route()<\/code> 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Fallback Orchestration: SMS as the Floor, Not the Alternative<\/h2>\n\n\n\n<a id=\"fallback-orchestration-sms-as-the-floor-not-the-alternative\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">The naive fallback is &#8220;if WhatsApp fails, send SMS.&#8221; That is wrong, and it is expensive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">Multi-Channel Fallback<\/a>, and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-are-trigger-error-codes-in-multi-channel-fallback\/\">trigger error codes knowledge base entry<\/a> describes the semantics:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">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.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">That default is the permissive one. Narrowing it is almost always the right move for a production system.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/fallback-orchestration.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"480\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/fallback-orchestration.webp\" alt=\"Illustration about Fallback Orchestration: SMS as the Floor, Not the Alternative\" class=\"wp-image-2538\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/fallback-orchestration.webp 800w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/fallback-orchestration-300x180.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/fallback-orchestration-768x461.webp 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A workable classification of outcomes into fallback decisions:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Outcome class<\/th><th>Example<\/th><th>Fallback?<\/th><th>Retry same channel?<\/th><\/tr><\/thead><tbody><tr><td>Recipient not reachable on this channel<\/td><td>Number not registered on WhatsApp<\/td><td>Yes, immediately<\/td><td>No<\/td><\/tr><tr><td>Content rejected by the channel<\/td><td>Template rejected or mismatched<\/td><td>Yes, if the other channel&#8217;s template is valid<\/td><td>No<\/td><\/tr><tr><td>Transient gateway or network error<\/td><td>Timeout, 5xx<\/td><td>No<\/td><td>Yes, with backoff<\/td><\/tr><tr><td>Rate limited<\/td><td>Throttled by the platform<\/td><td>No<\/td><td>Yes, after the window<\/td><\/tr><tr><td>Permanently blocked recipient<\/td><td>Blacklisted, opted out<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Delivered but unread<\/td><td><code>sent<\/code> with no <code>delivered<\/code> after N hours<\/td><td>Only for non-critical messages, and only once<\/td><td>No<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>sent<\/code> for six hours probably warrants an SMS. A promotional push does not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two rules that prevent most fallback incidents:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Fallback fires at most once per message.<\/strong> Record a <code>fallback_used<\/code> flag against the idempotency key. Never chain.<\/li>\n\n\n\n<li><strong>The final channel in the chain never falls back.<\/strong> This is why SMS belongs at the end, not the beginning, of any chain that includes it.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">For the retry mechanics underneath this (backoff, jitter, dead-lettering, and per-code retry eligibility) the deeper treatment is in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-retry-strategy-handling-failed-messages\/\">SMS API Retry Strategy: How to Handle Failed Message Delivery<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Idempotency Across Two Channels<\/h2>\n\n\n\n<a id=\"idempotency-across-two-channels\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">Single-channel idempotency is straightforward: generate a key, deduplicate on it, and use the gateway&#8217;s <code>duplicatecheck<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use a two-level key:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>logical_key   = sha256(tenant_id + message_key + recipient + business_event_id)\nphysical_key  = logical_key + \":\" + channel + \":\" + attempt_number\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Deduplicate sends on <code>physical_key<\/code>. Deduplicate the <em>user-visible outcome<\/em> on <code>logical_key<\/code>. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>delivered<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The gateway-side <code>duplicatecheck=true<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Register a webhook to receive delivery events in near real time rather than polling, using <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/webhook\/create<\/code> as documented in the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/create-webhook\/\">Create Webhook API<\/a>. Polling the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-delivery-report\/\">delivery report endpoint<\/a> on a schedule is a reasonable reconciliation backstop but a poor primary mechanism, because the polling interval directly becomes your fallback latency floor.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Decision Matrix by Message Class<\/h2>\n\n\n\n<a id=\"the-decision-matrix-by-message-class\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\">This is the table to copy into your own routing configuration and adapt.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Message class<\/th><th>Primary channel<\/th><th>Fallback<\/th><th>Reasoning<\/th><\/tr><\/thead><tbody><tr><td>Login OTP \/ 2FA<\/td><td>SMS<\/td><td>None needed<\/td><td>Reach is the entire requirement. A WhatsApp OTP that arrives after the code expires is worse than useless. See <a href=\"https:\/\/www.smsgatewaycenter.com\/otp-sms-india\/\">OTP SMS<\/a><\/td><\/tr><tr><td>Password reset<\/td><td>SMS<\/td><td>None needed<\/td><td>Same as above, and often the user is locked out of the device the app is on<\/td><\/tr><tr><td>Payment or fraud alert<\/td><td>SMS<\/td><td>None needed<\/td><td>Regulatory expectation plus time criticality<\/td><\/tr><tr><td>Order confirmation<\/td><td>WhatsApp template<\/td><td>SMS on unreachable<\/td><td>Rich receipt with an image performs better, but the user must get it<\/td><\/tr><tr><td>Shipment dispatched<\/td><td>WhatsApp template with media and button<\/td><td>SMS on unreachable<\/td><td>The tracking button is the whole value<\/td><\/tr><tr><td>Delivery attempt failed<\/td><td>SMS<\/td><td>None<\/td><td>Time-critical, the courier is at the door<\/td><\/tr><tr><td>Appointment reminder<\/td><td>WhatsApp template<\/td><td>SMS at T minus 2 hours if undelivered<\/td><td>Rich reminder with a reschedule button, SMS as the safety net<\/td><\/tr><tr><td>Cart abandonment<\/td><td>WhatsApp template<\/td><td>None<\/td><td>Non-critical, do not pay twice for a marketing nudge<\/td><\/tr><tr><td>Promotional campaign<\/td><td>WhatsApp template<\/td><td>None<\/td><td>Non-critical, and SMS promotional sends carry their own DND and regulatory overhead<\/td><\/tr><tr><td>Customer support reply<\/td><td>WhatsApp session message<\/td><td>None<\/td><td>The window is open by definition, since the user just messaged you<\/td><\/tr><tr><td>Survey \/ NPS<\/td><td>WhatsApp template with buttons<\/td><td>None<\/td><td>Buttons collapse the response friction<\/td><\/tr><tr><td>Service outage notice<\/td><td>SMS<\/td><td>WhatsApp as a supplement, not a fallback<\/td><td>Broadcast reach matters, and outages often mean users are already having connectivity trouble<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The pattern in that table: <strong>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.<\/strong> Latency tolerance is a better discriminator than message content.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Pros and Cons<\/h2>\n\n\n\n<a id=\"pros-and-cons\"><\/a>\n\n\n\n<h3 class=\"wp-block-heading\">SMS API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pros<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Universal reach with no app, no account, and no data connection required<\/li>\n\n\n\n<li>Works during degraded connectivity, which is exactly when outage and fraud alerts matter most<\/li>\n\n\n\n<li>Delivery codes are operator-sourced and actionable<\/li>\n\n\n\n<li>Simple stateless integration, no session tracking<\/li>\n\n\n\n<li>Well-understood regulatory framework in India via DLT<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cons<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>No rich content, no buttons, no read receipts<\/li>\n\n\n\n<li>Segment-based billing punishes long messages and Unicode<\/li>\n\n\n\n<li>Sender identity is a six-character header with limited brand presence in India<\/li>\n\n\n\n<li>Two-way requires separate infrastructure (long code or virtual number)<\/li>\n\n\n\n<li>Link-based CTAs suffer from link truncation and phishing wariness<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">WhatsApp Business API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pros<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Rich media, headers, footers, and interactive buttons in a single message<\/li>\n\n\n\n<li>Native two-way conversation with no extra number to provision<\/li>\n\n\n\n<li>Read receipts give real engagement signal<\/li>\n\n\n\n<li>Verified business identity rather than a six-character string<\/li>\n\n\n\n<li>Long message bodies without per-segment cost<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cons<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Requires the app installed, registered, and connected<\/li>\n\n\n\n<li>Stateful: the 24-hour window has to be tracked and enforced in your code<\/li>\n\n\n\n<li>Template approval adds a second, non-substitutable governance queue<\/li>\n\n\n\n<li>No reliable timeout signal, which disqualifies it for time-critical flows<\/li>\n\n\n\n<li>A rejected or paused template silently removes an entire message class from the channel<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Migration Anti-Patterns<\/h2>\n\n\n\n<a id=\"migration-anti-patterns\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Moving OTP to WhatsApp to save cost.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Treating WhatsApp as an SMS replacement rather than an addition.<\/strong> Every team that decommissions SMS entirely rebuilds it within a year, usually after an incident. Keep the SMS path warm even at low volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Building channel selection into feature code.<\/strong> Fifteen services each with their own <code>if (user.prefersWhatsApp)<\/code> branch is fifteen places to change when the policy changes. One router, called by all of them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Skipping inbound webhook processing on WhatsApp.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One template registry per channel.<\/strong> Two registries drift. One canonical definition with two external identifiers attached does not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Falling back on every failure.<\/strong> Duplicate messages, double cost, and user annoyance. Narrow the trigger set to codes that genuinely mean &#8220;this channel cannot deliver this message.&#8221;<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<a id=\"faqs\"><\/a>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: Can I send OTPs over WhatsApp Business API?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: What exactly is the 24-hour window and when does it reset?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: Do I need DLT registration if I only use WhatsApp?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: Can the same message body be used for both channels?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You should keep one canonical body and derive both from it, but the registered versions will not be byte-identical. DLT uses <code>{#var#}<\/code> 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 <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/validateTemplate\/\">validateTemplate endpoint<\/a> so drift surfaces in CI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: How do I know whether a number is on WhatsApp before sending?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: Should fallback go WhatsApp to SMS, or SMS to WhatsApp?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: How is billing different between the two channels?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SMS is billed per segment, so a 300-character Unicode message costs several times a short <a href=\"https:\/\/www.gsma.com\/\">GSM<\/a>-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 <a href=\"https:\/\/www.smsgatewaycenter.com\/pricing\/\">pricing page<\/a> or contact the team, since rates vary by destination and volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: What happens if my WhatsApp template gets rejected after it was approved?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Q: Can I use RCS instead of choosing between these two?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-messaging\/\">RCS Messaging<\/a> for the current picture.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Run both channels from one account.<\/strong> 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 <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-getting-started-guide\/\">SMS API getting started guide<\/a>, pull both Postman collections, and have a working dual-channel send in an afternoon. <a href=\"https:\/\/unify.smsgateway.center\/signup\/\">Sign up<\/a> or <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">talk to the team<\/a> about routing and fallback configuration for your message classes.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>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.<\/p>\n","protected":false},"author":118,"featured_media":2537,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[2016,404,1440,2031,2028,2030,481,27,445,2029,706,632],"class_list":["post-2536","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-delivery-reports","tag-dlt","tag-message-routing","tag-messaging-architecture","tag-multi-channel-fallback","tag-otp","tag-sms-api","tag-sms-gateway-center","tag-smsgatewaycenter","tag-template-approval","tag-waba","tag-whatsapp-business-api"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2536","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/users\/118"}],"replies":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/comments?post=2536"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2536\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2537"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2536"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2536"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2536"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}