{"id":2628,"date":"2026-08-12T13:29:18","date_gmt":"2026-08-12T07:59:18","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2628"},"modified":"2026-08-12T13:29:42","modified_gmt":"2026-08-12T07:59:42","slug":"dlt-template-rejected-reasons-and-fixes","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-template-rejected-reasons-and-fixes\/","title":{"rendered":"DLT Template Rejected: Every Reason and How to Fix It"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A DLT content template gets rejected for a small number of repeatable reasons: variable misuse, category misclassification, header mismatch, unwhitelisted URLs, disallowed characters and duplicates. This guide maps every rejection cause to a concrete fix, then shows how to stop the second, quieter failure: a template that is approved but still fails at send time with status code 188.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes-1024x584.webp\" alt=\"Diagram of a DLT content template passing through validation gates with one rejected line looping back for correction\" class=\"wp-image-2629\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/dlt-template-rejected-reasons-and-fixes.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/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\" data-type=\"internal\" data-id=\"#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=\"#two-different-failures\">Two Different Failures That Both Look Like Rejection<\/a><\/li>\n\n\n\n<li><a href=\"#master-table\">The Master Rejection Reason Table<\/a><\/li>\n\n\n\n<li><a href=\"#variables\">Variables: The Single Largest Cause of Rejection<\/a><\/li>\n\n\n\n<li><a href=\"#category\">Category Misclassification<\/a><\/li>\n\n\n\n<li><a href=\"#header-mismatch\">Header and Sender ID Mismatch<\/a><\/li>\n\n\n\n<li><a href=\"#urls\">URLs, Shortlinks and Whitelisting<\/a><\/li>\n\n\n\n<li><a href=\"#characters\">Special Characters, Unicode and Emoji<\/a><\/li>\n\n\n\n<li><a href=\"#length\">Length, Segments and Why Rejection Is Also a Cost Problem<\/a><\/li>\n\n\n\n<li><a href=\"#duplicates\">Duplicate and Near-Duplicate Templates<\/a><\/li>\n\n\n\n<li><a href=\"#language\">Language and Script Registration<\/a><\/li>\n\n\n\n<li><a href=\"#consent\">Consent Templates Are Not Content Templates<\/a><\/li>\n\n\n\n<li><a href=\"#mismatch-188\">Approved but Mismatched: Status Code 188<\/a><\/li>\n\n\n\n<li><a href=\"#programmatic-create\" data-type=\"internal\" data-id=\"#programmatic-create\">Creating Templates Programmatically<\/a><\/li>\n\n\n\n<li><a href=\"#pre-send-validation\">Pre-Send Validation: Catching Mismatch Before the Operator Does<\/a><\/li>\n\n\n\n<li><a href=\"#rendering\">Rendering DLT Variables Without Corrupting Them<\/a><\/li>\n\n\n\n<li><a href=\"#debug-table\">Symptom to Cause to Fix: Debug Table<\/a><\/li>\n\n\n\n<li><a href=\"#resubmit\">How to Resubmit a Rejected Template Without Getting Rejected Again<\/a><\/li>\n\n\n\n<li><a href=\"#registry\">Building a Template Registry You Can Actually Audit<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Pre-Submission Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#mistakes\">Ten Mistakes That Keep Costing Teams Approvals<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\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\">A DLT content template is rejected when its declared structure does not match its actual content: the wrong category for the message intent, variables used as content rather than as placeholders, a header that is not bound to the entity submitting it, an unwhitelisted URL or callback number, characters the portal will not accept, or a body that already exists under another template ID. Every one of these is a rule check, not a judgement call, which means every one of them has a deterministic fix. Fix the rule and resubmit. There is no penalty for resubmission and no cap on attempts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rejection you can see on the portal is the easy one. The expensive one is the template that gets approved and then still fails at send time, because your outgoing message no longer matches the approved body character for character. That failure surfaces as <code>statusCode<\/code> <code>188<\/code>, <code>message template mismatch<\/code>, and it is preventable with a single API call in your deployment pipeline.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\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>Registration rejection and send-time mismatch are two different failures. Registration rejection happens on the DLT portal before any message exists. Mismatch happens on an approved template at scrubbing time. Fixing one does not fix the other.<\/li>\n\n\n\n<li>Variable misuse causes more rejections than everything else combined. Use <code>{#var#}<\/code> exactly, never a variant, never as the entire message, never back to back, and never to carry content that changes the meaning or category of the message.<\/li>\n\n\n\n<li>Category and content must agree. Promotional wording inside a service or transactional template is rejected on sight, and it is the most common category failure.<\/li>\n\n\n\n<li>The header you select must already be approved and bound to the same principal entity. A header approved for a different entity or in a different category will not attach.<\/li>\n\n\n\n<li>Punctuation, spacing and case matter at scrubbing time even when they were accepted at registration. Validate the rendered message, not the template string, before you send.<\/li>\n\n\n\n<li>Validate with <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/validateTemplate<\/code> in CI, not in production. A mismatch caught in a pull request costs nothing. A mismatch caught in production costs a failed campaign and, since billing fires at submission, it can still cost credits.<\/li>\n\n\n\n<li>Templates can be created programmatically with <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/create<\/code>, which returns <code>Template created successfully and given for approval<\/code>. Approval still happens on the portal side.<\/li>\n\n\n\n<li>A template that is 11 characters over a segment boundary costs a second credit on every single message until you re-register a shorter one. Rejection is a compliance problem. Template length is a billing problem. Solve both at design time.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"two-different-failures\" class=\"wp-block-heading\">Two Different Failures That Both Look Like Rejection<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures-1024x584.webp\" alt=\"Diagram about Two Different Failures That Both Look Like Rejection\" class=\"wp-image-2630\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/two-different-failures.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Teams conflate these constantly, and the conflation wastes days. They are structurally different events at different points in the message lifecycle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Failure A: registration rejection.<\/strong> You submit a content template on your operator&#8217;s DLT portal. A reviewer or an automated rule check evaluates it against the TCCCPR framework and the portal&#8217;s own content policy. It comes back rejected with a reason string. No message has been sent. No credits have moved. Nothing is broken in your code. The fix is entirely in the template text and its declared metadata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Failure B: send-time mismatch.<\/strong> You have an approved template and a valid <code>dltTemplateId<\/code>. You call the send API. The message reaches the operator&#8217;s scrubbing layer, which compares the rendered message body against the registered template associated with that ID. If they do not reconcile, the message is dropped and you get a compliance failure rather than a delivery failure. In SMSGatewayCenter&#8217;s delivery reporting this presents as a template check failure, and in the pre-send validation API it presents as <code>statusCode<\/code> <code>188<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The distinction matters because the diagnostics differ. For Failure A, the portal tells you why. For Failure B, the portal has already said yes, so the only signal you get is a rejected message and a status code. You have to reconstruct what your code actually sent, which is why logging the fully rendered message body, not just the template ID and the variable array, is a hard requirement in any India-facing messaging stack.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rest of this guide handles Failure A first, because a clean template registration removes most of the surface area for Failure B.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"master-table\" class=\"wp-block-heading\">The Master Rejection Reason Table<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the centrepiece. Find your rejection reason in the left column, apply the fix in the right column, resubmit.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>#<\/th><th>Rejection reason<\/th><th>What the portal is actually checking<\/th><th>The fix<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Invalid variable format<\/td><td>The placeholder token does not match the accepted <code>{#var#}<\/code> form<\/td><td>Replace every placeholder with the literal string <code>{#var#}<\/code>. No numbering, no naming, no spaces inside the braces. <code>{#var1#}<\/code>, <code>{# var #}<\/code>, <code>{{var}}<\/code>, <code>%s<\/code>, <code>${name}<\/code> and <code>[name]<\/code> are all rejected.<\/td><\/tr><tr><td>2<\/td><td>Excessive variables<\/td><td>The ratio of variable placeholders to fixed text is high enough that the template could carry arbitrary content<\/td><td>Reduce variables to the values that genuinely change per recipient. Move anything static into fixed text. If a variable exists only to carry a product name that never changes, hardcode it.<\/td><\/tr><tr><td>3<\/td><td>Variable-only or near variable-only content<\/td><td>The template has no meaningful fixed content, so it cannot be scrubbed against<\/td><td>Add real fixed wording that establishes the message purpose. A template that reads <code>{#var#} {#var#} {#var#}<\/code> is a blank cheque and is always rejected.<\/td><\/tr><tr><td>4<\/td><td>Adjacent variables<\/td><td>Two placeholders sit next to each other with no separating fixed text<\/td><td>Insert fixed text between them, even a single word or a punctuation-plus-space separator. Adjacent variables make the boundary between values undecidable at scrubbing time.<\/td><\/tr><tr><td>5<\/td><td>Variable at the very start or very end<\/td><td>The scrubber cannot anchor the template if the boundary is a wildcard<\/td><td>Anchor both ends with fixed text. Start with your brand or the message purpose, end with your brand or a closing phrase.<\/td><\/tr><tr><td>6<\/td><td>Promotional content in a Service or Transactional category<\/td><td>Marketing language, offers, discounts, calls to action or incentives inside a non-promotional category<\/td><td>Either strip the promotional language and keep the category, or move the template to the Promotional category and accept the promotional sending window and DND filtering that comes with it.<\/td><\/tr><tr><td>7<\/td><td>Category mismatch, Service Implicit versus Service Explicit<\/td><td>The message content implies a consent basis that does not match the declared category<\/td><td>Map intent to category properly. See the <a href=\"#category\">category section<\/a> below. A shipping update is Service Implicit. A curated recommendation to an opted-in customer is Service Explicit.<\/td><\/tr><tr><td>8<\/td><td>Transactional category misuse<\/td><td>Transactional is reserved for a narrow class of one-time-password and account-critical traffic for registered financial entities<\/td><td>Do not register order confirmations or delivery alerts as Transactional. Use Service Implicit. If you genuinely need Transactional, confirm your entity type is eligible on the portal before submitting.<\/td><\/tr><tr><td>9<\/td><td>Header not approved or not bound<\/td><td>The selected sender ID is not approved for this principal entity, or not approved for this category<\/td><td>Register and get the header approved first, in the same category. See <a href=\"#header-mismatch\" data-type=\"internal\" data-id=\"#header-mismatch\">Header and Sender ID Mismatch<\/a>.<\/td><\/tr><tr><td>10<\/td><td>Header mismatch with content<\/td><td>The header is a brand identity that does not appear anywhere in the fixed text<\/td><td>Include your brand name in the fixed content of the template. This is the cheapest fix in the entire list and it prevents a surprising number of rejections.<\/td><\/tr><tr><td>11<\/td><td>Unregistered or unwhitelisted URL<\/td><td>The template contains a link whose domain has not been submitted for whitelisting<\/td><td>Whitelist the exact domain, including the shortlink domain if you use one, before submitting a template that contains it.<\/td><\/tr><tr><td>12<\/td><td>Shortener domain in template<\/td><td>Public URL shorteners are generally not accepted because the destination is unverifiable<\/td><td>Use a branded short domain you control and whitelist it. Do not use a public shortener.<\/td><\/tr><tr><td>13<\/td><td>URL inside a variable<\/td><td>The template hides a link behind a placeholder, defeating URL review<\/td><td>Put the domain in fixed text and only the path or token in the variable, for example <code>example.com\/t\/{#var#}<\/code>.<\/td><\/tr><tr><td>14<\/td><td>Unregistered callback number<\/td><td>A phone number appears in the template but is not on the entity&#8217;s approved list<\/td><td>Register the number on the portal first, or remove it and direct users to a registered support channel.<\/td><\/tr><tr><td>15<\/td><td>Disallowed special characters<\/td><td>Characters or symbols the portal does not accept in template bodies<\/td><td>Remove emoji and decorative symbols. Keep punctuation minimal and conventional. See <a href=\"#characters\" data-type=\"internal\" data-id=\"#characters\">Special Characters<\/a>.<\/td><\/tr><tr><td>16<\/td><td>Emoji in template<\/td><td>Emoji are frequently blocked outright and also force the message into Unicode encoding<\/td><td>Remove them. If you truly need them, verify acceptance for your category on your portal before designing around them.<\/td><\/tr><tr><td>17<\/td><td>Spelling and grammar errors<\/td><td>Reviewers reject templates with obvious errors because the template becomes the permanent, immutable message body<\/td><td>Proofread before submission. You cannot edit an approved template; you can only register a new one and migrate.<\/td><\/tr><tr><td>18<\/td><td>Duplicate template<\/td><td>An identical or near-identical body already exists under another template ID for the same entity<\/td><td>Search your existing approved templates first. If a duplicate exists, reuse its ID instead of registering a new one. See <a href=\"#duplicates\" data-type=\"internal\" data-id=\"#duplicates\">Duplicates<\/a>.<\/td><\/tr><tr><td>19<\/td><td>Language or script not registered<\/td><td>The template body is in a language or script not enabled for the entity, or mixes scripts unexpectedly<\/td><td>Register each language variant as its own template. Do not assume a single template covers a transliterated version.<\/td><\/tr><tr><td>20<\/td><td>Mixed script within one template<\/td><td>Latin and a regional script interleaved in a way the portal does not accept<\/td><td>Split into two templates, one per script, each with its own ID.<\/td><\/tr><tr><td>21<\/td><td>Excessive length<\/td><td>The body exceeds the portal&#8217;s accepted character ceiling for a single template<\/td><td>Shorten. Long bodies are also a per-message billing cost. See <a href=\"#length\" data-type=\"internal\" data-id=\"#length\">Length and Segments<\/a>.<\/td><\/tr><tr><td>22<\/td><td>Consent scope contains variables<\/td><td>Variables were used inside a consent template, where they are not applicable<\/td><td>Remove all variables from consent templates. Variables belong to content templates only. See <a href=\"#consent\" data-type=\"internal\" data-id=\"#consent\">Consent Templates<\/a>.<\/td><\/tr><tr><td>23<\/td><td>Content does not match declared entity business<\/td><td>The template describes a service outside the entity&#8217;s registered business activity<\/td><td>Align the template with your registered business scope, or update your entity record before resubmitting.<\/td><\/tr><tr><td>24<\/td><td>Prohibited or restricted content<\/td><td>Content in a category the regulator restricts<\/td><td>Do not resubmit variations. Restructure the offering or the message purpose entirely.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If your rejection reason string does not map cleanly onto a row above, it is almost always a variant of row 1, 6 or 9. Check those three first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"variables\" class=\"wp-block-heading\">Variables: The Single Largest Cause of Rejection<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you fix nothing else in this article, fix your variables. Across every DLT support queue this is the dominant failure mode, and it fails for reasons that have nothing to do with your wording.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The exact token<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The accepted placeholder is the literal seven-character string <code>{#var#}<\/code>. That is an opening brace, a hash, the lowercase letters <code>var<\/code>, a hash, a closing brace. Every deviation is a rejection:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{#var#}     correct\n{#VAR#}     rejected, case\n{#var1#}    rejected, numbering is not part of the token\n{# var #}   rejected, whitespace inside the braces\n{#var}      rejected, missing closing hash\n{{var}}     rejected, wrong syntax entirely\n{name}      rejected\n%s          rejected\n${amount}   rejected\n&#91;OTP]       rejected\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Variables are positional. The first <code>{#var#}<\/code> in the template maps to your first value, the second to your second, and so on. There is no name binding and there is no reordering. This has direct consequences for how you render them in code, covered in <a href=\"#rendering\">Rendering DLT Variables<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The character budget per variable<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Portal convention treats one variable as consuming a fixed allowance of thirty characters when the template&#8217;s total length is computed at registration. SMSGatewayCenter&#8217;s own <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-sms-message-content-template-registration-guide\/\">DLT SMS message content template registration guide<\/a> documents this behaviour on the Airtel portal: you choose the number of variables based on the message length you need, because each one consumes budget whether or not you fill it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two practical consequences follow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, a template with six variables has already spent a large chunk of its allowance before you write a single word of fixed text. If your template is being rejected for length and you cannot see why, count the variables.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Second, and more expensively, the thirty-character allowance is a registration-time accounting figure and has nothing to do with what you are billed at send time. Billing is computed on the actual rendered message. A variable that resolves to a six-character order ID does not cost thirty characters of credit. But it did cost you thirty characters of registration budget, which may have forced you to shorten fixed text you actually needed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why variable-heavy templates get rejected<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The regulator&#8217;s concern is straightforward: a template that is mostly placeholders is not a template, it is an open channel. The scrubbing layer exists to verify that the message a subscriber receives matches something a principal entity registered and took responsibility for. If eighty percent of the body is variable, that verification is meaningless.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the rules that follow are all versions of the same principle:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No variable-only templates.<\/strong> <code>{#var#}<\/code> alone, or <code>{#var#} {#var#}<\/code>, is always rejected.<\/li>\n\n\n\n<li><strong>No adjacent variables.<\/strong> <code>{#var#}{#var#}<\/code> and <code>{#var#} {#var#}<\/code> are both problematic. Put fixed text between them. <code>Order {#var#} for {#var#} is confirmed<\/code> is fine. <code>{#var#} {#var#} is confirmed<\/code> is not.<\/li>\n\n\n\n<li><strong>Anchor both ends.<\/strong> Starting or ending the template with a variable leaves the scrubber without a fixed boundary to match against. Lead with your brand or the message purpose and close with your brand.<\/li>\n\n\n\n<li><strong>Do not let a variable change the message category.<\/strong> If a variable could plausibly carry a promotional offer inside a service template, the template is doing something the category does not permit, regardless of what you actually put in it in production.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Good and bad variable structure, side by side<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Rejected<\/th><th>Why<\/th><th>Approved version<\/th><\/tr><\/thead><tbody><tr><td><code>{#var#}<\/code><\/td><td>Variable only, no fixed content<\/td><td><code>Your ACME verification code is {#var#}. Do not share it with anyone. ACME<\/code><\/td><\/tr><tr><td><code>Hi {#var#} {#var#}<\/code><\/td><td>Adjacent variables, no anchor, no purpose<\/td><td><code>Hi {#var#}, your ACME order {#var#} has been confirmed. ACME<\/code><\/td><\/tr><tr><td><code>{#var#} is your OTP<\/code><\/td><td>Leading variable<\/td><td><code>Your ACME login OTP is {#var#}. Valid for 10 minutes. ACME<\/code><\/td><\/tr><tr><td><code>Dear customer, {#var#}<\/code><\/td><td>Trailing variable carries the entire payload<\/td><td><code>Dear customer, your ACME statement for {#var#} is now available in the app. ACME<\/code><\/td><\/tr><tr><td><code>Order update: {#var#}<\/code><\/td><td>The variable carries the whole message body<\/td><td><code>Order update: your ACME order {#var#} has shipped and will arrive by {#var#}. Track it in the ACME app. ACME<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note what the approved column has in common. Fixed text at both ends, the brand name present in the body, and each variable carrying exactly one data value with fixed text describing what that value means.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"category\" class=\"wp-block-heading\">Category Misclassification<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The second largest cause. Four categories exist, and they are not interchangeable. Choosing the wrong one gets the template rejected at registration, and choosing a category your content does not support gets it rejected even when the wording looks harmless to you.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Category<\/th><th>Consent basis<\/th><th>What belongs here<\/th><th>Sending window<\/th><th>DND treatment<\/th><\/tr><\/thead><tbody><tr><td><strong>Promotional<\/strong><\/td><td>No prior relationship required, but recipients on the preference registry are filtered<\/td><td>Offers, discounts, campaigns, product announcements, anything with a commercial call to action<\/td><td>Restricted daytime window<\/td><td>Filtered against the customer preference registry<\/td><\/tr><tr><td><strong>Service Implicit<\/strong><\/td><td>An existing customer relationship creates implied consent<\/td><td>Order confirmations, shipping updates, appointment reminders, payment receipts, account alerts, service notifications<\/td><td>Full day<\/td><td>Delivered to registry-listed numbers<\/td><\/tr><tr><td><strong>Service Explicit<\/strong><\/td><td>Recipient has given explicit, recorded consent through a registered consent template<\/td><td>Content that goes beyond the immediate transaction: recommendations, curated updates, opted-in newsletters, offers to consented customers<\/td><td>Full day<\/td><td>Delivered to consented recipients<\/td><\/tr><tr><td><strong>Transactional<\/strong><\/td><td>Narrow, entity-restricted<\/td><td>One-time passwords and account-critical traffic for eligible registered entities such as banks and financial institutions<\/td><td>Full day<\/td><td>Delivered<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">SMSGatewayCenter maintains dedicated explainers on the two service categories that are worth reading before you classify anything: <a href=\"https:\/\/www.smsgatewaycenter.com\/service-implicit-sms\/\">Service Implicit SMS<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/service-explicit-sms\/\">Service Explicit SMS<\/a>. The distinction between them is a consent-record distinction, not a content distinction, and that is the part teams get wrong.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The three category mistakes that cause almost all category rejections<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One: promotional wording inside a service template.<\/strong> This is the big one. A shipping notification that ends with <code>Shop more at acme.com and get 10% off<\/code> is a promotional message wearing a service template&#8217;s clothes. The presence of any incentive, discount, offer, upsell or campaign call to action moves the message into Promotional regardless of what the rest of the body says. Strip it or reclassify.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Watch for the softer versions too. <code>Explore our new range<\/code>, <code>Refer a friend<\/code>, <code>Limited time<\/code>, <code>Download now to get<\/code>, and even an unqualified <code>Visit our website<\/code> can trip category review inside a service template.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two: registering everything as Transactional because it delivers to DND numbers.<\/strong> Teams discover that Transactional bypasses preference filtering and try to route everything through it. Transactional is entity-restricted and content-restricted. Order confirmations and delivery updates are Service Implicit, and Service Implicit already delivers to registry-listed numbers, so the workaround is solving a problem you do not have. If you are unsure where OTP traffic belongs, SMSGatewayCenter answers this directly in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/under-which-category-should-i-register-my-otp-templates-on-the-dlt-portal\/\">Under Which Category Should I Register My OTP Templates on the DLT Portal?<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three: Service Explicit without a registered consent template.<\/strong> Service Explicit requires a consent artefact. If you register Service Explicit content without having a corresponding approved consent template and a real consent record, you will either be rejected at registration or fail later at the consent check during scrubbing. SMSGatewayCenter documents that failure mode in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-consent_failed-consent-fail-by-dlt-mean\/\">What does CONSENT_FAILED mean?<\/a>, and the registration side in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/how-register-consent-template-dlt-portals\/\">How to Register Consent Template on DLT Portals<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A decision matrix for classifying a message<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Question<\/th><th>Yes<\/th><th>No<\/th><\/tr><\/thead><tbody><tr><td>Does the message contain any offer, discount, incentive or commercial call to action?<\/td><td>Promotional<\/td><td>Continue<\/td><\/tr><tr><td>Is it an OTP or account-critical authentication message for an eligible financial entity?<\/td><td>Transactional<\/td><td>Continue<\/td><\/tr><tr><td>Is the message a direct consequence of a transaction or relationship the recipient initiated?<\/td><td>Service Implicit<\/td><td>Continue<\/td><\/tr><tr><td>Do you hold a recorded, explicit consent against a registered consent template for this recipient and this content type?<\/td><td>Service Explicit<\/td><td>Reclassify as Promotional or do not send<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Run every template through this matrix before you touch the portal. It takes thirty seconds and it removes the most common rejection cause after variables.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"header-mismatch\" class=\"wp-block-heading\">Header and Sender ID Mismatch<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A header, also called the sender ID, is a separate registered artefact from a template. Templates attach to headers. If the attachment is invalid, the template is rejected even when its text is perfect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four things must line up:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>The header must already be approved.<\/strong> You cannot submit a template against a header that is still pending. Register the header, wait for approval, then register templates against it.<\/li>\n\n\n\n<li><strong>The header must belong to the same principal entity.<\/strong> A header approved under a different entity ID will not attach, and this bites hard during company restructures and brand acquisitions.<\/li>\n\n\n\n<li><strong>The header category must match the template category.<\/strong> A header approved for Promotional traffic cannot carry Service Implicit templates. This is a frequent surprise for teams that registered a single header early on and later expanded into a second category.<\/li>\n\n\n\n<li><strong>The header must be bound to your telemarketer chain.<\/strong> In India, the principal entity to telemarketer binding governs which aggregator may actually deliver traffic on your behalf. If that chain is broken, the template may register fine and then fail at delivery. SMSGatewayCenter explains this in <a href=\"https:\/\/www.smsgatewaycenter.com\/pe-tm-binding-chain-process-in-dlt\/\">PE-TM Binding Chain Process in DLT<\/a>, and it is the first thing to check when messages that worked last month stop working after a provider change.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Indian alphanumeric headers are six characters. Register them through the portal, or programmatically:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST https:\/\/unify.smsgateway.center\/SMSApi\/senderid\/create \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -d \"senderid=ACMEIN\" \\\n  -d \"output=json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Success response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"senderid\",\n    \"action\": \"create\",\n    \"status\": \"success\",\n    \"msg\": \"SenderId created successfully.\",\n    \"code\": \"200\"\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That call submits the header. Approval still happens through the DLT and operator process. Full parameter documentation lives at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/create-sender-id\/\">Create Sender ID<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two rejection causes on the delivery side are worth knowing because they look like template problems and are not: <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-blocked_senderid-mean\/\">blocked sender ID<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-invalid_senderid-invalid-senderid-mean\/\">invalid sender ID<\/a>. If your template ID is correct and your body matches but messages still fail, check the header before you rewrite the template.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Put your brand in the body<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A small, high-yield habit: include the brand name that your header represents inside the fixed text of every template. It resolves header-content mismatch reviews, it improves recipient trust, and it costs you six characters. Templates whose body never names the sending brand attract reviewer scrutiny for no benefit.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"urls\" class=\"wp-block-heading\">URLs, Shortlinks and Whitelisting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Links are the third most common rejection cause and the one with the most avoidable failure mode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong> any domain that appears in a template body must be whitelisted for your entity before the template is submitted. Not after. If you submit a template containing an unwhitelisted domain, it is rejected, and whitelisting the domain afterwards does not retroactively approve the template. You resubmit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Public shorteners are effectively unusable.<\/strong> The whole point of review is that a human or a rule can inspect the destination. A public shortener hides it. Use a branded short domain you own, whitelist it once, and route all campaign links through it. SMSGatewayCenter&#8217;s <a href=\"https:\/\/www.smsgatewaycenter.com\/smart-link-tracking\/\">smart link tracking<\/a> exists for exactly this reason: it gives you click attribution without a third-party shortener domain in the body.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not hide the URL inside a variable.<\/strong> Templates like <code>Click {#var#} to track your order<\/code> are rejected because the reviewable content is a wildcard. Split it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Rejected:  Track your ACME order here: {#var#} ACME\nApproved:  Track your ACME order at acme.in\/t\/{#var#} ACME\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The domain sits in fixed text where it can be reviewed. Only the opaque token varies. This also makes your scrubbing behaviour more predictable, because the fixed portion of the URL becomes part of the template match.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Callback numbers are treated the same way.<\/strong> A phone number in a template body must be a number registered against your entity. An unregistered support number in the fixed text is a rejection cause that reads like a content rejection and is actually a registration gap.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"characters\" class=\"wp-block-heading\">Special Characters, Unicode and Emoji<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This section is short on rules and long on traps.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The rules<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Emoji are generally not accepted in DLT template bodies. Decorative symbols, box-drawing characters, arrows and unusual punctuation attract rejection. Keep the body to letters, digits, spaces and conventional punctuation. If you need a currency symbol, verify its acceptance before designing a template around it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The trap that costs teams weeks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Word processors and rich text editors silently substitute characters. If you draft a template in a document editor and paste it into the DLT portal, you may be submitting characters you did not type:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>What you typed<\/th><th>What the editor substituted<\/th><th>Codepoint<\/th><th>Consequence<\/th><\/tr><\/thead><tbody><tr><td><code>'<\/code> apostrophe<\/td><td><code>'<\/code> right single quotation mark<\/td><td>U+2019<\/td><td>Forces Unicode encoding, may be rejected<\/td><\/tr><tr><td><code>-<\/code> hyphen<\/td><td><code>\u2013<\/code> en dash<\/td><td>U+2013<\/td><td>Forces Unicode encoding<\/td><\/tr><tr><td><code>...<\/code> three dots<\/td><td><code>\u2026<\/code> ellipsis<\/td><td>U+2026<\/td><td>Forces Unicode encoding<\/td><\/tr><tr><td><code>\"<\/code> quote<\/td><td><code>\"<\/code> <code>\"<\/code> curly quotes<\/td><td>U+201C, U+201D<\/td><td>Forces Unicode encoding<\/td><\/tr><tr><td><code>Rs<\/code><\/td><td><code>\u20b9<\/code> rupee sign<\/td><td>U+20B9<\/td><td>Forces Unicode encoding<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The rejection consequence is bad enough. The billing consequence is worse and it is silent. A single U+2019 apostrophe drops your entire message out of the GSM-7 alphabet and into Unicode, which cuts your per-segment capacity from 160 characters to 70. A 150-character message that cost one credit now costs three. SMSGatewayCenter&#8217;s <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/text-messages-chart\/\">text messages chart<\/a> documents the mapping: English 160 characters per segment and 153 when concatenated, Unicode 70 per segment and 67 when concatenated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The fix is procedural, not technical.<\/strong> Draft templates in a plain text editor. Never paste from a document editor or an email client into the portal. If you must, run the text through a codepoint check first:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Flag any character outside the basic printable ASCII range before submitting.\ndef audit_template(text: str) -&gt; list&#91;tuple&#91;int, str, str]]:\n    findings = &#91;]\n    for index, ch in enumerate(text):\n        cp = ord(ch)\n        if cp &lt; 0x20 or cp &gt; 0x7E:\n            findings.append((index, ch, f\"U+{cp:04X}\"))\n    return findings\n\n\ntpl = \"Your ACME code is {#var#}. Don't share it. ACME\"\nfor index, ch, cp in audit_template(tpl):\n    print(f\"position {index}: {ch!r} is {cp}\")\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run that on every template before submission and again on every rendered message before sending. It catches the substitution class of bugs in a single pass.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can also confirm the encoding decision against the platform itself rather than guessing, using the message length and cost endpoint:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST https:\/\/unify.smsgateway.center\/SMSApi\/info\/msg \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -d \"msg=Your ACME code is 483920. Do not share it. ACME\" \\\n  -d \"output=json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"info\",\n    \"action\": \"msg\",\n    \"status\": \"success\",\n    \"msg\": \"success\",\n    \"code\": \"200\",\n    \"count\": 1,\n    \"msgList\": {\n      \"encoding\": \"TEXT\",\n      \"length\": 11,\n      \"remaining\": 149,\n      \"credit\": 1\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If <code>encoding<\/code> comes back as something other than <code>TEXT<\/code> for a template you believe is plain English, you have a substituted character. Find it with the audit function above. This endpoint is documented at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-sms-length-cost\/\">Get SMS Length and Cost<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"length\" class=\"wp-block-heading\">Length, Segments and Why Rejection Is Also a Cost Problem<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rejection for excessive length is straightforward: shorten the template. The more interesting problem is the template that is approved at a length you will regret.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An approved template is immutable. You cannot edit it. If your approved body renders to 171 characters in production, every message costs two credits instead of one, forever, until you register a replacement template and migrate your code to the new ID. Eleven characters over the boundary is a permanent fifty percent increase in per-message cost on that flow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is why template length belongs in the design review, not the compliance review. Before you submit a template, render it with realistic maximum-length variable values and price it:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Flow<\/th><th>Template fixed text<\/th><th>Max variable payload<\/th><th>Rendered max<\/th><th>Credits<\/th><th>Annual cost impact at 2M messages<\/th><\/tr><\/thead><tbody><tr><td>OTP<\/td><td>46 chars<\/td><td>6<\/td><td>52<\/td><td>1<\/td><td>Baseline<\/td><\/tr><tr><td>Order confirmed<\/td><td>118 chars<\/td><td>24<\/td><td>142<\/td><td>1<\/td><td>Baseline<\/td><\/tr><tr><td>Shipping update<\/td><td>149 chars<\/td><td>28<\/td><td>177<\/td><td>2<\/td><td>Double<\/td><\/tr><tr><td>Payment receipt<\/td><td>133 chars<\/td><td>22<\/td><td>155<\/td><td>1<\/td><td>Baseline, 5 chars of headroom<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Row three is the problem and row four is the near miss. The shipping update needs eighteen characters removed from its fixed text before submission. The payment receipt is fine today and will break the first time a variable carries a longer value than you tested with. Give every template at least fifteen characters of headroom under its boundary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The mechanics of how per-message cost is actually computed, including the encoding cliff and how retry policy amplifies spend, are covered in depth in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/bulk-sms-pricing-in-india-what-actually-drives-cost\/\">Bulk SMS Pricing in India: What Actually Drives Cost<\/a>. The short version relevant here: billing fires at submission to the operator, not at delivery, so a template that produces two segments bills two credits whether or not the recipient ever sees the message.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Price the template, not the message<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Call the length and cost endpoint at template save time, in your template registry, with the longest realistic variable values substituted in. Not at send time. At send time you have already committed. Wiring this into your template management tooling turns a recurring billing surprise into a design-time check.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"duplicates\" class=\"wp-block-heading\">Duplicate and Near-Duplicate Templates<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Portals reject templates whose body already exists under another approved ID for the same entity. This frustrates teams because the duplicate is usually accidental: two product squads registered the same order confirmation independently, or someone resubmitted after assuming an earlier submission had failed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Before submitting, search your existing approved templates.<\/strong> If a matching body exists, reuse its ID. Registering a second ID for identical content gives you nothing and creates a governance problem: two IDs for one message means your reconciliation reports split a single flow across two rows.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Near-duplicate rejection is subtler. Two templates that differ only in a word or in punctuation may be treated as duplicates. If you need genuine variants, make them meaningfully different in fixed content, not cosmetically different.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is a related delivery-side failure that is entirely unrelated to template duplication and gets confused with it: duplicate message suppression at send time. That is a platform feature, not a DLT rule, and it surfaces separately. SMSGatewayCenter documents it in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-duplicate_fail-duplicate-fail-mean\/\">What does DUPLICATE_FAIL mean?<\/a>. If you see duplicate failures on messages whose templates registered fine, you are looking at that, not at a template duplication rejection.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"language\" class=\"wp-block-heading\">Language and Script Registration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each language variant is its own template with its own ID. There is no inheritance and no automatic transliteration coverage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three specific failures:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Transliteration is not the same language.<\/strong> A Hindi message written in Devanagari and the same message written in Latin script are two different templates requiring two registrations. Teams routinely register the Devanagari version, then send the Latin transliteration under that ID, and get a mismatch at scrubbing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mixed script within one template.<\/strong> Interleaving Latin and a regional script in a single body is frequently rejected. If your message genuinely needs both, for example an English brand name inside a Hindi sentence, verify acceptance for your entity and category rather than assuming.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Regional-language templates are Unicode.<\/strong> A Devanagari, Tamil, Bengali or Gujarati template is encoded as Unicode, which means 70 characters per segment. A regional template of the same word count as its English counterpart will typically cost more credits per message. Budget for this at design time. In your send call this corresponds to <code>msgType=unicode<\/code> on <code>SMSApi\/send<\/code> rather than <code>msgType=text<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"consent\" class=\"wp-block-heading\">Consent Templates Are Not Content Templates<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A consent template records the wording by which a subscriber granted explicit consent. It is a different artefact from a content template, and applying content template rules to it causes rejection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rule that catches most teams: <strong>variables are not applicable in consent templates.<\/strong> No <code>{#var#}<\/code> anywhere in the consent scope. A consent template is a fixed statement of what the subscriber agreed to. If it contains placeholders, the consent is unbounded and therefore not consent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consent templates matter operationally because Service Explicit traffic depends on them. Register the consent template, capture and store real consent records against it, and only then register Service Explicit content templates. Doing it in the other order produces a template that registers and then fails at the consent check during scrubbing, which is a much harder failure to diagnose. Registration mechanics are covered in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/how-register-consent-template-dlt-portals\/\">How to Register Consent Template on DLT Portals<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"mismatch-188\" class=\"wp-block-heading\">Approved but Mismatched: Status Code 188<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Now the second failure mode. Your template is approved. Your <code>dltTemplateId<\/code> is correct. Your messages are still failing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At scrubbing, the operator compares the rendered message body against the registered template for that ID. The comparison is a structural match: fixed text must match, variable positions must be occupied by something, and the whole thing must reconcile. If it does not, the message is dropped as a compliance failure. SMSGatewayCenter&#8217;s pre-send validation API reports this as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"error\",\n  \"statusCode\": \"188\",\n  \"reason\": \"message template mismatch\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and the corresponding delivery-side status is documented in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-template_check_failed-template-mismatch-mean\/\">What does TEMPLATE_CHECK_FAILED mean?<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The causes, in order of how often they actually happen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. Someone edited the copy without re-registering.<\/strong> A product manager changed <code>Your order has shipped<\/code> to <code>Your order is on its way<\/code> in a config file. The template ID did not change. Everything now fails. This is the single most common cause and the reason your template strings must live in version control with a code review gate, not in an editable admin panel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. Whitespace drift.<\/strong> A trailing space, a double space between words, a line break where the template has none, or a missing line break where it has one. Invisible in a diff viewer, fatal at scrubbing. Normalise aggressively and assert on the normalised form in tests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. Punctuation normalisation differences.<\/strong> The validation endpoint returns a <code>template<\/code> field containing a punctuation-normalized version of the registered template. Compare your rendered output against that normalised form, not against what you think you registered. Trailing periods, the presence or absence of a full stop before the brand sign-off, and hyphen versus en dash are the usual culprits.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>4. A variable that resolved empty.<\/strong> If your renderer substituted an empty string, the rendered body now has fixed text running together where a value should be. Structurally that is a different message. Validate that every variable has a non-empty value before rendering, and fail loudly rather than sending a message with a hole in it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5. A variable value that contains template-like text.<\/strong> A customer whose name is literally <code>{#var#}<\/code>, or an order note containing braces, can shift the parse. Rare, but it happens with user-generated content, and it is a real argument for sanitising variable values.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>6. Wrong template ID for the right message.<\/strong> Two similar flows, two similar templates, the IDs got swapped in a config file. The body is valid, just not for that ID.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>7. Encoding drift.<\/strong> The template was registered with a plain apostrophe. Your code emits U+2019 because the copy came from a document. Character for character these are different strings.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"programmatic-create\" class=\"wp-block-heading\">Creating Templates Programmatically<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You do not have to submit every template by hand. SMSGatewayCenter exposes template creation over the API, documented at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/create-message-template\/\">Create Message Template<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/create \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -d \"message=Your ACME verification code is {#var#}. Valid for 10 minutes. Do not share it. ACME\" \\\n  -d \"output=json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Response:<\/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}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Read that response message carefully. <code>given for approval<\/code> is not <code>approved<\/code>. The API call submits the template into the approval workflow. It does not bypass review, and nothing about programmatic submission makes rejection less likely. Everything in the rejection table above still applies.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What programmatic creation buys you is reproducibility. Template bodies live in your repository, get reviewed in pull requests, and get submitted by a script rather than retyped into a web form by whoever is available. Retyping is where whitespace drift and character substitution enter the system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal submission script that audits before it submits:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\nimport httpx\n\nAPI_BASE = \"https:\/\/unify.smsgateway.center\"\nTOKEN = re.compile(r\"\\{#var#\\}\")\n\n\nclass TemplateError(Exception):\n    pass\n\n\ndef audit(body: str) -&gt; None:\n    \"\"\"Fail fast on the rejection causes we can detect locally.\"\"\"\n    if not body.strip():\n        raise TemplateError(\"empty template\")\n\n    for index, ch in enumerate(body):\n        if ord(ch) &lt; 0x20 or ord(ch) &gt; 0x7E:\n            raise TemplateError(\n                f\"non-ASCII character {ch!r} (U+{ord(ch):04X}) at position {index}; \"\n                \"this will force Unicode encoding and may be rejected\"\n            )\n\n    stripped = TOKEN.sub(\"\", body).strip()\n    if not stripped:\n        raise TemplateError(\"template is variables only\")\n\n    if re.search(r\"\\{#var#\\}\\s*\\{#var#\\}\", body):\n        raise TemplateError(\"adjacent variables with no fixed text between them\")\n\n    if body.strip().startswith(\"{#var#}\"):\n        raise TemplateError(\"template starts with a variable; anchor it with fixed text\")\n\n    if body.strip().endswith(\"{#var#}\"):\n        raise TemplateError(\"template ends with a variable; anchor it with fixed text\")\n\n    if \"  \" in body:\n        raise TemplateError(\"double space; normalise whitespace before submitting\")\n\n    if body != body.strip():\n        raise TemplateError(\"leading or trailing whitespace\")\n\n\ndef submit(body: str, api_key: str) -&gt; dict:\n    audit(body)\n    resp = httpx.post(\n        f\"{API_BASE}\/SMSApi\/template\/create\",\n        headers={\"apikey\": api_key},\n        data={\"message\": body, \"output\": \"json\"},\n        timeout=15.0,\n    )\n    resp.raise_for_status()\n    return resp.json()\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note <code>data=<\/code> rather than <code>json=<\/code>. This endpoint is form-encoded. Passing a JSON body to it produces a confusing authentication error rather than a helpful one, which is a trap the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-python-integration-tutorial\/\">Python integration tutorial<\/a> covers in more detail.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The local audit catches roughly half the rejection table before you spend a review cycle. It cannot catch category mismatch, header binding or URL whitelisting, because those require portal-side state. Treat it as a linter, not a guarantee.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"pre-send-validation\" class=\"wp-block-heading\">Pre-Send Validation: Catching Mismatch Before the Operator Does<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation-1024x584.webp\" alt=\"Diagram about Pre-Send Validation\" class=\"wp-image-2631\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/08\/pre-send-validation.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This is the single highest-value practice in this entire guide, and almost nobody does it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SMSGatewayCenter exposes a validation endpoint that performs the same reconciliation the scrubber will perform, without sending anything and without billing anything. It is documented at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/validateTemplate\/\">Validate Template<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST https:\/\/unify.smsgateway.center\/SMSApi\/validateTemplate \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\/x-www-form-urlencoded\" \\\n  --data-urlencode \"senderid=ACMEIN\" \\\n  --data-urlencode \"msg=Your ACME verification code is 483920. Valid for 10 minutes. Do not share it. ACME\" \\\n  --data-urlencode \"dltTemplateId=1107xxxxxxxxxxxxxx\" \\\n  --data-urlencode \"output=json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A successful validation returns the sender ID, the template ID, a <code>text<\/code> field, and critically a <code>template<\/code> field containing the punctuation-normalized registered template. A mismatch returns <code>statusCode<\/code> <code>188<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where to call it<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>In continuous integration, on every pull request that touches a template string.<\/strong> Not in your production send path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Calling it inline before every send doubles your latency and your request volume for a check whose answer changes only when someone edits a template. The answer is deterministic per template version. Compute it once, at build time, and fail the build.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\nimport sys\nimport httpx\n\nAPI_BASE = \"https:\/\/unify.smsgateway.center\"\n\n# Template strings live in code, reviewed in pull requests, keyed by flow.\n# Values are the longest realistic sample for each variable position.\nTEMPLATES = {\n    \"otp_login\": {\n        \"dlt_template_id\": os.environ&#91;\"DLT_TPL_OTP_LOGIN\"],\n        \"senderid\": \"ACMEIN\",\n        \"body\": \"Your ACME verification code is {#var#}. Valid for 10 minutes. Do not share it. ACME\",\n        \"sample_vars\": &#91;\"483920\"],\n    },\n    \"order_shipped\": {\n        \"dlt_template_id\": os.environ&#91;\"DLT_TPL_ORDER_SHIPPED\"],\n        \"senderid\": \"ACMEIN\",\n        \"body\": \"Hi {#var#}, your ACME order {#var#} has shipped and arrives by {#var#}. Track it in the ACME app. ACME\",\n        \"sample_vars\": &#91;\"Priya Ramachandran\", \"AC-90881234\", \"18 Aug\"],\n    },\n}\n\n\ndef render(body: str, values: list&#91;str]) -&gt; str:\n    \"\"\"Positional substitution, one pass, left to right. See the rendering section.\"\"\"\n    out = &#91;]\n    cursor = 0\n    token = \"{#var#}\"\n    for value in values:\n        idx = body.find(token, cursor)\n        if idx == -1:\n            raise ValueError(\"more values supplied than placeholders in template\")\n        out.append(body&#91;cursor:idx])\n        out.append(value)\n        cursor = idx + len(token)\n    if body.find(token, cursor) != -1:\n        raise ValueError(\"fewer values supplied than placeholders in template\")\n    out.append(body&#91;cursor:])\n    return \"\".join(out)\n\n\ndef main() -&gt; int:\n    api_key = os.environ&#91;\"SGC_API_KEY\"]\n    failures = &#91;]\n\n    with httpx.Client(base_url=API_BASE, timeout=20.0) as client:\n        for name, tpl in TEMPLATES.items():\n            rendered = render(tpl&#91;\"body\"], tpl&#91;\"sample_vars\"])\n            resp = client.post(\n                \"\/SMSApi\/validateTemplate\",\n                headers={\"apikey\": api_key},\n                data={\n                    \"senderid\": tpl&#91;\"senderid\"],\n                    \"msg\": rendered,\n                    \"dltTemplateId\": tpl&#91;\"dlt_template_id\"],\n                    \"output\": \"json\",\n                },\n            )\n            payload = resp.json()\n            code = str(payload.get(\"statusCode\", \"\"))\n\n            if code == \"200\":\n                print(f\"PASS  {name}\")\n            elif code == \"188\":\n                failures.append(name)\n                print(f\"FAIL  {name}: template mismatch (188)\")\n                print(f\"      sent:       {rendered!r}\")\n                print(f\"      registered: {payload.get('template')!r}\")\n            else:\n                failures.append(name)\n                print(f\"FAIL  {name}: {code} {payload.get('reason')}\")\n\n    if failures:\n        print(f\"\\n{len(failures)} template(s) failed validation: {', '.join(failures)}\")\n        return 1\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The diagnostic on failure is the point. When it fails, it prints both what you sent and the normalised registered form. Ninety percent of the time the difference is visible immediately: a period, a double space, a curly apostrophe.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The same check in Node.js<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import { setTimeout as delay } from 'node:timers\/promises';\n\nconst API_BASE = 'https:\/\/unify.smsgateway.center';\nconst TOKEN = '{#var#}';\n\nfunction render(body, values) {\n  let out = '';\n  let cursor = 0;\n  for (const value of values) {\n    const idx = body.indexOf(TOKEN, cursor);\n    if (idx === -1) throw new Error('more values than placeholders');\n    out += body.slice(cursor, idx) + value;\n    cursor = idx + TOKEN.length;\n  }\n  if (body.indexOf(TOKEN, cursor) !== -1) throw new Error('fewer values than placeholders');\n  return out + body.slice(cursor);\n}\n\nexport async function validateTemplate({ apiKey, senderid, dltTemplateId, body, vars }) {\n  const rendered = render(body, vars);\n\n  const res = await fetch(`${API_BASE}\/SMSApi\/validateTemplate`, {\n    method: 'POST',\n    headers: {\n      apikey: apiKey,\n      'Content-Type': 'application\/x-www-form-urlencoded',\n    },\n    body: new URLSearchParams({\n      senderid,\n      msg: rendered,\n      dltTemplateId,\n      output: 'json',\n    }),\n    \/\/ fetch in Node has no default timeout. Without this a stalled request never settles.\n    signal: AbortSignal.timeout(20_000),\n  });\n\n  const payload = await res.json();\n  const code = String(payload.statusCode ?? '');\n\n  return {\n    ok: code === '200',\n    code,\n    reason: payload.reason,\n    sent: rendered,\n    registered: payload.template ?? null,\n  };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>AbortSignal.timeout<\/code> is not incidental. <code>fetch<\/code> in Node has no default timeout, so a hung validation call will stall your CI job indefinitely rather than failing it. This and the rest of the Node-specific failure surface are covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-nodejs-integration-tutorial\/\">Node.js integration tutorial<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The same check in PHP<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\ndeclare(strict_types=1);\n\nuse GuzzleHttp\\Client;\n\nfinal class TemplateValidator\n{\n    private const TOKEN = '{#var#}';\n\n    public function __construct(\n        private readonly Client $http,\n        private readonly string $apiKey,\n    ) {}\n\n    \/** @param list&lt;string&gt; $values *\/\n    public function render(string $body, array $values): string\n    {\n        $cursor = 0;\n        $out = '';\n        foreach ($values as $value) {\n            $idx = strpos($body, self::TOKEN, $cursor);\n            if ($idx === false) {\n                throw new RuntimeException('more values than placeholders');\n            }\n            $out .= substr($body, $cursor, $idx - $cursor) . $value;\n            $cursor = $idx + strlen(self::TOKEN);\n        }\n        if (strpos($body, self::TOKEN, $cursor) !== false) {\n            throw new RuntimeException('fewer values than placeholders');\n        }\n\n        return $out . substr($body, $cursor);\n    }\n\n    \/** @param list&lt;string&gt; $values *\/\n    public function validate(string $senderId, string $dltTemplateId, string $body, array $values): array\n    {\n        $rendered = $this-&gt;render($body, $values);\n\n        $response = $this-&gt;http-&gt;post('https:\/\/unify.smsgateway.center\/SMSApi\/validateTemplate', &#91;\n            'headers'     =&gt; &#91;'apikey' =&gt; $this-&gt;apiKey],\n            'form_params' =&gt; &#91;\n                'senderid'      =&gt; $senderId,\n                'msg'           =&gt; $rendered,\n                'dltTemplateId' =&gt; $dltTemplateId,\n                'output'        =&gt; 'json',\n            ],\n            \/\/ Keep error bodies. Without this Guzzle throws and the JSON reason is lost.\n            'http_errors' =&gt; false,\n            'timeout'     =&gt; 20,\n        ]);\n\n        $payload = json_decode((string) $response-&gt;getBody(), true, 512, JSON_THROW_ON_ERROR);\n\n        return &#91;\n            'ok'         =&gt; (string) ($payload&#91;'statusCode'] ?? '') === '200',\n            'code'       =&gt; (string) ($payload&#91;'statusCode'] ?? ''),\n            'reason'     =&gt; $payload&#91;'reason'] ?? null,\n            'sent'       =&gt; $rendered,\n            'registered' =&gt; $payload&#91;'template'] ?? null,\n        ];\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>http_errors =&gt; false<\/code> matters. Left at Guzzle&#8217;s default, a non-2xx response throws before you can read the JSON body, and the <code>188<\/code> reason string you needed disappears into a generic exception. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-php-integration-tutorial\/\">PHP integration tutorial<\/a> goes deeper on this and the surrounding client architecture.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Java<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.StringJoiner;\n\npublic final class TemplateValidator {\n\n    private static final String ENDPOINT =\n        \"https:\/\/unify.smsgateway.center\/SMSApi\/validateTemplate\";\n\n    private final HttpClient client = HttpClient.newBuilder()\n        .connectTimeout(Duration.ofSeconds(10))\n        .build();\n\n    public HttpResponse&lt;String&gt; validate(String apiKey,\n                                         String senderId,\n                                         String dltTemplateId,\n                                         String renderedMessage) throws Exception {\n\n        Map&lt;String, String&gt; form = Map.of(\n            \"senderid\", senderId,\n            \"msg\", renderedMessage,\n            \"dltTemplateId\", dltTemplateId,\n            \"output\", \"json\"\n        );\n\n        StringJoiner body = new StringJoiner(\"&amp;\");\n        form.forEach((k, v) -&gt; body.add(\n            URLEncoder.encode(k, StandardCharsets.UTF_8) + \"=\" +\n            URLEncoder.encode(v, StandardCharsets.UTF_8)\n        ));\n\n        HttpRequest request = HttpRequest.newBuilder()\n            .uri(URI.create(ENDPOINT))\n            .timeout(Duration.ofSeconds(20))\n            .header(\"apikey\", apiKey)\n            .header(\"Content-Type\", \"application\/x-www-form-urlencoded\")\n            .POST(HttpRequest.BodyPublishers.ofString(body.toString()))\n            .build();\n\n        return client.send(request, HttpResponse.BodyHandlers.ofString());\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Postman<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A ready-made collection covering the send, OTP, template and report endpoints is available from the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/download-sms-postman\/\">SMS Postman collection download<\/a>. Import it, set an environment variable for your API key, and you can validate templates interactively before you write any code. For exploratory work there is also a live API testing surface at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-messaging-api\/\">SMS Messaging API<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Wiring it into the send call<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Once validation passes in CI, the send call carries the entity and template IDs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST https:\/\/unify.smsgateway.center\/SMSApi\/send \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -d \"sendMethod=quick\" \\\n  -d \"msgType=text\" \\\n  -d \"mobile=91XXXXXXXXXX\" \\\n  -d \"senderid=ACMEIN\" \\\n  -d \"msg=Your ACME verification code is 483920. Valid for 10 minutes. Do not share it. ACME\" \\\n  -d \"dltEntityId=1101XXXXXXXXXXXXXX\" \\\n  -d \"dltTemplateId=1107XXXXXXXXXXXXXX\" \\\n  -d \"output=json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Success response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"mobile\": \"91XXXXXXXXXX\",\n  \"invalidMobile\": \"\",\n  \"transactionId\": \"...\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Full parameter reference at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-sms-batch\/\">Send SMS<\/a>, and if you are integrating from scratch start with the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-getting-started-guide\/\">SMS API getting started guide<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rendering\" class=\"wp-block-heading\">Rendering DLT Variables Without Corrupting Them<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Variable substitution looks trivial and is not. The naive implementations in every language are wrong in ways that produce mismatch failures you will spend a day debugging.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why regular expression replacement is unsafe<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In most languages the replacement argument to a regex substitution is itself a template, not a literal. Backreference sequences inside your data are interpreted:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Python:<\/strong> <code>re.sub(r\"\\{#var#\\}\", value, body, count=1)<\/code> interprets <code>\\1<\/code> and <code>\\g&lt;0><\/code> inside <code>value<\/code>. A customer address containing <code>\\1<\/code> corrupts the output. <code>re.escape<\/code> escapes the pattern, not the replacement, so it does not help.<\/li>\n\n\n\n<li><strong>JavaScript:<\/strong> <code>body.replace(TOKEN, value)<\/code> interprets <code>$&amp;<\/code>, <code>$'<\/code>, <code>$`<\/code> and <code>$1<\/code> inside <code>value<\/code>. A value containing <code>$&amp;<\/code> expands to the matched substring, which means a literal <code>{#var#}<\/code> appears in your output. This is not hypothetical for user-generated content or free-text order notes.<\/li>\n\n\n\n<li><strong>PHP:<\/strong> <code>preg_replace<\/code> has the same backreference exposure through <code>$1<\/code> and <code>\\1<\/code>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Why chained literal replacement is unsafe<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>str.replace(token, value)<\/code> called once per variable in a loop re-scans the entire string each time, including text you already inserted. If a substituted value happens to contain the literal <code>{#var#}<\/code>, the next iteration finds it and substitutes into your data rather than into the template. Every subsequent variable then lands in the wrong position, and the message either fails validation or, worse, sends with the recipient&#8217;s name where the order number should be.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The correct approach: single-pass positional substitution with a cursor<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Walk the template once, left to right. Track a cursor. For each value, find the next token from the cursor, copy the fixed text before it, append the value, and advance the cursor past the token. Never look backwards, never re-scan inserted text, never interpret the replacement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is exactly what the <code>render<\/code> functions in the previous section do, in Python, JavaScript and PHP. They share three properties that matter:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Inserted text is never re-examined.<\/strong> The cursor only moves forward.<\/li>\n\n\n\n<li><strong>The replacement is a literal.<\/strong> No template interpretation, no backreference expansion.<\/li>\n\n\n\n<li><strong>Arity is checked.<\/strong> Supplying too many or too few values raises rather than silently producing a malformed body. A template arity error at render time is a caught bug. The same error uncaught is a scrubbing failure and a wasted credit.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Add one more guard on top: reject empty variable values before rendering. An empty substitution collapses fixed text together and produces a structurally different message that the scrubber will not match.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def render_checked(body: str, values: list&#91;str]) -&gt; str:\n    for i, v in enumerate(values):\n        if v is None or not str(v).strip():\n            raise ValueError(f\"variable at position {i} is empty; refusing to render\")\n    return render(body, &#91;str(v) for v in values])\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" id=\"debug-table\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Symptom to Cause to Fix: Debug Table<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Symptom<\/th><th>Likely cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td>Template rejected immediately, no reviewer comment<\/td><td>Automated rule check on variable syntax<\/td><td>Check the <code>{#var#}<\/code> token character for character<\/td><\/tr><tr><td>Rejected with a category comment<\/td><td>Promotional wording in a service template<\/td><td>Strip the incentive or reclassify<\/td><\/tr><tr><td>Rejected mentioning header<\/td><td>Header not approved, wrong entity, or wrong category<\/td><td>Approve the header first, in the matching category<\/td><\/tr><tr><td>Rejected mentioning URL<\/td><td>Domain not whitelisted, or a public shortener<\/td><td>Whitelist the exact domain; move to a branded short domain<\/td><\/tr><tr><td>Rejected as duplicate<\/td><td>Identical body already approved under another ID<\/td><td>Find and reuse the existing ID<\/td><\/tr><tr><td>Template approved, messages fail with 188<\/td><td>Rendered body no longer matches registered body<\/td><td>Run the CI validator; diff <code>sent<\/code> against <code>registered<\/code><\/td><\/tr><tr><td>188 only on some recipients<\/td><td>A variable resolves empty or unusually long for those recipients<\/td><td>Guard against empty values; test with maximum-length values<\/td><\/tr><tr><td>Messages worked last month, fail now with no code change<\/td><td>Broken entity to telemarketer binding after a provider or account change<\/td><td>Verify the binding chain<\/td><\/tr><tr><td>Messages cost twice what you modelled<\/td><td>A substituted Unicode character forced the Unicode branch<\/td><td>Audit codepoints; check <code>encoding<\/code> via the length and cost endpoint<\/td><\/tr><tr><td>Template registered fine, delivery reports show consent failure<\/td><td>Service Explicit without a registered consent template or consent record<\/td><td>Register the consent template and capture consent<\/td><\/tr><tr><td>Everything correct but sender ID reported invalid<\/td><td>Header blocked or not bound on this route<\/td><td>Check header status before touching the template<\/td><\/tr><tr><td>Validation returns 188 but the strings look identical<\/td><td>Invisible whitespace or a curly apostrophe<\/td><td>Print <code>repr()<\/code> of both strings, not the strings<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"resubmit\" class=\"wp-block-heading\">How to Resubmit a Rejected Template Without Getting Rejected Again<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There is no penalty for resubmission and no cap on attempts, which tempts teams into resubmitting minor variations until something sticks. That is the slow path. Do this instead.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Read the rejection reason string verbatim and map it to a row in the <a href=\"#master-table\" data-type=\"internal\" data-id=\"#master-table\">master table<\/a>.<\/strong> Do not paraphrase it. The exact wording usually identifies the rule.<\/li>\n\n\n\n<li><strong>Fix the rule, not the sentence.<\/strong> If the reason is variable misuse, rewriting your marketing copy will not help. Change the variable structure.<\/li>\n\n\n\n<li><strong>Re-run the local audit.<\/strong> Codepoint check, variable structure check, whitespace check, arity check.<\/li>\n\n\n\n<li><strong>Re-check the category against the decision matrix.<\/strong> A surprising number of rejections nominally about wording are actually about category.<\/li>\n\n\n\n<li><strong>Confirm the header is approved, in the same category, under the same entity.<\/strong><\/li>\n\n\n\n<li><strong>Whitelist any domain in the body before resubmitting, not after.<\/strong><\/li>\n\n\n\n<li><strong>Price the rendered maximum-length version.<\/strong> Make sure you are not about to permanently approve a template that costs two credits per message.<\/li>\n\n\n\n<li><strong>Search your existing approved templates for a duplicate.<\/strong> If one exists, do not resubmit at all. Use the existing ID.<\/li>\n\n\n\n<li><strong>Submit from version control, not from a text editor.<\/strong> Either through <code>SMSApi\/template\/create<\/code> or by copying from a plain text file, never by retyping.<\/li>\n\n\n\n<li><strong>When approved, immediately add the template to your CI validation set<\/strong> so the next copy edit fails the build rather than the campaign.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"registry\" class=\"wp-block-heading\">Building a Template Registry You Can Actually Audit<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The teams that stop having DLT problems all converge on the same structure: templates become code artefacts rather than portal state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A workable registry entry holds, per template:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Why it matters<\/th><\/tr><\/thead><tbody><tr><td>Internal flow key, for example <code>order_shipped<\/code><\/td><td>Code references the key, never the raw ID<\/td><\/tr><tr><td><code>dltTemplateId<\/code><\/td><td>Injected from environment, never hardcoded across environments<\/td><\/tr><tr><td><code>dltEntityId<\/code><\/td><td>Same entity for all templates under one principal<\/td><\/tr><tr><td>Header, <code>senderid<\/code><\/td><td>Must match the header the template was approved against<\/td><\/tr><tr><td>Category<\/td><td>Drives sending window expectations and consent requirements<\/td><\/tr><tr><td>Exact registered body, byte for byte<\/td><td>The source of truth your CI validates against<\/td><\/tr><tr><td>Variable count and semantic label per position<\/td><td>Positional variables have no names; the registry supplies them<\/td><\/tr><tr><td>Maximum realistic value per position<\/td><td>Used for both validation and cost modelling<\/td><\/tr><tr><td>Rendered maximum length and credit count<\/td><td>Catches the two-credit template before approval<\/td><\/tr><tr><td>Language and script<\/td><td>Determines <code>msgType=text<\/code> versus <code>msgType=unicode<\/code><\/td><\/tr><tr><td>Approval date and approval status<\/td><td>Audit trail<\/td><\/tr><tr><td>Supersedes and superseded-by<\/td><td>Migration path when copy changes force a new template<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The last row is the one people skip and later need. Because approved templates are immutable, changing copy means registering a new template and migrating traffic. Without a supersession record, you end up with two live IDs, some traffic on each, and reconciliation reports that split one business flow across two rows with no way to join them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For reconciliation, the delivery report API returns <code>dltTemplateId<\/code> on every record alongside <code>cost<\/code>, <code>length<\/code>, <code>status<\/code> and <code>msgType<\/code>, which means you can group your actual spend and failure rate by template. That endpoint is documented at <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-delivery-report\/\">SMS Delivery Report<\/a>, and the status vocabulary is explained in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/understanding-delivery-reports-dlr\/\">Understanding Delivery Reports<\/a>. Grouping template check failures by <code>dltTemplateId<\/code> over a week tells you exactly which template drifted, which is a far faster diagnosis than reading application logs.<\/p>\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-Submission Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run this before every template submission.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Variables<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Every placeholder is the literal <code>{#var#}<\/code>, verified character by character<\/li>\n\n\n\n<li>[ ] No template consists only of variables<\/li>\n\n\n\n<li>[ ] No two variables are adjacent<\/li>\n\n\n\n<li>[ ] The template does not start with a variable<\/li>\n\n\n\n<li>[ ] The template does not end with a variable<\/li>\n\n\n\n<li>[ ] Every variable carries exactly one data value, with fixed text describing it<\/li>\n\n\n\n<li>[ ] Variable count is the minimum needed, with static values hardcoded<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Category<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Category chosen using the decision matrix, not by habit<\/li>\n\n\n\n<li>[ ] No offer, discount, incentive or commercial call to action inside a service or transactional template<\/li>\n\n\n\n<li>[ ] Service Explicit templates have a registered consent template and real consent records<\/li>\n\n\n\n<li>[ ] Transactional used only if the entity is eligible<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Header<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Header already approved, not pending<\/li>\n\n\n\n<li>[ ] Header belongs to the same principal entity<\/li>\n\n\n\n<li>[ ] Header category matches the template category<\/li>\n\n\n\n<li>[ ] Entity to telemarketer binding is intact<\/li>\n\n\n\n<li>[ ] Brand name appears in the fixed text of the body<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Content<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Every domain in the body is whitelisted<\/li>\n\n\n\n<li>[ ] No public URL shortener<\/li>\n\n\n\n<li>[ ] The domain sits in fixed text, only the token is a variable<\/li>\n\n\n\n<li>[ ] Every phone number in the body is registered<\/li>\n\n\n\n<li>[ ] No emoji, no decorative symbols<\/li>\n\n\n\n<li>[ ] Codepoint audit run; no character outside basic printable ASCII unless the template is deliberately Unicode<\/li>\n\n\n\n<li>[ ] Spelling and grammar proofread by someone who did not write it<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Structure and cost<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] No leading, trailing or double whitespace<\/li>\n\n\n\n<li>[ ] Rendered maximum-length version priced with the length and cost endpoint<\/li>\n\n\n\n<li>[ ] At least fifteen characters of headroom under the segment boundary<\/li>\n\n\n\n<li>[ ] Language and script registered; regional templates budgeted as Unicode<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Governance<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Existing approved templates searched for a duplicate<\/li>\n\n\n\n<li>[ ] Template body committed to version control<\/li>\n\n\n\n<li>[ ] Submitted from the repository, not retyped<\/li>\n\n\n\n<li>[ ] On approval, added to the CI validation set<\/li>\n\n\n\n<li>[ ] Registry entry created with variable labels, max values and supersession fields<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"mistakes\" class=\"wp-block-heading\">Ten Mistakes That Keep Costing Teams Approvals<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Editing template copy in an admin panel.<\/strong> The template ID does not change, so nothing warns you. Every message on that flow fails from the moment the edit ships. Template strings belong in version control behind a code review gate.<\/li>\n\n\n\n<li><strong>Pasting templates from a document editor.<\/strong> Curly apostrophes and en dashes arrive invisibly, force Unicode encoding, and quietly triple your per-message cost.<\/li>\n\n\n\n<li><strong>Treating registration approval as the end of compliance.<\/strong> Approval means the template is valid. It says nothing about whether the message your code renders still matches it.<\/li>\n\n\n\n<li><strong>Calling the validation endpoint in the production send path.<\/strong> It doubles latency and request volume to answer a question whose answer only changes at deploy time. Run it in CI.<\/li>\n\n\n\n<li><strong>Registering everything as Transactional.<\/strong> It is entity-restricted, it will be rejected, and Service Implicit already delivers to preference-registry numbers for genuine service messages.<\/li>\n\n\n\n<li><strong>Using regex replacement for variable substitution.<\/strong> A <code>$&amp;<\/code> or a <code>\\1<\/code> inside customer data corrupts the rendered body and produces a mismatch you will not reproduce locally.<\/li>\n\n\n\n<li><strong>Approving a template eleven characters over a segment boundary.<\/strong> Immutable template plus permanent second credit on every message. Price before you submit.<\/li>\n\n\n\n<li><strong>Hiding a URL inside a variable.<\/strong> Rejected at registration, and if it somehow passes it makes your scrubbing behaviour unpredictable.<\/li>\n\n\n\n<li><strong>Registering a second template ID for a body that already exists.<\/strong> Rejected as duplicate, and even when a near-variant passes it splits one business flow across two IDs in every report you will ever run.<\/li>\n\n\n\n<li><strong>Retrying a template-failed message.<\/strong> A compliance rejection is permanent for that message. Retrying re-submits, which can re-bill, and it will fail identically. Template failures belong in the permanent-failure class, as covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-retry-strategy-handling-failed-messages\/\">SMS API retry strategy guide<\/a>.<\/li>\n<\/ol>\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>Why was my DLT template rejected?<\/strong> Almost always one of six things: the <code>{#var#}<\/code> token was written incorrectly or overused, the content did not match the declared category, the header was not approved or not bound to your entity, a URL or callback number was not whitelisted, disallowed characters were present, or an identical body already existed. Match your rejection reason against the <a href=\"#master-table\" data-type=\"internal\" data-id=\"#master-table\">master table<\/a> and fix the rule rather than rewording the sentence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I edit an approved DLT template?<\/strong> No. Approved templates are immutable. Changing the copy means registering a new template, receiving a new template ID, and migrating your code and configuration to that ID. Plan for this by recording a supersedes relationship in your template registry so reporting can join the old and new flows.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there a limit on how many times I can resubmit a rejected template?<\/strong> There is no cap on resubmissions and no penalty for them. That said, resubmitting variations without identifying the failing rule wastes review cycles. Work the checklist first.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between a rejected template and a template mismatch?<\/strong> A rejected template never got approved. A mismatch happens on an approved template when the message your code actually rendered does not reconcile with the registered body at scrubbing time. The second surfaces as <code>statusCode<\/code> <code>188<\/code> from the validation endpoint and as a template check failure in delivery reports.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What does status code 188 mean?<\/strong> <code>188<\/code> with reason <code>message template mismatch<\/code> means the message body you supplied does not match the registered template for the <code>dltTemplateId<\/code> you supplied. The usual causes are an unregistered copy edit, whitespace drift, a punctuation difference, an empty variable, or the wrong template ID for that message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does my template fail even though the text looks identical?<\/strong> Print the repr of both strings rather than the strings themselves. The difference is nearly always invisible: a trailing space, a double space, a line break, or a curly apostrophe where the registered template has a straight one. The validation endpoint returns the punctuation-normalized registered template so you can diff against it directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How many variables can I use in one template?<\/strong> There is no single universal number, but the practical constraint is twofold. Portal convention charges a fixed character allowance per variable against the template&#8217;s length budget, so more variables leave less room for fixed text. And a high variable-to-fixed-text ratio is itself a rejection cause because the template stops being reviewable. Use the minimum number of variables that carries genuinely per-recipient data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I use emoji in a DLT template?<\/strong> Generally no. Emoji are commonly rejected outright, and even where accepted they force the message into Unicode encoding, which drops per-segment capacity from 160 characters to 70 and multiplies your credit cost.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do I need a separate template for each language?<\/strong> Yes. Each language variant is a separate registration with its own template ID, and a transliteration into Latin script counts as a different template from the same message in a regional script. Regional-script templates are Unicode-encoded, so budget for higher credit consumption per message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why can I not use a URL shortener in my template?<\/strong> Because the destination is unverifiable at review time. Use a branded short domain you control, whitelist that domain, and put it in the fixed text of the template with only the opaque token as a variable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>My template registered fine but messages fail with a consent error. Why?<\/strong> You most likely registered Service Explicit content without a corresponding approved consent template and recorded consent for those recipients. The content template check passes and the consent check then fails at scrubbing. Register the consent template first, capture consent, then send.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Everything worked last month and now nothing does, without any code change. What broke?<\/strong> Check your entity to telemarketer binding before you look at templates. A provider change, an account migration or a lapsed binding breaks delivery for templates that are still perfectly valid. Header status is the second thing to check.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I validate every message against the template before sending?<\/strong> No. Validate at build time, once per template version, in continuous integration. The answer is deterministic per template version, so an inline check on every send buys you nothing and costs latency and request volume on your hottest path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I create DLT templates through the API instead of the portal?<\/strong> Yes. <code>POST https:\/\/unify.smsgateway.center\/SMSApi\/template\/create<\/code> submits a template and returns <code>Template created successfully and given for approval<\/code>. Note that it submits for approval, it does not approve. The advantage is that template bodies live in your repository and get reviewed as code, which eliminates the retyping errors that cause whitespace and character-substitution failures.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lets onboard you<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Stop debugging templates in production.<\/strong> SMSGatewayCenter exposes template creation, pre-send validation and template-level delivery reporting on the same API, so you can catch a mismatch in a pull request instead of a campaign. <a href=\"https:\/\/unify.smsgateway.center\/signup\/\" target=\"_blank\" rel=\"noreferrer noopener\">Create a free account<\/a> or <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/\">browse the developer API reference<\/a> to wire validation into your build today.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>A DLT content template gets rejected for a small number of repeatable reasons: variable misuse, category misclassification, header mismatch, unwhitelisted URLs, disallowed characters and duplicates. This guide maps every rejection cause to a concrete fix, then shows how to stop the second, quieter failure: a template that is approved but still fails at send time with status code 188.<\/p>\n","protected":false},"author":118,"featured_media":2629,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[268],"tags":[2089,404,446,2092,2091,2090,481,1372,2088,1288,256],"class_list":["post-2628","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials","tag-content-template","tag-dlt","tag-dlt-template","tag-india-sms","tag-scrubbing","tag-sender-id","tag-sms-api","tag-sms-compliance","tag-tcccpr","tag-template-rejection","tag-trai"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2628","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=2628"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2628\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2629"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2628"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2628"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2628"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}