{"id":3035,"date":"2026-09-23T12:06:55","date_gmt":"2026-09-23T06:36:55","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=3035"},"modified":"2026-09-23T12:09:36","modified_gmt":"2026-09-23T06:39:36","slug":"message-template-management-four-channels","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/message-template-management-four-channels\/","title":{"rendered":"Message Template Management Across SMS, RCS, WhatsApp and Telegram: One API Comparison"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Four channels, five placeholder spellings, four identity models and three approval shapes. A field-by-field comparison of the SMS DLT, RCS, WhatsApp and Telegram template endpoints, with a portable registry schema, safe rendering code in four languages and a change workflow that survives approval delays.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels-1024x584.webp\" alt=\"Four translucent template panels in teal, blue and orange, each with a different bracket style, connected by lines to one shared key on a rail below.\" class=\"wp-image-3036\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/message-template-management-four-channels.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Four channels, four template models, one stable key of your own that maps to all of them<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Table of Contents<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#the-short-answer\">The Short Answer<\/a><\/li>\n\n\n\n<li><a href=\"#tldr\">TL;DR<\/a><\/li>\n\n\n\n<li><a href=\"#at-a-glance\">Four Template Models at a Glance<\/a><\/li>\n\n\n\n<li><a href=\"#what-a-template-is\">What a Template Actually Is on Each Channel<\/a><\/li>\n\n\n\n<li><a href=\"#placeholder-syntax\">Placeholder Syntax: Five Spellings, No Shared Code<\/a><\/li>\n\n\n\n<li><a href=\"#who-renders\">Who Renders the Variables<\/a><\/li>\n\n\n\n<li><a href=\"#sms-lifecycle\">The SMS Template Lifecycle Over the API<\/a><\/li>\n\n\n\n<li><a href=\"#dlt-variable-tags\">DLT Variable Tags and What They Change in Your Code<\/a><\/li>\n\n\n\n<li><a href=\"#validating-sms\">Validating an SMS Body Before You Send<\/a><\/li>\n\n\n\n<li><a href=\"#rcs-lifecycle\">The RCS Template Lifecycle<\/a><\/li>\n\n\n\n<li><a href=\"#whatsapp-lifecycle\">The WhatsApp Template Lifecycle<\/a><\/li>\n\n\n\n<li><a href=\"#telegram-templates\">The Telegram Template Model<\/a><\/li>\n\n\n\n<li><a href=\"#template-identity\">Identity: Why an Edit Is a New Template<\/a><\/li>\n\n\n\n<li><a href=\"#approval-state\">Approval State: A String, an Integer, Two Fields or Nothing<\/a><\/li>\n\n\n\n<li><a href=\"#envelopes\">Response Envelopes Across the Template Endpoints<\/a><\/li>\n\n\n\n<li><a href=\"#registry-schema\">A Portable Template Registry Schema<\/a><\/li>\n\n\n\n<li><a href=\"#rendering-code\">Rendering Safely in Four Languages<\/a><\/li>\n\n\n\n<li><a href=\"#change-workflow\">The Template Change Workflow<\/a><\/li>\n\n\n\n<li><a href=\"#ten-mistakes\">Ten Template Mistakes That Reach Production<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">Decision Matrix<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Pre-Launch Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#unspecified-behaviour\">Unspecified Behaviour and How to Code Around It<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<h2 id=\"the-short-answer\" class=\"wp-block-heading\">The Short Answer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You cannot manage SMS, RCS, WhatsApp and Telegram templates with one piece of code, because the four channels disagree on every property that matters. They use five different placeholder spellings (DLT typed tags such as <code>{#numeric#}<\/code>, the legacy DLT <code>{#var#}<\/code>, RCS <code>[name]<\/code>, WhatsApp <code>{{1}}<\/code> and Telegram <code>{name}<\/code>). They identify a template four different ways (a local <code>mtId<\/code> plus a regulator <code>dltTemplateId<\/code>, an RCS <code>name<\/code>, a WhatsApp name plus language pair, and a Telegram integer <code>templateId<\/code>). They report approval as a string, an integer, two independent string fields, or not at all. And only SMS has an update endpoint; on RCS and WhatsApp an edit is a delete and a recreate, and on SMS a re-tagged DLT template comes back with a new DLT Template ID anyway.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The design that works on all four is simple to state: key your own system on a stable template identifier you own, store every provider identity as a versioned row beneath it, render variables with a tokenizer rather than a regex replace, gate sending on an approval state you normalise yourself, and never retire an old version until every scheduled send that references it has drained.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Five placeholder syntaxes.<\/strong> SMS DLT typed tags (<code>{#numeric#}<\/code>, <code>{#alphanumeric#}<\/code>, <code>{#url#}<\/code>, <code>{#urlott#}<\/code>, <code>{#cbn#}<\/code>, <code>{#email#}<\/code>) replace the generic <code>{#var#}<\/code> under the TRAI direction of 18 November 2025. RCS uses <code>[name]<\/code>, WhatsApp <code>{{1}}<\/code>, Telegram <code>{name}<\/code>. A substitution routine written for one channel silently no-ops on the other three.<\/li>\n\n\n\n<li><strong>Two rendering models.<\/strong> On SMS, WhatsApp and Telegram you send the finished text. On RCS you send the template name plus a <code>customParams<\/code> JSON string and the platform renders.<\/li>\n\n\n\n<li><strong>Only SMS has an update call.<\/strong> <code>SMSApi\/template\/update<\/code> takes an <code>id<\/code> and re-enters approval. RCS and WhatsApp have create, read and delete only. Telegram templates are read-only over the API.<\/li>\n\n\n\n<li><strong>Create calls do not return the identifier you need next.<\/strong> <code>SMSApi\/template\/create<\/code> returns no <code>mtId<\/code>. RCS and WhatsApp create calls return &#8220;Accepted for processing.&#8221;, which means queued, not approved. Always read back after a write.<\/li>\n\n\n\n<li><strong>Names change on the way in.<\/strong> WhatsApp lowercases a submitted template name; RCS returns categories in a different case from the one submitted. Never key a local table on the string you sent.<\/li>\n\n\n\n<li><strong>Approval is four different shapes.<\/strong> SMS <code>status<\/code> is a string such as <code>\"ENABLED\"<\/code>. RCS <code>status<\/code> is an integer. WhatsApp has <code>whatsAppStatus<\/code> and <code>systemStatus<\/code>, and both must clear. Telegram has no approval field.<\/li>\n\n\n\n<li><strong>Deletes differ in timing.<\/strong> SMS template delete is synchronous. WhatsApp template delete is asynchronous (&#8220;Accepted for processing.&#8221;). Re-list after every delete.<\/li>\n\n\n\n<li><strong>Error payloads differ.<\/strong> The Telegram <code>get<\/code> action flips its <code>template<\/code> object to an empty array on error; <code>validateTemplate<\/code> drops every key except <code>status<\/code>, <code>statusCode<\/code> and <code>reason<\/code>. Parse loosely, branch on the top-level <code>status<\/code> string, bind types only on success.<\/li>\n\n\n\n<li><strong>Render with a tokenizer.<\/strong> <code>preg_replace<\/code>, <code>String.prototype.replace<\/code>, <code>re.sub<\/code> and <code>Matcher.replaceAll<\/code> all interpret special characters in the replacement value. A value of <code>Rs $100<\/code> can reach the handset as <code>Rs 0<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"at-a-glance\" class=\"wp-block-heading\">Four Template Models at a Glance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The table below is the whole comparison on one screen. Every row is expanded in its own section further down.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Property<\/th><th>SMS (DLT)<\/th><th>RCS<\/th><th>WhatsApp<\/th><th>Telegram<\/th><\/tr><\/thead><tbody><tr><td>Base path<\/td><td><code>SMSApi\/template\/<\/code><\/td><td><code>RCSApi\/template\/<\/code><\/td><td><code>WAApi\/template<\/code><\/td><td><code>rest\/tg\/v1\/templates<\/code><\/td><\/tr><tr><td>Operations over the API<\/td><td>create, read, update, delete, plus <code>SMSApi\/validateTemplate<\/code><\/td><td>create, list, delete<\/td><td>create, read (list and single), delete<\/td><td>list, get<\/td><\/tr><tr><td>Operation selector<\/td><td>URL path<\/td><td>URL path<\/td><td>HTTP method on one URL<\/td><td><code>action<\/code> query parameter<\/td><\/tr><tr><td>Placeholder<\/td><td><code>{#numeric#}<\/code> and five other typed tags; legacy <code>{#var#}<\/code><\/td><td><code>[name]<\/code>, <code>[otp]<\/code>, <code>[custom_param_1]<\/code><\/td><td><code>{{1}}<\/code>, <code>{{2}}<\/code><\/td><td><code>{name}<\/code>, <code>{orderId}<\/code><\/td><\/tr><tr><td>Who renders<\/td><td>You<\/td><td>The platform, from <code>customParams<\/code><\/td><td>You, into <code>msg<\/code><\/td><td>You, into <code>text<\/code><\/td><\/tr><tr><td>Local identity<\/td><td><code>mtId<\/code> (quoted string)<\/td><td><code>name<\/code><\/td><td><code>templateName<\/code> plus <code>language<\/code><\/td><td><code>templateId<\/code> (unquoted integer)<\/td><\/tr><tr><td>External identity<\/td><td><code>dltTemplateId<\/code> (regulator)<\/td><td>none separate<\/td><td>none separate<\/td><td>none<\/td><\/tr><tr><td>Update endpoint<\/td><td>Yes, by <code>id<\/code>, re-enters approval<\/td><td>No<\/td><td>No<\/td><td>No (portal only)<\/td><\/tr><tr><td>Approval field<\/td><td><code>status<\/code>, string, e.g. <code>\"ENABLED\"<\/code><\/td><td><code>status<\/code>, integer<\/td><td><code>whatsAppStatus<\/code> and <code>systemStatus<\/code><\/td><td>none<\/td><\/tr><tr><td>Create returns the id<\/td><td>No<\/td><td>Name echoed as <code>templateName<\/code><\/td><td>No<\/td><td>Not creatable over the API<\/td><\/tr><tr><td>Delete timing<\/td><td>Synchronous<\/td><td>Not stated<\/td><td>Asynchronous<\/td><td>Not deletable over the API<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><\/blockquote>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-template-models-four-channels.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-template-models-four-channels.svg\" alt=\"Four-column comparison of SMS DLT, RCS, WhatsApp and Telegram template models showing placeholder syntax, who renders variables, identity, approval field and whether an update endpoint exists.\" class=\"wp-image-3038\"\/><\/a><figcaption class=\"wp-element-caption\">The four template models side by side. Only one row, &#8220;you own the stable key&#8221;, is the same on every channel, and that row is the one you add yourself.<\/figcaption><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"what-a-template-is\" class=\"wp-block-heading\">What a Template Actually Is on Each Channel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The word &#8220;template&#8221; names four different objects on this platform, and treating them as one is the root of most template bugs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On SMS in India, a template is a regulatory contract.<\/strong> The Principal Entity registers a content template on an operator DLT portal and receives a DLT Template ID. Every message sent under that ID must match the registered text character for character, with variables only where the template allows them. The platform&#8217;s own <code>SMSApi\/template\/<\/code> family stores a local copy of that body, with a small local identifier <code>mtId<\/code>, the DLT ID, the template type (for example <code>\"Service Implicit\"<\/code>) and the sender IDs it may be used with. The template does not render anything. It is a pattern your finished message must match. See the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-template-rejected-reasons-and-fixes\/\">DLT template rejection guide<\/a> for the business side of registration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On RCS, a template is a stored, rendered message.<\/strong> You create it against a bot with a JSON <code>templateData<\/code> payload (name, type, category, content and suggestions). At send time you pass the template name as <code>templateCode<\/code> and the variable values as <code>customParams<\/code>, and the platform builds the card. Three types exist: <code>text_message<\/code>, <code>rich_card<\/code> and <code>carousel<\/code>. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/rcs-messaging-api-reference-migration-from-sms\/\">RCS API reference<\/a> covers the send side in full.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On WhatsApp, a template is a Meta-approved message definition scoped to a business number.<\/strong> It has a category (<code>UTILITY<\/code>, <code>MARKETING<\/code> or <code>AUTHENTICATION<\/code>), a language, a body with positional <code>{{1}}<\/code> variables, optional header and footer, and optional buttons. Meta approves it; the platform also tracks its own approval state. Outside the 24-hour customer service window, a template is the only way to start a conversation. Meta&#8217;s own <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/templates\/overview\" target=\"_blank\" rel=\"noopener nofollow\">template overview<\/a> describes the category rules.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On Telegram, a template is a saved compose form.<\/strong> It is a stored body with <code>{name}<\/code> style fields plus optional media, button JSON and a reply-markup flag. There is no approval, no regulator and no provider-side rendering. The templates endpoint exists so your application can load a saved compose payload and prefill it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The practical consequence: on SMS the template constrains what you send, on RCS it is what you send, on WhatsApp it is both, and on Telegram it is a convenience. Any abstraction that hides these differences behind one <code>sendTemplate(name, vars)<\/code> call will be wrong on at least one channel.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"placeholder-syntax\" class=\"wp-block-heading\">Placeholder Syntax: Five Spellings, No Shared Code<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Channel<\/th><th>Placeholder form<\/th><th>Named or positional<\/th><th>Where it may appear<\/th><th>Source of truth<\/th><\/tr><\/thead><tbody><tr><td>SMS DLT (current)<\/td><td><code>{#numeric#}<\/code>, <code>{#alphanumeric#}<\/code>, <code>{#url#}<\/code>, <code>{#urlott#}<\/code>, <code>{#cbn#}<\/code>, <code>{#email#}<\/code><\/td><td>Typed, positional<\/td><td>Body only<\/td><td>The registered DLT template<\/td><\/tr><tr><td>SMS DLT (legacy)<\/td><td><code>{#var#}<\/code><\/td><td>Untyped, positional<\/td><td>Body only<\/td><td>The registered DLT template<\/td><\/tr><tr><td>RCS<\/td><td><code>[name]<\/code>, <code>[otp]<\/code>, <code>[orderid]<\/code>, <code>[amount]<\/code>, <code>[date]<\/code>, <code>[tracking_id]<\/code>, <code>[custom_param_1]<\/code><\/td><td>Named<\/td><td>Text, card titles, card descriptions, suggestion text, postbacks, URLs<\/td><td><code>RCSApi\/template\/list<\/code> row <code>data<\/code><\/td><\/tr><tr><td>WhatsApp<\/td><td><code>{{1}}<\/code>, <code>{{2}}<\/code><\/td><td>Positional<\/td><td>Body; header allows at most one variable; footer allows none<\/td><td><code>WAApi\/template<\/code> read row<\/td><\/tr><tr><td>Telegram<\/td><td><code>{name}<\/code>, <code>{orderId}<\/code><\/td><td>Named<\/td><td>Body (and caption for media types)<\/td><td><code>rest\/tg\/v1\/templates?action=get<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Three things in that table cause production incidents.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The DLT tags are typed now, and the type is enforced against your data.<\/strong> Under the TRAI direction on variable tagging of 18 November 2025, each variable in a DLT content template must declare what it holds. Operators are phasing in enforcement, first logging mismatches and then rejecting them. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-are-dlt-variable-tags-and-which-one-should-i-use\/\">DLT variable tags guide<\/a> lists the six tags and their limits. From a developer&#8217;s point of view, the key change is that the placeholder now carries a validation rule, so your renderer has to validate values, not just substitute them. That is covered in its own section below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>RCS names are bare in <code>customParams<\/code>.<\/strong> The template text says <code>[name]<\/code>; the send payload&#8217;s <code>customParams<\/code> object is keyed on names without brackets. Code that passes <code>{\"[name]\": \"Asha\"}<\/code> sends a card with a literal <code>[name]<\/code> in it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>WhatsApp positions are one-based and dense.<\/strong> <code>{{1}}<\/code> is the first variable. There is no <code>{{0}}<\/code>. A renderer that builds an array and indexes from zero shifts every value by one position, and because every value is still a plausible string, the message looks fine in a quick test until a name lands where an order number should be.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A regex that matches <code>\\{[^}]+\\}<\/code> catches Telegram fields, WhatsApp double braces (as nested matches) and DLT tags all at once, and treats them as the same thing. Write one pattern per channel and anchor it exactly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\n\nPLACEHOLDER = {\n    \"sms_dlt\": re.compile(r\"\\{#(numeric|alphanumeric|url|urlott|cbn|email|var)#\\}\"),\n    \"rcs\": re.compile(r\"\\&#91;(&#91;A-Za-z0-9_]+)\\]\"),\n    \"whatsapp\": re.compile(r\"\\{\\{(&#91;1-9]&#91;0-9]*)\\}\\}\"),\n    \"telegram\": re.compile(r\"\\{(&#91;A-Za-z_]&#91;A-Za-z0-9_]*)\\}\"),\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note that the Telegram pattern also matches the inner part of a WhatsApp <code>{{1}}<\/code> if applied to WhatsApp text, and it would match <code>{#var#}<\/code> if the hash characters were allowed. That is exactly why the patterns must be chosen by channel before parsing, never inferred from the text.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"who-renders\" class=\"wp-block-heading\">Who Renders the Variables<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the difference that decides the shape of your send path.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Channel<\/th><th>What you send<\/th><th>Where the values go<\/th><th>What the platform checks<\/th><\/tr><\/thead><tbody><tr><td>SMS<\/td><td>The finished text in <code>msg<\/code>, plus <code>dltTemplateId<\/code> and <code>dltEntityId<\/code><\/td><td>Already substituted into <code>msg<\/code><\/td><td>That <code>msg<\/code> matches the registered template for that ID and sender<\/td><\/tr><tr><td>RCS<\/td><td><code>templateCode<\/code> (the template <code>name<\/code>) plus <code>customParams<\/code><\/td><td>A JSON string inside the <code>msg<\/code> JSON string<\/td><td>Renders the stored template<\/td><\/tr><tr><td>WhatsApp<\/td><td><code>templateName<\/code> plus the text in <code>msg<\/code>, with optional <code>header<\/code> and <code>footer<\/code><\/td><td>Substituted into <code>msg<\/code> by you<\/td><td>Sends under the named template<\/td><\/tr><tr><td>Telegram<\/td><td>The finished text in <code>text<\/code><\/td><td>Already substituted<\/td><td>Nothing template-related<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On SMS, rendering is your responsibility and the platform is the judge.<\/strong> You fill the template and send the result. If the result does not match the registered DLT template for the <code>dltTemplateId<\/code> and sender ID you supplied, it is rejected. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/otp-delivery-india-reliability-checklist\/\">OTP delivery checklist<\/a> walks through how a trailing space or a changed full stop becomes a template mismatch in practice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On RCS, rendering is the platform&#8217;s job and your job is the payload.<\/strong> The send body is a JSON string in the <code>msg<\/code> form field. Inside it, <code>customParams<\/code> is itself a JSON string, not a nested object. The published example is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"contentMessage\": {\n    \"templateMessage\": {\n      \"templateCode\": \"WELCOME_TEMPLATE\",\n      \"customParams\": \"{\\\"custom_var\\\":\\\"John\\\",\\\"amount\\\":\\\"999\\\"}\"\n    }\n  },\n  \"ttl\": \"86400s\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That is three encoding layers: form encoding around a JSON string, around a JSON string. Build each layer with a real serialiser and never by string concatenation. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-do-i-use-variables-to-personalise-rcs-templates\/\">RCS variables guide<\/a> confirms that API senders pass values as <code>customParams<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On WhatsApp, you render and name.<\/strong> <code>WAApi\/send<\/code> requires both <code>templateName<\/code> and <code>msg<\/code>. The send call has no per-variable parameter, so the finished body goes in <code>msg<\/code>. Send the body with every <code>{{n}}<\/code> replaced, byte for byte identical to the approved body elsewhere. Session messages, sent inside the customer service window, omit <code>templateName<\/code> entirely.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>On Telegram, the template is a prefill.<\/strong> Load the body with <code>action=get<\/code>, substitute <code>{name}<\/code> fields yourself, and send the result as <code>text<\/code> through the send endpoint. Mind the limits: 4096 characters for a text body and 1024 for a media caption. A template that fits at authoring time can overflow once a long value is substituted.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"sms-lifecycle\" class=\"wp-block-heading\">The SMS Template Lifecycle Over the API<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The SMS family is the only one with all four CRUD operations, and it is also the one where the create call gives you the least back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Create.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/create<\/code> with <code>message<\/code> and <code>output<\/code>. That is the whole required parameter set: no name, no DLT ID, no sender ID, no template type. The response carries no identifier:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"response\": {\n        \"api\": \"template\",\n        \"action\": \"create\",\n        \"status\": \"success\",\n        \"msg\": \"Template created successfully and given for approval\",\n        \"code\": \"200\"\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/read<\/code> with <code>output<\/code> only. It returns every template on the account; there is no filter by ID, name or status. The row is double wrapped, with the body three levels deep at <code>response.templateList[i].template.template<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"template\": {\n    \"mtId\": \"39\",\n    \"identifier\": \"bank details without statement\",\n    \"template\": \"{#var#}{#var#}\\nA\/c Name: {#var#}{#var#}\\nA\/c No: {#var#}\\nBranch: {#var#}{#var#}\\nIFSC Code: {#var#}\\nYour Brand\",\n    \"msgType\": \"text\",\n    \"dltTemplateId\": \"10070142034xxxxxxxx\",\n    \"dltTemplateType\": \"Service Implicit\",\n    \"senderIds\": \"SENDER\",\n    \"status\": \"ENABLED\",\n    \"lastUpdated\": \"1687764018447\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every value is a quoted string, including <code>mtId<\/code> and the epoch millisecond <code>lastUpdated<\/code>. <code>senderIds<\/code> is plural in name but a single string in the sample; split it on commas defensively. <code>identifier<\/code> is a human label, not a key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Update.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/update<\/code> with <code>message<\/code>, <code>id<\/code> and <code>output<\/code>. POST only. The response message is &#8220;Template updated successfully and given for approval.&#8221;, so an update sends the template back through approval. Until it clears, do not send against the new body.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Delete.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/delete<\/code> with <code>id<\/code> and <code>output<\/code>. POST only. Returns &#8220;Template deleted successfully.&#8221; with <code>code<\/code> <code>\"200\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because create returns nothing you can key on, the working pattern is create, then read, then find your row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -s -X POST 'https:\/\/unify.smsgateway.center\/SMSApi\/template\/create' \\\n  -H 'apikey: YOUR_API_KEY' \\\n  -H 'Content-Type: application\/x-www-form-urlencoded' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'message=Dear {#alphanumeric#}, your OTP is {#numeric#}. Do not share it. - YourBrand' \\\n  --data-urlencode 'output=json'\n\ncurl -s -X POST 'https:\/\/unify.smsgateway.center\/SMSApi\/template\/read' \\\n  -H 'apikey: YOUR_API_KEY' \\\n  -H 'Content-Type: application\/x-www-form-urlencoded' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'output=json'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then scan <code>templateList<\/code> for the row whose <code>template<\/code> body equals the text you submitted, and if more than one matches, take the one with the highest <code>lastUpdated<\/code>. Store its <code>mtId<\/code> as text. Use the <code>apikey<\/code> header rather than a password parameter on every call; <code>userid<\/code> still travels alongside it because on this platform a key authorises a request but does not identify the account.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The sample request on the create page uses <code>###123###<\/code> as its placeholder, and the page introduction mentions <code>{OTP}<\/code>. Neither is a DLT placeholder. Register bodies using the DLT tag syntax your registered template uses.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"dlt-variable-tags\" class=\"wp-block-heading\">DLT Variable Tags and What They Change in Your Code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The six typed tags turn a DLT placeholder from &#8220;any text goes here&#8221; into &#8220;text of this shape goes here&#8221;. Operators check the value you send against the tag.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Tag<\/th><th>Use it for<\/th><th>Rule your renderer must enforce<\/th><\/tr><\/thead><tbody><tr><td><code>{#numeric#}<\/code><\/td><td>OTPs, amounts, account and order numbers made only of digits<\/td><td>Digits 0 to 9 only; up to 40 characters<\/td><\/tr><tr><td><code>{#alphanumeric#}<\/code><\/td><td>Names, reference IDs, ticket numbers<\/td><td>Letters and numbers; up to 40 characters; no link<\/td><\/tr><tr><td><code>{#url#}<\/code><\/td><td>Website and landing page links<\/td><td>Up to 120 characters; must match a whitelisted URL CTA<\/td><\/tr><tr><td><code>{#urlott#}<\/code><\/td><td>App download and OTT links, such as WhatsApp links<\/td><td>Up to 120 characters; must match a CTA registered under OTT or APK<\/td><\/tr><tr><td><code>{#cbn#}<\/code><\/td><td>Callback numbers<\/td><td>3 to 14 digits; must match a whitelisted number CTA; keep &#8220;+91&#8221; in the fixed text<\/td><\/tr><tr><td><code>{#email#}<\/code><\/td><td>Email addresses<\/td><td>Valid email format; up to 40 characters<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Four consequences for code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Validate every value against its tag before you render.<\/strong> An OTP with a space in it, an amount formatted as <code>1,299.00<\/code> in a <code>{#numeric#}<\/code> slot, or a customer name that happens to contain a URL will all fail. Catch them before the send, where you can return a clear error, not after, where you pay for a rejection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Adjacency is now restricted.<\/strong> Only <code>{#alphanumeric#}<\/code> may appear back to back without fixed text between. Legacy templates are full of <code>{#var#}{#var#}<\/code> runs (the read sample above has five of them) because registrants used adjacent variables to buy extra length. When those templates are re-tagged, adjacent pairs must become adjacent <code>{#alphanumeric#}<\/code> tags or be split by fixed text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Re-tagging produces a new DLT Template ID.<\/strong> The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-do-i-update-my-existing-dlt-templates-to-use-variable-tags\/\">variable tag migration guide<\/a> says the DLT portal issues a new Template ID for the updated template, which must then be updated in the account and in any application that sends through the API. Your code must treat the DLT ID as a versioned attribute of a template, not as its permanent identity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Amount formatting belongs in fixed text.<\/strong> Put the currency marker and any separators in the registered template (&#8220;Rs {#numeric#}&#8221;) and send digits only. If you need paise, register two numeric slots with a fixed full stop between them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal validator:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\n\nTAG_RULES = {\n    \"numeric\":      re.compile(r\"^&#91;0-9]{1,40}$\"),\n    \"alphanumeric\": re.compile(r\"^(?!.*(https?:\/\/|www\\.))&#91;A-Za-z0-9 ]{1,40}$\"),\n    \"email\":        re.compile(r\"^&#91;^@\\s]{1,64}@&#91;^@\\s]+\\.&#91;A-Za-z]{2,}$\"),\n    \"cbn\":          re.compile(r\"^&#91;0-9]{3,14}$\"),\n}\n\ndef check_value(tag: str, value: str) -&gt; None:\n    if tag in (\"url\", \"urlott\"):\n        if len(value) &gt; 120:\n            raise ValueError(f\"{tag} value longer than 120 characters\")\n        return  # whitelist match is checked against your stored CTA list\n    if tag == \"email\" and len(value) &gt; 40:\n        raise ValueError(\"email value longer than 40 characters\")\n    rule = TAG_RULES.get(tag)\n    if rule is None:\n        raise ValueError(f\"unknown DLT tag {tag}\")\n    if not rule.match(value):\n        raise ValueError(f\"value {value!r} does not satisfy {{#{tag}#}}\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Whether <code>{#alphanumeric#}<\/code> accepts spaces and punctuation depends on operator implementation; the rule above allows spaces, which covers names, and rejects links. Tighten or loosen it once you have observed your own rejections in delivery reports.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"validating-sms\" class=\"wp-block-heading\">Validating an SMS Body Before You Send<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SMSApi\/validateTemplate<\/code> checks a message against a registered template without sending it. It is POST only and requires <code>application\/x-www-form-urlencoded<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Parameter<\/th><th>Required<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td><code>senderid<\/code><\/td><td>Yes<\/td><td>Sender ID registered with the template<\/td><\/tr><tr><td><code>msg<\/code><\/td><td>Yes<\/td><td>The message text to check<\/td><\/tr><tr><td><code>dltTemplateId<\/code><\/td><td>Yes<\/td><td>The DLT Template ID to check against<\/td><\/tr><tr><td><code>output<\/code><\/td><td>No<\/td><td><code>json<\/code> recommended<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Success is a flat object, not wrapped in <code>response<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"template\": \"Your Instant Demo account is live  This is a Demo Test message  {#var#} Demo Message SMSGatewayCenter\",\n  \"dltTemplateId\": \"xxxx16194317445xxxx\",\n  \"senderId\": \"SMSGAT\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\",\n  \"text\": \"Your Instant Demo account is live. This is a Demo Test message. {#var#} Demo Message SMSGatewayCenter\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A mismatch returns three keys only:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"error\",\n  \"statusCode\": \"188\",\n  \"reason\": \"message template mismatch\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Five details matter when you wire this in.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>The request says <code>senderid<\/code>; the response says <code>senderId<\/code>.<\/strong> Do not reuse one struct for both directions.<\/li>\n\n\n\n<li><strong>The <code>template<\/code> echo is normalised.<\/strong> Full stops come back as spaces. Never compare that field to your stored body; compare <code>status<\/code> only.<\/li>\n\n\n\n<li><strong>The error drops <code>template<\/code>, <code>dltTemplateId<\/code>, <code>senderId<\/code> and <code>text<\/code>.<\/strong> A typed response class that requires them throws on exactly the response that explains the failure.<\/li>\n\n\n\n<li><strong>Validate the finished message, the exact string you are about to send,<\/strong> not the template skeleton with placeholders still in it. Operators judge the finished message. The examples on the validation page pass the skeleton, which proves the template is registered but not that your rendered values fit.<\/li>\n\n\n\n<li><strong>Status code <code>188<\/code> is the mismatch code.<\/strong> Log it with the <code>dltTemplateId<\/code> and the rendered text so a failure can be reproduced without guessing which values were substituted.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">For reverse lookups, <code>SMSApi\/preview<\/code> returns the matched <code>dltTemplateId<\/code> and part count for a finished message, and is covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-preview-cost-estimation-before-sending\/\">preview and cost estimation guide<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rcs-lifecycle\" class=\"wp-block-heading\">The RCS Template Lifecycle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">RCS templates belong to a bot. There are three operations over the API and no update.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Create.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/RCSApi\/template\/create<\/code>, form encoded, with <code>botId<\/code> and <code>templateData<\/code>, where <code>templateData<\/code> is a JSON string:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -s -X POST 'https:\/\/unify.smsgateway.center\/RCSApi\/template\/create' \\\n  -H 'apikey: YOUR_API_KEY' \\\n  -H 'Content-Type: application\/x-www-form-urlencoded' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'botId=YOUR_BOT_ID' \\\n  --data-urlencode 'templateData={\"name\":\"my_text_template\",\"type\":\"text_message\",\"templateState\":\"Create\",\"fallbackText\":\"\",\"category\":\"TRANSACTIONAL\",\"textMessageContent\":\"Hello &#91;name], your OTP is &#91;otp].\",\"suggestions\":&#91;]}'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The response is flat and tells you only that the request was queued:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"reason\": \"Accepted for processing.\",\n  \"templateName\": \"my_text_template\",\n  \"status\": \"success\",\n  \"statusCode\": \"200\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Accepted is not approved. A new template starts as Pending in the portal and is not usable for live sends until it is enabled.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>List.<\/strong> <code>GET https:\/\/unify.smsgateway.center\/RCSApi\/template\/list<\/code> with <code>botId<\/code> required. The response is a bare object with no status envelope at all:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"templates\": &#91;\n    {\n      \"data\": \"{\\\"name\\\":\\\"Flight_Booking\\\",\\\"type\\\":\\\"carousel\\\",...}\",\n      \"creationTime\": 1784978480698,\n      \"botName\": \"YourBot\",\n      \"name\": \"Flight_Booking\",\n      \"updationTime\": 1784978480698,\n      \"category\": \"Promotional\",\n      \"type\": \"carousel\",\n      \"status\": 1\n    }\n  ]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>data<\/code> is a JSON string holding the full template definition; decode it a second time to read the text and placeholders. <code>status<\/code> is an unquoted integer. <code>creationTime<\/code> and <code>updationTime<\/code> are unquoted epoch milliseconds. Because there is no <code>status<\/code> string at the top level, a generic &#8220;is this response a success&#8221; helper that looks for one will misreport every successful list call as a failure. Treat the presence of a <code>templates<\/code> array as success for this endpoint only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Delete.<\/strong> <code>POST https:\/\/unify.smsgateway.center\/RCSApi\/template\/delete<\/code>. Delete by the exact <code>name<\/code> string you read back from the list, then list again to confirm the row is gone. The round trip costs one GET and removes any doubt about which key the endpoint matched on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Send.<\/strong> The template&#8217;s <code>name<\/code> becomes <code>templateCode<\/code> in the send payload. The platform&#8217;s own wording: &#8220;Use the template <code>name<\/code> as <code>templateCode<\/code> when sending RCS.&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The portal recognises five template states: Pending, Enabled, Rejected, Disabled and Deleted, and describes deleted templates as unrecoverable. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-do-rcs-template-statuses-mean-and-how-do-i-manage-templates\/\">RCS template status guide<\/a> says only enabled templates should be used for sending. The list row reports state as an integer, so you need a mapping from integer to state; the section on approval state below gives a safe way to build one.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"whatsapp-lifecycle\" class=\"wp-block-heading\">The WhatsApp Template Lifecycle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp templates live on one URL, <code>https:\/\/unify.smsgateway.center\/WAApi\/template<\/code>, and the HTTP method selects the operation: POST creates, GET reads, DELETE deletes. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/whatsapp-business-api-wire-contract\/\">WhatsApp wire contract reference<\/a> covers the family&#8217;s envelopes in detail; here is what matters for template management.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Create (POST).<\/strong> Required: <code>wabaNumber<\/code>, <code>msgType<\/code>, <code>category<\/code> (<code>UTILITY<\/code>, <code>MARKETING<\/code> or <code>AUTHENTICATION<\/code>), <code>body<\/code>, <code>templateName<\/code>, <code>language<\/code>. Optional: <code>header<\/code> (maximum 60 characters, at most one variable), <code>headerSample<\/code>, <code>bodySample<\/code>, <code>footer<\/code> (maximum 60 characters, no variables), <code>templateDescription<\/code>, <code>buttons<\/code> (a JSON string) and <code>mediaType<\/code>. The response reason is &#8220;Accepted for processing.&#8221;. Supply <code>bodySample<\/code> whenever the body has variables: Meta reviews templates with sample values, and a variable with no plausible sample is a common reason for rejection. Meta&#8217;s <a href=\"https:\/\/developers.facebook.com\/documentation\/business-messaging\/whatsapp\/templates\/template-categorization\" target=\"_blank\" rel=\"noopener nofollow\">template categorization rules<\/a> decide whether a template filed as <code>UTILITY<\/code> stays there.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read (GET).<\/strong> The list read and the single-template read publish identical samples, so a single read returns an array, not an object. Index it; do not bind it to a single-row type. A row looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"template\": { \"footer\": \"...\", \"header\": \"...\", \"body\": \"Hi {{1}}, your order {{2}} has shipped.\" },\n  \"creationTime\": 1740835233326,\n  \"templateName\": \"test1\",\n  \"whatsAppStatus\": \"pending\",\n  \"systemStatus\": \"pending\",\n  \"mediaType\": \"text\",\n  \"language\": \"en\",\n  \"category\": \"UTILITY\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The body sits one level down inside a nested <code>template<\/code> object, next to <code>header<\/code> and <code>footer<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The name you get back is not always the name you sent.<\/strong> Submit <code>Test1<\/code> and the read returns <code>test1<\/code>. Normalise every WhatsApp template name to lower case before you store it or compare it, and prefer lower-case snake case names from the start so there is nothing to normalise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Delete (DELETE).<\/strong> Pass the template name and <code>language<\/code>. Language is part of identity: a template called <code>order_update<\/code> in <code>en<\/code> and one in <code>hi<\/code> are two templates, and deleting one leaves the other. The response is &#8220;Accepted for processing.&#8221;, so the deletion completes later. Re-read and confirm the row has gone before creating a replacement with the same name and language.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Send.<\/strong> <code>WAApi\/send<\/code> takes <code>templateName<\/code> and the finished text in <code>msg<\/code>, plus optional <code>header<\/code> and <code>footer<\/code>. Render every <code>{{n}}<\/code> yourself.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"telegram-templates\" class=\"wp-block-heading\">The Telegram Template Model<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram templates are read-only over the API. <code>GET https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/templates<\/code> supports two actions:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><code>action<\/code><\/th><th>Returns<\/th><\/tr><\/thead><tbody><tr><td><code>list<\/code> (default)<\/td><td>All active templates: <code>templateId<\/code>, <code>templateName<\/code>, <code>msgType<\/code>, <code>mediaUrl<\/code><\/td><\/tr><tr><td><code>get<\/code> with <code>templateId<\/code><\/td><td>One template&#8217;s full compose payload: <code>body<\/code>, <code>mediaPayloadJson<\/code>, <code>buttonsJson<\/code>, <code>useReplyMarkup<\/code> and, for some message types, location, contact or poll fields<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The list response uses the shared REST envelope:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"templatesList\": &#91;\n    { \"templateId\": 12, \"templateName\": \"Order Confirmation\", \"msgType\": \"text\", \"mediaUrl\": \"\" }\n  ],\n  \"totalRecords\": 1,\n  \"page\": 1,\n  \"limit\": 1,\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>get<\/code> response puts the template in an object:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"template\": {\n    \"templateId\": 12,\n    \"templateName\": \"Order Confirmation\",\n    \"msgType\": \"text\",\n    \"body\": \"Hi {name}, your order #{orderId} has shipped!\",\n    \"mediaUrl\": \"\",\n    \"mediaPayloadJson\": \"\",\n    \"buttonsJson\": \"\",\n    \"useReplyMarkup\": 0\n  },\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On a <code>get<\/code> error, such as a missing or unknown <code>templateId<\/code>, <code>template<\/code> becomes an empty array and <code>statusCode<\/code> carries values such as <code>400<\/code> or <code>404<\/code>. That is the object-to-array flip that runs through every Telegram endpoint, described in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/telegram-messaging-api-chat-id-model\/\">Telegram API reference<\/a>. A strongly typed client that declares <code>template<\/code> as an object throws on the error body before you can read <code>reason<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four more points for Telegram:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>templateId<\/code> is an unquoted integer; <code>templateName<\/code> is a display string with spaces (&#8220;Order Confirmation&#8221;) and is not a key.<\/li>\n\n\n\n<li><code>mediaPayloadJson<\/code> and <code>buttonsJson<\/code> are JSON strings. Decode them a second time.<\/li>\n\n\n\n<li>The list returns active templates only. A template that disappears from the list has been disabled or removed in the portal; handle that as &#8220;not sendable&#8221;, not as an error.<\/li>\n\n\n\n<li>Pass credentials in the <code>apikey<\/code> header rather than in the query string. The published samples put <code>password<\/code> in the URL of a GET request, which writes it into every proxy and access log on the path.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"template-identity\" class=\"wp-block-heading\">Identity: Why an Edit Is a New Template<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every channel has at least one way in which changing a template changes its identity, and none of them tells you so in the response.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Channel<\/th><th>Identity<\/th><th>What happens on an edit<\/th><th>What you must do<\/th><\/tr><\/thead><tbody><tr><td>SMS<\/td><td><code>mtId<\/code> locally; <code>dltTemplateId<\/code> at the regulator<\/td><td><code>SMSApi\/template\/update<\/code> keeps the call keyed on <code>id<\/code> and re-enters approval. A body change registered on the DLT portal comes back with a new DLT Template ID<\/td><td>Store <code>dltTemplateId<\/code> per version; never assume it is permanent<\/td><\/tr><tr><td>RCS<\/td><td><code>name<\/code>, which is also the <code>templateCode<\/code> used to send<\/td><td>No update endpoint; edit means delete and create<\/td><td>Create the new version under a new name; switch senders; then delete the old one<\/td><\/tr><tr><td>WhatsApp<\/td><td><code>templateName<\/code> plus <code>language<\/code><\/td><td>No update endpoint; edit means delete and create; delete is asynchronous<\/td><td>Create under a new name (for example with a <code>_v2<\/code> suffix), wait for approval, switch, then delete<\/td><\/tr><tr><td>Telegram<\/td><td><code>templateId<\/code> (integer)<\/td><td>Edited in the portal; the API gives no version marker<\/td><td>Snapshot the body you rendered with every send<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two design rules follow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rule one: own the key.<\/strong> Give every logical template a stable identifier in your own system, such as <code>order_shipped<\/code>, and never use a provider name or ID as that key. Map it to one active provider version per channel. When WhatsApp approves <code>order_shipped_v3<\/code>, you move a pointer; you do not rename anything that the rest of your code depends on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rule two: version by creating, not by mutating.<\/strong> Because two of the four channels cannot edit in place, and the third changes its regulator ID when edited, the only workflow that behaves the same everywhere is: create a new version alongside the old, wait for approval, switch traffic, drain, then retire. Even on SMS, where <code>update<\/code> exists, creating a new template and switching is safer than updating the live one, because an update pulls the live template back into approval and anything sent against it in the meantime is at risk.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">outbound message table design<\/a> explains why every outbound row should reference the exact template version it was sent under. Template versioning is what makes that reference meaningful.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"approval-state\" class=\"wp-block-heading\">Approval State: A String, an Integer, Two Fields or Nothing<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Channel<\/th><th>Field<\/th><th>Type<\/th><th>Values seen in published samples<\/th><th>Sendable when<\/th><\/tr><\/thead><tbody><tr><td>SMS<\/td><td><code>status<\/code> on the read row<\/td><td>Quoted string<\/td><td><code>\"ENABLED\"<\/code><\/td><td><code>status<\/code> is <code>ENABLED<\/code> and <code>dltTemplateId<\/code> is populated<\/td><\/tr><tr><td>RCS<\/td><td><code>status<\/code> on the list row<\/td><td>Unquoted integer<\/td><td><code>1<\/code><\/td><td>The integer maps to Enabled<\/td><\/tr><tr><td>WhatsApp<\/td><td><code>whatsAppStatus<\/code> and <code>systemStatus<\/code><\/td><td>Quoted strings<\/td><td><code>\"pending\"<\/code> for both<\/td><td>Both fields report approval<\/td><\/tr><tr><td>Telegram<\/td><td>none<\/td><td>none<\/td><td>none<\/td><td>The template appears in <code>action=list<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp is the one that catches teams out. There are two independent approval fields: one reflects Meta&#8217;s decision, the other the platform&#8217;s own state. A template Meta has approved can still be held by the platform, and the reverse. Gate on both.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You cannot enumerate every value these fields may take, so build the normaliser as an allowlist:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SENDABLE = {\n    (\"sms\", \"status\"): {\"ENABLED\"},\n    (\"rcs\", \"status\"): {1},\n    # Fill these from the values you observe on templates you know are live:\n    (\"whatsapp\", \"whatsAppStatus\"): set(),\n    (\"whatsapp\", \"systemStatus\"): set(),\n}\n\ndef is_sendable(channel: str, row: dict) -&gt; bool:\n    if channel == \"telegram\":\n        return True  # presence in action=list is the only signal\n    if channel == \"whatsapp\":\n        return (row.get(\"whatsAppStatus\", \"\").lower() in SENDABLE&#91;(\"whatsapp\", \"whatsAppStatus\")]\n                and row.get(\"systemStatus\", \"\").lower() in SENDABLE&#91;(\"whatsapp\", \"systemStatus\")])\n    key = (channel, \"status\")\n    return row.get(\"status\") in SENDABLE&#91;key]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The allowlist approach fails closed: an unfamiliar value makes a template unsendable rather than sendable. Store the raw provider value next to your normalised state so that when a new value appears you can see it, add it to the right set, and re-run the check.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"envelopes\" class=\"wp-block-heading\">Response Envelopes Across the Template Endpoints<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th>Top-level shape<\/th><th>Success signal<\/th><th>Code field and type<\/th><th>Error behaviour<\/th><\/tr><\/thead><tbody><tr><td><code>SMSApi\/template\/create<\/code>, <code>read<\/code>, <code>update<\/code>, <code>delete<\/code><\/td><td>Wrapped in <code>response<\/code><\/td><td><code>response.status<\/code> is <code>\"success\"<\/code><\/td><td><code>code<\/code>, quoted <code>\"200\"<\/code><\/td><td>Parse loosely<\/td><\/tr><tr><td><code>SMSApi\/validateTemplate<\/code><\/td><td>Flat<\/td><td><code>status<\/code> is <code>\"success\"<\/code><\/td><td><code>statusCode<\/code>, quoted<\/td><td>Drops every key except <code>status<\/code>, <code>statusCode<\/code>, <code>reason<\/code><\/td><\/tr><tr><td><code>RCSApi\/template\/create<\/code><\/td><td>Flat<\/td><td><code>status<\/code> is <code>\"success\"<\/code><\/td><td><code>statusCode<\/code>, quoted<\/td><td>Parse loosely<\/td><\/tr><tr><td><code>RCSApi\/template\/list<\/code><\/td><td>Bare <code>{\"templates\": [...]}<\/code><\/td><td>Presence of <code>templates<\/code><\/td><td>none<\/td><td>Parse loosely<\/td><\/tr><tr><td><code>WAApi\/template<\/code><\/td><td>Flat<\/td><td><code>status<\/code> is <code>\"success\"<\/code><\/td><td><code>statusCode<\/code>, quoted<\/td><td>Parse loosely<\/td><\/tr><tr><td><code>rest\/tg\/v1\/templates<\/code><\/td><td>Flat REST envelope<\/td><td><code>status<\/code> is <code>\"success\"<\/code><\/td><td><code>statusCode<\/code>, quoted<\/td><td>Payload key becomes <code>[]<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Five envelopes for one concept. The pattern that survives all of them is the two-stage parse used throughout this series:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Parse the body into a loose map, never straight into a typed class.<\/li>\n\n\n\n<li>Find the status: <code>response.status<\/code> for the SMS family, <code>status<\/code> for everything else, and the presence of <code>templates<\/code> for the RCS list.<\/li>\n\n\n\n<li>If it is not a success, return an error carrying the raw body and stop.<\/li>\n\n\n\n<li>Only on success, bind the payload to a typed structure for that endpoint.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Never branch on <code>code<\/code> or <code>statusCode<\/code> values, and never parse the text of <code>msg<\/code> or <code>reason<\/code>. On this platform the SMS group delete returns a code other than <code>200<\/code> on success; the only field whose meaning has held across every family is the <code>status<\/code> string.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"registry-schema\" class=\"wp-block-heading\">A Portable Template Registry Schema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The schema below holds all four channels without pretending they are the same. It is PostgreSQL; the ideas carry to any relational store.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- One row per logical template, keyed on a name you own and never change.\nCREATE TABLE template_family (\n    family_key      text PRIMARY KEY,            -- e.g. 'order_shipped'\n    description     text NOT NULL,\n    created_at      timestamptz NOT NULL DEFAULT now()\n);\n\n-- One row per provider-side template version.\nCREATE TABLE template_version (\n    id                  bigserial PRIMARY KEY,\n    family_key          text NOT NULL REFERENCES template_family(family_key),\n    channel             text NOT NULL CHECK (channel IN ('sms','rcs','whatsapp','telegram')),\n    version             integer NOT NULL,\n    scope_ref           text,          -- RCS botId, WhatsApp wabaNumber; NULL for SMS and Telegram\n    provider_name       text,          -- RCS name, WhatsApp templateName (lower-cased), Telegram templateName\n    provider_language   text,          -- WhatsApp only\n    provider_local_id   text,          -- SMS mtId, Telegram templateId, both stored as text\n    dlt_template_id     text,          -- SMS only; nineteen digits, never numeric\n    dlt_entity_id       text,          -- SMS only\n    sender_ids          text&#91;],        -- SMS only\n    placeholder_syntax  text NOT NULL CHECK (placeholder_syntax IN ('dlt_typed','dlt_var','rcs_bracket','wa_positional','tg_brace')),\n    placeholders        jsonb NOT NULL,-- ordered: &#91;{\"name\":\"otp\",\"tag\":\"numeric\"}, ...]\n    body_snapshot       text NOT NULL, -- exactly as read back from the provider\n    approval_raw        jsonb,         -- the provider's own fields, verbatim\n    approval_state      text NOT NULL DEFAULT 'submitted'\n                        CHECK (approval_state IN ('draft','submitted','sendable','rejected','disabled','retiring','retired','unknown')),\n    submitted_at        timestamptz,\n    sendable_at         timestamptz,\n    retired_at          timestamptz,\n    UNIQUE (family_key, channel, version)\n);\n\n-- Exactly one active version per family and channel. Switching is one UPDATE.\nCREATE TABLE template_active (\n    family_key   text NOT NULL REFERENCES template_family(family_key),\n    channel      text NOT NULL,\n    version_id   bigint NOT NULL REFERENCES template_version(id),\n    switched_at  timestamptz NOT NULL DEFAULT now(),\n    PRIMARY KEY (family_key, channel)\n);\n\n-- Every outbound message records the version it was rendered from.\n-- ALTER TABLE outbound_message ADD COLUMN template_version_id bigint REFERENCES template_version(id);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Seven decisions, and why each one holds up:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>family_key<\/code> is yours.<\/strong> Every provider identifier on this platform can change when content changes. A key you own is the only thing application code can depend on across an edit.<\/li>\n\n\n\n<li><strong>Provider identifiers are text.<\/strong> <code>mtId<\/code> is a quoted string, <code>dltTemplateId<\/code> runs to nineteen digits, and <code>templateId<\/code> is an integer on Telegram. Text holds all three without precision loss and without a per-channel column type.<\/li>\n\n\n\n<li><strong><code>provider_name<\/code> is stored lower-cased for WhatsApp.<\/strong> The platform lower-cases it on the way in; storing what you sent guarantees a mismatch on the first read.<\/li>\n\n\n\n<li><strong><code>placeholder_syntax<\/code> is explicit.<\/strong> The renderer chooses its pattern from this column, never from the text. That prevents the Telegram pattern from being applied to WhatsApp text and quietly half-matching.<\/li>\n\n\n\n<li><strong><code>body_snapshot<\/code> is what the provider returned, not what you submitted.<\/strong> For validation and support tickets you need the text the provider holds.<\/li>\n\n\n\n<li><strong><code>approval_raw<\/code> sits next to <code>approval_state<\/code>.<\/strong> When a provider introduces a value your normaliser does not know, the state goes to <code>unknown<\/code> and the raw value tells you what arrived.<\/li>\n\n\n\n<li><strong><code>template_active<\/code> is a separate table.<\/strong> Switching versions is one atomic row update, and the history of switches is queryable. Senders read the active pointer; they never pick &#8220;the newest approved row&#8221;, which would switch traffic the moment an approval landed, before anyone had checked it.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rendering-code\" class=\"wp-block-heading\">Rendering Safely in Four Languages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every mainstream regex replace function treats some characters in the replacement value as instructions. Template values are customer data: names, addresses, amounts, promo codes. Sooner or later one of them contains a <code>$<\/code> or a backslash. Each sample below shows a different channel and a different trap, and all four use the same fix: find placeholders with a pattern, but build the output from pieces, so the value is never interpreted.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: DLT typed tags, and the backslash trap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>re.sub(pattern, value, text)<\/code> interprets backslash escapes in <code>value<\/code>. A value such as <code>C:\\new<\/code> inserts a newline; <code>\\1<\/code> raises an error. The tokenizer below validates each value against its DLT tag and never passes a value to <code>re.sub<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\n\nDLT = re.compile(r\"\\{#(numeric|alphanumeric|url|urlott|cbn|email|var)#\\}\")\n\ndef render_dlt(template: str, values: list&#91;str]) -&gt; str:\n    out, pos, i = &#91;], 0, 0\n    for m in DLT.finditer(template):\n        if i &gt;= len(values):\n            raise ValueError(\"fewer values than placeholders\")\n        tag, value = m.group(1), values&#91;i]\n        if tag != \"var\":\n            check_value(tag, value)      # from the DLT tag section above\n        out.append(template&#91;pos:m.start()])\n        out.append(value)                # appended, never interpreted\n        pos, i = m.end(), i + 1\n    if i != len(values):\n        raise ValueError(\"more values than placeholders\")\n    out.append(template&#91;pos:])\n    return \"\".join(out)\n\nmsg = render_dlt(\n    \"Dear {#alphanumeric#}, your OTP is {#numeric#}. Do not share it. - YourBrand\",\n    &#91;\"Asha\", \"482913\"],\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Count mismatches raise rather than leaving a placeholder in the text. A literal <code>{#numeric#}<\/code> reaching a handset is a template mismatch on SMS and an embarrassment everywhere else.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">JavaScript: WhatsApp positional variables, and the dollar trap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>String.prototype.replace<\/code> treats <code>$&amp;<\/code>, <code>$`<\/code>, <code>$'<\/code> and <code>$$<\/code> in a replacement string as patterns. A promo code of <code>SAVE$$<\/code> arrives as <code>SAVE$<\/code>. Passing a function as the replacement disables that interpretation. WhatsApp positions are one-based, so index <code>values[n - 1]<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const WA = \/\\{\\{(&#91;1-9]&#91;0-9]*)\\}\\}\/g;\n\nfunction renderWhatsApp(body, values) {\n  const seen = new Set();\n  const text = body.replace(WA, (_, n) =&gt; {\n    const idx = Number(n) - 1;\n    if (idx &gt;= values.length) throw new Error(`no value for {{${n}}}`);\n    seen.add(idx);\n    return String(values&#91;idx]);          \/\/ function return is taken literally\n  });\n  if (seen.size !== values.length) throw new Error(\"unused values supplied\");\n  return text;\n}\n\n\/\/ Two-stage parse of a WAApi\/template GET response\nfunction templateRows(raw) {\n  const doc = JSON.parse(raw);\n  if (doc.status !== \"success\") throw new Error(`template read failed: ${raw}`);\n  return doc;                            \/\/ bind rows only after the status check\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same template body may reuse a position (<code>{{1}}<\/code> twice). The <code>seen<\/code> set counts distinct positions, so reuse is allowed while unused values are still caught.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP: RCS customParams, and the preg_replace trap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>preg_replace<\/code> reads <code>$n<\/code> and <code>\\n<\/code> in the replacement as back-references, with <code>n<\/code> up to two digits. A value of <code>Rs $100<\/code> becomes <code>Rs 0<\/code>, because <code>$10<\/code> refers to a group that does not exist and is replaced with nothing. For RCS you do not substitute at all: you build <code>customParams<\/code>, and the only hazard left is encoding. Each JSON layer needs its own <code>json_encode<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\nfunction rcsSendPayload(string $templateCode, array $vars): string {\n    $params = &#91;];\n    foreach ($vars as $name =&gt; $value) {\n        $params&#91;trim($name, '&#91;]')] = (string) $value;   \/\/ key without brackets\n    }\n    $msg = &#91;\n        'contentMessage' =&gt; &#91;\n            'templateMessage' =&gt; &#91;\n                'templateCode' =&gt; $templateCode,\n                'customParams' =&gt; json_encode($params, JSON_UNESCAPED_UNICODE),   \/\/ inner layer: a string\n            ],\n        ],\n        'ttl' =&gt; '86400s',\n    ];\n    return json_encode($msg, JSON_UNESCAPED_UNICODE);                   \/\/ outer layer\n}\n\n\/\/ If you must substitute text in PHP, use a callback:\nfunction renderTelegramStyle(string $body, array $vars): string {\n    return preg_replace_callback('\/\\{(&#91;A-Za-z_]&#91;A-Za-z0-9_]*)\\}\/', function ($m) use ($vars) {\n        if (!array_key_exists($m&#91;1], $vars)) {\n            throw new RuntimeException(\"missing value for {$m&#91;1]}\");\n        }\n        return (string) $vars&#91;$m&#91;1]];\n    }, $body);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Send the result of <code>rcsSendPayload()<\/code> as the <code>msg<\/code> form field with <code>format=json<\/code>, <code>botId<\/code>, <code>sendMethod<\/code> and <code>msgType=text<\/code>. Form-encode it with <code>http_build_query<\/code> or <code>CURLOPT_POSTFIELDS<\/code> as an array, never by string concatenation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Java: Telegram template get, and the replaceAll trap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Matcher.replaceAll<\/code> and <code>String.replaceAll<\/code> throw <code>IllegalArgumentException<\/code> on a value containing <code>$<\/code> followed by something that is not a valid group reference, and treat backslash as an escape. <code>Matcher.quoteReplacement<\/code> neutralises both. The parse below also survives the Telegram object-to-array flip on error.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class TelegramTemplates {\n    private static final ObjectMapper MAPPER = new ObjectMapper();\n    private static final Pattern FIELD = Pattern.compile(\"\\\\{(&#91;A-Za-z_]&#91;A-Za-z0-9_]*)\\\\}\");\n\n    public static String body(String rawJson) throws Exception {\n        JsonNode doc = MAPPER.readTree(rawJson);             \/\/ loose parse first\n        if (!\"success\".equals(doc.path(\"status\").asText())) {\n            throw new IllegalStateException(\"template get failed: \" + rawJson);\n        }\n        JsonNode tpl = doc.path(\"template\");                 \/\/ object on success, &#91;] on error\n        if (!tpl.isObject()) throw new IllegalStateException(\"unexpected template shape\");\n        return tpl.path(\"body\").asText();\n    }\n\n    public static String render(String body, Map&lt;String, String&gt; vars) {\n        Matcher m = FIELD.matcher(body);\n        StringBuilder out = new StringBuilder();\n        while (m.find()) {\n            String v = vars.get(m.group(1));\n            if (v == null) throw new IllegalArgumentException(\"missing value for \" + m.group(1));\n            m.appendReplacement(out, Matcher.quoteReplacement(v));\n        }\n        m.appendTail(out);\n        String text = out.toString();\n        if (text.length() &gt; 4096) throw new IllegalArgumentException(\"rendered text exceeds 4096 characters\");\n        return text;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For a media template the limit is 1024, because the text becomes a caption. Choose the limit from <code>msgType<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"change-workflow\" class=\"wp-block-heading\">The Template Change Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the procedure that behaves the same on all four channels, including the two that cannot edit in place.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Draft the new version in your registry.<\/strong> Insert a <code>template_version<\/code> row with <code>approval_state = 'draft'<\/code>, the next version number and the intended body. Run the renderer against a set of realistic values, including long names, amounts with no separators and values containing <code>$<\/code> and backslash.<\/li>\n\n\n\n<li><strong>Create it at the provider under a new name.<\/strong> On RCS and WhatsApp, use a versioned name such as <code>order_shipped_v3<\/code>. On SMS, register the new body on the DLT portal first to obtain its DLT Template ID, then create it with <code>SMSApi\/template\/create<\/code>. On Telegram, create or edit it in the portal.<\/li>\n\n\n\n<li><strong>Read back and capture identity.<\/strong> SMS: read the list, match on the exact body, take the newest <code>lastUpdated<\/code>, store <code>mtId<\/code> and <code>dltTemplateId<\/code>. RCS: list by <code>botId<\/code>, match on <code>name<\/code>. WhatsApp: read, match on lower-cased name and language. Telegram: list, match on <code>templateName<\/code>, then <code>get<\/code> by <code>templateId<\/code>. Store <code>body_snapshot<\/code> from the read, not from your draft.<\/li>\n\n\n\n<li><strong>Poll approval until sendable, with back-off.<\/strong> Poll every few minutes at first, then hourly. Stop and alert on a rejected state. Keep <code>approval_raw<\/code> updated on every poll.<\/li>\n\n\n\n<li><strong>Switch the active pointer.<\/strong> One update to <code>template_active<\/code>. New sends render from the new version from that moment; anything already queued keeps the version it was rendered with.<\/li>\n\n\n\n<li><strong>Drain, then retire.<\/strong> Wait until no queued message, retry, scheduled campaign or fallback step references the old version. Then delete it at the provider, re-read to confirm it has gone (WhatsApp deletes asynchronously), and mark the row <code>retired<\/code>.<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-template-change-workflow.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-template-change-workflow.svg\" alt=\"Three-panel flow from your template registry, through provider create, read back and approval polling, to a table showing what an edit changes on SMS, RCS, WhatsApp and Telegram.\" class=\"wp-image-3037\"\/><\/a><figcaption class=\"wp-element-caption\">Create alongside, switch a pointer, drain, then retire. The only edit workflow that behaves the same on all four channels.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Step six is where most outages come from. A scheduled SMS campaign or a delayed WhatsApp send created last week still references the old template. Delete that template and those sends fail at dispatch time, often at night. Scheduled SMS campaigns cannot be read back with their body or template, as covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/campaign-splitting-one-send-several-transactions\/\">campaign splitting guide<\/a>, so record the template version on your own side when you schedule.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"ten-mistakes\" class=\"wp-block-heading\">Ten Template Mistakes That Reach Production<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>One substitution function for every channel.<\/strong> <code>{#numeric#}<\/code>, <code>[otp]<\/code>, <code>{{1}}<\/code> and <code>{otp}<\/code> need four patterns. A shared function leaves literal placeholders in three of them.<\/li>\n\n\n\n<li><strong>Keying on the name you submitted.<\/strong> WhatsApp returns <code>test1<\/code> for <code>Test1<\/code>. The first lookup after create fails, and a retry creates a duplicate.<\/li>\n\n\n\n<li><strong>Treating &#8220;Accepted for processing.&#8221; as approval.<\/strong> RCS and WhatsApp create calls queue the template. Sending against it immediately fails.<\/li>\n\n\n\n<li><strong>Checking only one WhatsApp approval field.<\/strong> <code>whatsAppStatus<\/code> and <code>systemStatus<\/code> are independent. Both must clear.<\/li>\n\n\n\n<li><strong>Updating a live SMS template in place.<\/strong> <code>SMSApi\/template\/update<\/code> puts the template back into approval. Create a new one and switch.<\/li>\n\n\n\n<li><strong>Deleting the old version on switch day.<\/strong> Scheduled sends and retries still reference it. Drain first.<\/li>\n\n\n\n<li><strong>Regex replace with customer data.<\/strong> <code>Rs $100<\/code> becomes <code>Rs 0<\/code> in PHP, <code>SAVE$$<\/code> becomes <code>SAVE$<\/code> in JavaScript, and Java throws. Tokenize.<\/li>\n\n\n\n<li><strong>Formatting amounts inside a <code>{#numeric#}<\/code> slot.<\/strong> <code>1,299.00<\/code> fails a digits-only tag. Put separators in the fixed text.<\/li>\n\n\n\n<li><strong>Nesting <code>customParams<\/code> as an object.<\/strong> It is a JSON string inside the <code>msg<\/code> JSON string. An object where a string is expected sends placeholders unrendered or fails the send.<\/li>\n\n\n\n<li><strong>Binding the Telegram <code>get<\/code> response to an object type.<\/strong> On error <code>template<\/code> is <code>[]<\/code>, and the deserialiser throws before you can read <code>reason<\/code>.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"decision-matrix\" class=\"wp-block-heading\">Decision Matrix<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Situation<\/th><th>SMS<\/th><th>RCS<\/th><th>WhatsApp<\/th><th>Telegram<\/th><\/tr><\/thead><tbody><tr><td>Change wording of a live template<\/td><td>Register new DLT body, create new, switch<\/td><td>Create <code>_vN<\/code>, wait for Enabled, switch<\/td><td>Create <code>_vN<\/code>, wait for both approvals, switch<\/td><td>Edit in portal; snapshot bodies on send<\/td><\/tr><tr><td>Add a variable<\/td><td>New DLT registration with the right tag<\/td><td>New template with <code>[new_name]<\/code><\/td><td>New template, new <code>{{n}}<\/code>, new <code>bodySample<\/code><\/td><td>Edit in portal<\/td><\/tr><tr><td>Fix a typo only<\/td><td>Still a new DLT body and ID<\/td><td>Recreate<\/td><td>Recreate<\/td><td>Portal edit<\/td><\/tr><tr><td>Find the id after create<\/td><td>Read all, match body, newest <code>lastUpdated<\/code><\/td><td>List by <code>botId<\/code>, match <code>name<\/code><\/td><td>Read, match lower-cased name plus language<\/td><td>List, match name, then <code>get<\/code><\/td><\/tr><tr><td>Check before sending<\/td><td><code>SMSApi\/validateTemplate<\/code> on the finished text<\/td><td>Status integer maps to Enabled<\/td><td>Both approval fields<\/td><td>Present in <code>action=list<\/code><\/td><\/tr><tr><td>Where values go on send<\/td><td>Rendered into <code>msg<\/code><\/td><td><code>customParams<\/code> JSON string<\/td><td>Rendered into <code>msg<\/code><\/td><td>Rendered into <code>text<\/code><\/td><\/tr><tr><td>Remove a template<\/td><td><code>SMSApi\/template\/delete<\/code> by <code>id<\/code><\/td><td><code>RCSApi\/template\/delete<\/code> by <code>name<\/code><\/td><td>DELETE with name and <code>language<\/code><\/td><td>Portal<\/td><\/tr><tr><td>Confirm removal<\/td><td>Response is synchronous<\/td><td>Re-list<\/td><td>Re-read after a delay<\/td><td>Absent from list<\/td><\/tr><tr><td>Length limit to enforce<\/td><td>Part count; up to 255 parts<\/td><td>Card field limits<\/td><td>Header and footer 60 characters<\/td><td>4096 text, 1024 caption<\/td><\/tr><tr><td>Multi-language<\/td><td>One DLT template per language body<\/td><td>One template per language<\/td><td>Same name, different <code>language<\/code><\/td><td>One template per language<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"checklist\" class=\"wp-block-heading\">Pre-Launch Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Registry and identity<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Every logical template has a <code>family_key<\/code> you own, independent of any provider name<\/li>\n\n\n\n<li>Provider identifiers stored as text, including <code>mtId<\/code>, <code>dltTemplateId<\/code> and <code>templateId<\/code><\/li>\n\n\n\n<li>WhatsApp names stored lower-cased; comparisons are case-insensitive on RCS categories and WhatsApp names<\/li>\n\n\n\n<li><code>placeholder_syntax<\/code> stored per version and used to pick the renderer<\/li>\n\n\n\n<li><code>body_snapshot<\/code> taken from the provider read, not from your draft<\/li>\n\n\n\n<li><code>approval_raw<\/code> stored beside the normalised state<\/li>\n\n\n\n<li>Active version held in a pointer table, switched in one update<\/li>\n\n\n\n<li>Every outbound row references the template version it was rendered from<\/li>\n\n\n\n<li>Versioned provider names (<code>_v2<\/code>, <code>_v3<\/code>) on RCS and WhatsApp; no name reused after delete<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rendering and validation<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One placeholder pattern per channel, anchored exactly<\/li>\n\n\n\n<li>Output built by concatenating pieces; no regex replace with a value string<\/li>\n\n\n\n<li>Count mismatches between values and placeholders raise an error<\/li>\n\n\n\n<li>DLT values checked against their tag before render<\/li>\n\n\n\n<li>Amount separators and currency marks in fixed text, digits only in <code>{#numeric#}<\/code><\/li>\n\n\n\n<li>Adjacent DLT placeholders only where both are <code>{#alphanumeric#}<\/code><\/li>\n\n\n\n<li>Finished SMS text checked with <code>SMSApi\/validateTemplate<\/code> against the <code>dltTemplateId<\/code> and sender ID you will use<\/li>\n\n\n\n<li>WhatsApp positions indexed from one<\/li>\n\n\n\n<li>RCS <code>customParams<\/code> keyed without brackets and encoded as a JSON string<\/li>\n\n\n\n<li>Telegram rendered length checked against 4096 or 1024 depending on <code>msgType<\/code><\/li>\n\n\n\n<li>Test values include <code>$<\/code>, <code>$$<\/code>, <code>$&amp;<\/code>, backslash, a URL, a 40-character name and an empty string<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Lifecycle and operations<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Every create followed by a read that captures identity<\/li>\n\n\n\n<li>Approval polled with back-off; rejections alert a human<\/li>\n\n\n\n<li>Unknown approval values fail closed<\/li>\n\n\n\n<li>Every delete followed by a re-read<\/li>\n\n\n\n<li>No template deleted while a queued, retrying, scheduled or fallback send references it<\/li>\n\n\n\n<li>Responses parsed loosely, branched on the <code>status<\/code> string, typed only on success<\/li>\n\n\n\n<li><code>apikey<\/code> header used on every call; no passwords in query strings<\/li>\n\n\n\n<li>Template read calls cached; they return whole accounts and have no filter on SMS<\/li>\n\n\n\n<li>A nightly job re-reads every active template and alerts if one has disappeared or changed state<\/li>\n\n\n\n<li>DLT migration from <code>{#var#}<\/code> to typed tags tracked per template, with the new DLT Template IDs recorded<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"unspecified-behaviour\" class=\"wp-block-heading\">Unspecified Behaviour and How to Code Around It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A handful of template behaviours are not pinned down by anything you can read before you integrate. Each item below gives the choice that stays correct whichever way the behaviour turns out, and none of them requires waiting for an answer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Find a new SMS template by its exact body and the newest <code>lastUpdated<\/code>.<\/strong> The create call returns no <code>mtId<\/code> and the read call has no filter. Read the full list, compare the <code>template<\/code> field byte for byte with what you submitted, and if several rows match, take the highest <code>lastUpdated<\/code>. Store the <code>mtId<\/code> you find. If no row matches, the platform normalised your text; stop and inspect rather than retrying the create.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Only send against an SMS template whose <code>dltTemplateId<\/code> is populated and whose <code>status<\/code> is <code>ENABLED<\/code>.<\/strong> The create call accepts only a body, so the DLT ID arrives on the row by some later step. A template without a DLT ID cannot pass operator scrubbing, whatever its local status says. Gate on both fields together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Build the RCS status mapping from observation and fail closed.<\/strong> The list row carries an integer, and the portal names five states. Record the integer you see on a template the portal shows as Enabled, add it to your allowlist, and treat every other integer as not sendable. A new integer then blocks sends instead of letting them through.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Populate the WhatsApp approval allowlist from a template you have watched go live.<\/strong> The samples show <code>pending<\/code> for both fields. Note the pair of values on your first approved template and allow exactly that pair, lower-cased. When a template sits in an unfamiliar combination, it stays unsendable and <code>approval_raw<\/code> shows you why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Never reuse a WhatsApp name and language pair.<\/strong> WhatsApp deletion completes asynchronously, and recreating a deleted name while the delete is in flight is a race you do not need to run. Versioned names make the question irrelevant: <code>_v4<\/code> never collides with a <code>_v3<\/code> that is still being removed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Never reuse a deleted RCS template name either.<\/strong> Deleted RCS templates cannot be recovered, and whether their names are released for reuse is not something to depend on. Versioned names cost nothing and remove the dependency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Snapshot the Telegram body you rendered, on every send.<\/strong> A portal edit may or may not keep the same <code>templateId<\/code>. Storing the body you actually sent next to the message row means a later edit cannot change what your records say was delivered.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Key RCS <code>customParams<\/code> on the placeholder name without brackets, and prove it with one send to your own handset.<\/strong> The template holds <code>[name]<\/code>; the published payload keys look like <code>custom_var<\/code> and <code>amount<\/code>. Send one message with every variable set to a distinctive value and check the handset. One test settles the mapping for every template of that type.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Send WhatsApp <code>msg<\/code> as the approved body with every <code>{{n}}<\/code> substituted, and nothing else changed.<\/strong> Whether the platform matches <code>msg<\/code> against the approved body or builds the message from the values it extracts, a body identical to the approved one apart from the variables is correct under both. Do not trim, re-wrap or add a signature.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Compare template names and categories case-insensitively on RCS and WhatsApp.<\/strong> Both return values in a different case from the one submitted. A case-insensitive comparison is correct whether the flip is deliberate normalisation or incidental.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Treat any SMS template <code>status<\/code> other than <code>ENABLED<\/code> as not sendable.<\/strong> Other values may appear, but only <code>ENABLED<\/code> appears in the samples. An allowlist of one is safe no matter how many other states turn up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Twelve. Parse every template error response loosely.<\/strong> Most template endpoints publish only a success sample. Across the rest of the platform, error bodies either drop keys, flip objects to empty arrays or keep their shape. The two-stage parse from the envelopes section is correct under all three, so it needs no knowledge of which one a given endpoint uses.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I use one template across SMS, RCS, WhatsApp and Telegram?<\/strong><br>Not at the provider level. Each channel stores its own template with its own placeholder syntax, identity and approval. You can have one logical template in your own registry that maps to four provider templates, which is the design recommended above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between <code>{#var#}<\/code> and <code>{#numeric#}<\/code>?<\/strong><br><code>{#var#}<\/code> is the legacy untyped DLT placeholder. <code>{#numeric#}<\/code> is one of six typed tags introduced by the TRAI direction of 18 November 2025. A typed tag tells operators what the value must look like, and messages whose values do not match are rejected once enforcement is fully on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does editing a DLT template change its DLT Template ID?<\/strong><br>Yes, when the change is registered on the DLT portal. The migration guidance for variable tags says the portal issues a new Template ID for the updated template, and it must be updated in your account and in any application that sends through the API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there an update endpoint for RCS or WhatsApp templates?<\/strong><br>No. Both families offer create, read and delete. To change a template, create a new one under a versioned name, wait for approval, switch your senders, then delete the old one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does <code>SMSApi\/template\/create<\/code> not return a template ID?<\/strong><br>The response confirms that the template was created and given for approval, and carries no identifier. Read the list afterwards and match on the exact body to find the <code>mtId<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I know when a WhatsApp template is approved?<\/strong><br>Read it with a GET on <code>WAApi\/template<\/code> and check both <code>whatsAppStatus<\/code> and <code>systemStatus<\/code>. Both must report approval before you send.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What does &#8220;Accepted for processing.&#8221; mean?<\/strong><br>The request was queued. On RCS and WhatsApp create it means the template is waiting for review. On WhatsApp delete it means deletion will complete later. In both cases, re-read to see the outcome.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Where do variable values go when sending an RCS template?<\/strong><br>In <code>customParams<\/code>, a JSON string inside the <code>msg<\/code> JSON string, next to <code>templateCode<\/code>, which is the template&#8217;s <code>name<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I create Telegram templates through the API?<\/strong><br>No. <code>rest\/tg\/v1\/templates<\/code> offers <code>list<\/code> and <code>get<\/code> only. Create and edit Telegram templates in the portal, then read them over the API to prefill a send.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does my PHP template renderer turn <code>Rs $100<\/code> into <code>Rs 0<\/code>?<\/strong><br><code>preg_replace<\/code> reads <code>$10<\/code> in the replacement as a back-reference to group ten, which does not exist, so it is replaced with nothing. Use <code>preg_replace_callback<\/code> or build the string from pieces.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I validate the template or the finished message?<\/strong><br>The finished message. <code>SMSApi\/validateTemplate<\/code> accepts any text; passing the placeholder skeleton only proves the template exists, while passing the rendered text proves the message you are about to send will match.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does the validation response spell <code>senderId<\/code> differently from the request?<\/strong><br>The request parameter is <code>senderid<\/code> and the response field is <code>senderId<\/code>. Use separate request and response types.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do WhatsApp template names keep the case I submit?<\/strong><br>No. A name submitted as <code>Test1<\/code> reads back as <code>test1<\/code>. Use lower-case snake case names from the start.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How should I handle a template that disappears from the Telegram list?<\/strong><br>Treat it as disabled or removed: mark the version not sendable, alert, and fall back to the next version or channel. Presence in <code>action=list<\/code> is the only availability signal Telegram gives.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Lets Build<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Building a sender that has to work across SMS, RCS, WhatsApp and Telegram? <a href=\"https:\/\/www.smsgatewaycenter.com\/demo\/\">Try the API in the sandbox<\/a> before you register a single template, or <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">talk to our team<\/a> about template migration to DLT variable tags.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>Four channels, five placeholder spellings, four identity models and three approval shapes. A field-by-field comparison of the SMS DLT, RCS, WhatsApp and Telegram template endpoints, with a portable registry schema, safe rendering code in four languages and a change workflow that survives approval delays.<\/p>\n","protected":false},"author":118,"featured_media":3036,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[449,2277,1084,2031,2280,481,2281,2029,2278,2279],"class_list":["post-3035","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-dlt-template-api","tag-dlt-variable-tags","tag-message-templates","tag-messaging-architecture","tag-rcs-templates","tag-sms-api","tag-telegram-templates","tag-template-approval","tag-template-placeholders","tag-whatsapp-template-api"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3035","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=3035"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3035\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/3036"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=3035"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=3035"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=3035"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}