{"id":2528,"date":"2026-07-27T11:25:25","date_gmt":"2026-07-27T05:55:25","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2528"},"modified":"2026-07-27T11:25:29","modified_gmt":"2026-07-27T05:55:29","slug":"sms-api-retry-strategy-handling-failed-messages","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-retry-strategy-handling-failed-messages\/","title":{"rendered":"SMS API Retry Strategy: How to Handle Failed Message Delivery"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Retry logic for SMS should be driven by the specific failure reason on the delivery report, not by a blanket &#8220;try again three times&#8221; rule. Transport-layer failures (timeouts, connection resets) are almost always safe to retry immediately. Carrier-layer failures reported through the delivery report, such as <code>OPERATOR_DOWN<\/code> or <code>SYSTEM_FAIL<\/code>, should be retried with exponential backoff and a retry cap. Compliance-layer failures, such as a rejected DLT template or an invalid sender ID, should never be retried automatically because retrying does not fix the underlying cause and just burns credits. Every retry attempt needs an idempotency check so a slow delivery report doesn&#8217;t cause the same message to be sent twice.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why &#8220;Just Retry It&#8221; Is the Wrong Default<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A message fails. The obvious instinct is to send it again. For transactional SMS, that instinct is expensive to get wrong in either direction: retry too little and a customer never gets their OTP or shipping alert, retry too much and you either duplicate the message, burn through the account balance on messages that were never going to deliver, or get flagged for looking like spam traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is not a fixed retry count. It&#8217;s a policy that reads the actual reason a message failed and only retries the failure classes where a retry is statistically likely to succeed. That requires knowing what your gateway&#8217;s delivery report is actually telling you, and reacting differently to different codes rather than treating every non-delivery the same way.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This article assumes you&#8217;re already pulling delivery status, either through the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-delivery-report\/\">delivery report API<\/a> or through <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/real-time-dlr-webhooks\/\">real-time DLR webhooks<\/a>. If you haven&#8217;t wired that up yet, do that first. A retry strategy with no visibility into why messages fail is just guessing on a timer.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow-1024x584.webp\" alt=\"Flowchart illustration showing SMS delivery outcomes branching into delivered, retry, and permanent failure paths\" class=\"wp-image-2529\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/sms-api-retry-strategy-decision-flow.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Classify the Failure Before You Retry<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every SMS failure falls into one of three layers, and each layer needs a different response.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/classify-the-failure-before-you-retry.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"570\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/classify-the-failure-before-you-retry.webp\" alt=\"Illustration about Classify the Failure Before You Retry\" class=\"wp-image-2531\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/classify-the-failure-before-you-retry.webp 1000w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/classify-the-failure-before-you-retry-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/classify-the-failure-before-you-retry-768x438.webp 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Transport layer.<\/strong> The HTTP request to the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-sms-batch\/\">Send SMS endpoint<\/a> itself failed: connection timeout, DNS resolution failure, 5xx response, or a dropped TLS handshake. The message was never accepted by the gateway, so there&#8217;s no duplicate-send risk. These are safe to retry immediately with a short backoff, typically within seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Carrier layer.<\/strong> The gateway accepted the message and returned a transaction ID, but the delivery report later comes back with a non-delivered status and a cause code such as <code>OPERATOR_DOWN<\/code>, <code>OPERATOR_TEMP_ISSUE<\/code>, <code>SYSTEM_FAIL<\/code>, or <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-bad-network-network-fluctuation-mean\/\">Bad Network<\/a>. These are almost always transient, operator-side conditions. Retry these, but with exponential backoff and a hard cap, because retrying into a genuinely down operator every 10 seconds for an hour just wastes attempts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Compliance layer.<\/strong> The message was rejected because of a scrubber match, an unregistered or mismatched DLT template ID, an invalid or unapproved sender ID, or a malformed destination number. Nothing about retrying the same request changes any of these conditions. Retrying here is pure waste, and in the case of DLT template mismatches, it can also generate confusing audit trails when you&#8217;re troubleshooting compliance issues later. If you&#8217;re still building out your DLT registration, that&#8217;s a separate and more foundational problem than retry logic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Delivery Error Codes and What They Actually Mean for Retry Policy<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SMSGatewayCenter exposes the full list of delivery status identifiers through the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-delivery-error-code-list\/\">Delivery Error Codes API<\/a>, which returns a <code>deliverycodesList<\/code> array where each entry has a <code>peId<\/code>, an <code>identifier<\/code> (the code name, e.g. <code>SUCCESS<\/code> or <code>OTHER<\/code>), a <code>status<\/code> (<code>DELIVERED<\/code> or <code>FAILED<\/code>), and a <code>cause<\/code> description. The related <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-api-response-error-code-list\/\">API Response Error Code List<\/a> covers request-level errors (like invalid sender ID or malformed parameters) rather than post-submission delivery outcomes, so your retry logic needs to check both depending on where in the pipeline the failure occurred.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rather than hardcoding every one of the roughly 45 delivery codes into your application, pull this list at startup or cache it periodically, then map each <code>identifier<\/code> your delivery reports return to one of the three retry classes above. The individual glossary write-ups on codes like <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-in_retry-retry-attempted-mean\/\">IN_RETRY<\/a>, <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-system_fail-system-failure-mean\/\">SYSTEM_FAIL<\/a>, <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-operator_down-operator-unreachable-mean\/\">OPERATOR_DOWN<\/a>, <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-operator_temp_issue-far-end-operator-issue-mean\/\">OPERATOR_TEMP_ISSUE<\/a>, <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-expired-expired-message-mean\/\">Expired<\/a>, and <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-other_sys_fail-report-pending-from-operator-multipart-mean\/\">OTHER_SYS_FAIL<\/a> are useful references for understanding what each individual code means; this section is about turning that glossary into a policy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note that <code>IN_RETRY<\/code> specifically means the gateway itself is already retrying the message against the operator on your behalf. If you see <code>IN_RETRY<\/code> on the delivery report, do not fire your own application-level retry on top of it. That produces the exact duplicate-send problem described in the idempotency section below. Wait for <code>IN_RETRY<\/code> to resolve to either <code>DELIVERED<\/code> or a terminal failure code, or for the message to hit <code>Expired<\/code> if it sits in the operator queue too long.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Retry Decision Matrix<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Failure signal<\/th><th>Layer<\/th><th>Retry?<\/th><th>Backoff strategy<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>HTTP timeout \/ connection reset on Send SMS call<\/td><td>Transport<\/td><td>Yes<\/td><td>Immediate, then short fixed delay (1-2s)<\/td><td>Message was never accepted; no duplicate risk<\/td><\/tr><tr><td>HTTP 5xx from gateway<\/td><td>Transport<\/td><td>Yes<\/td><td>Immediate, then short fixed delay<\/td><td>Confirm no transaction ID was returned before retrying<\/td><\/tr><tr><td><code>OPERATOR_DOWN<\/code><\/td><td>Carrier<\/td><td>Yes<\/td><td>Exponential backoff, cap at 3-5 attempts<\/td><td>Operator infrastructure issue, not your integration<\/td><\/tr><tr><td><code>OPERATOR_TEMP_ISSUE<\/code><\/td><td>Carrier<\/td><td>Yes<\/td><td>Exponential backoff, cap at 3-5 attempts<\/td><td>Often resolves within minutes<\/td><\/tr><tr><td><code>SYSTEM_FAIL<\/code><\/td><td>Carrier<\/td><td>Conditional<\/td><td>Exponential backoff, cap at 2-3 attempts<\/td><td>If frequent across many messages, treat as a systemic issue and stop auto-retrying, escalate to support<\/td><\/tr><tr><td>Bad Network \/ network fluctuation<\/td><td>Carrier<\/td><td>Yes<\/td><td>Exponential backoff<\/td><td>Transient by definition<\/td><\/tr><tr><td><code>IN_RETRY<\/code><\/td><td>Carrier (gateway-managed)<\/td><td>No<\/td><td>N\/A<\/td><td>Gateway is already retrying; do not stack your own retry on top<\/td><\/tr><tr><td><code>Expired<\/code><\/td><td>Carrier<\/td><td>No (resend as new message, don&#8217;t retry same attempt)<\/td><td>N\/A<\/td><td>Message sat too long in queue; if still relevant, send fresh with new transaction<\/td><\/tr><tr><td>Invalid or unregistered sender ID<\/td><td>Compliance<\/td><td>No<\/td><td>N\/A<\/td><td>Fix the sender ID configuration first<\/td><\/tr><tr><td>DLT template ID mismatch or rejection<\/td><td>Compliance<\/td><td>No<\/td><td>N\/A<\/td><td>Correct the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/create-message-template\/\">DLT template<\/a> mapping, retrying does nothing<\/td><\/tr><tr><td>Malformed or invalid destination number<\/td><td>Compliance<\/td><td>No<\/td><td>N\/A<\/td><td>Validate input before sending, not after failure<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Building an Idempotent Retry Queue<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The single most common bug in home-grown SMS retry systems is duplicate sends: a message is accepted by the gateway, the delivery report is delayed or the webhook fires late, the application times out waiting and re-sends the same message, and now the customer gets the OTP twice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is to generate your own idempotency key per logical send attempt, separate from the gateway&#8217;s <code>transactionId<\/code>, and to use the gateway&#8217;s built-in <code>duplicatecheck<\/code> parameter on the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-sms-batch\/\">Send SMS endpoint<\/a> as a second line of defense, not the only line of defense. Your application should track state per message: <code>queued<\/code>, <code>sent-awaiting-dlr<\/code>, <code>delivered<\/code>, <code>failed-retryable<\/code>, <code>failed-terminal<\/code>. Only messages in <code>failed-retryable<\/code> state, past their backoff window, and under their retry cap should re-enter the send queue.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/building-an-idempotent-retry-queue.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1000\" height=\"570\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/building-an-idempotent-retry-queue.webp\" alt=\"Illustration about Building an Idempotent Retry Queue\" class=\"wp-image-2530\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/building-an-idempotent-retry-queue.webp 1000w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/building-an-idempotent-retry-queue-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/07\/building-an-idempotent-retry-queue-768x438.webp 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal state machine:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Message enters queue with a unique <code>idempotencyKey<\/code>.<\/li>\n\n\n\n<li>On send, record <code>transactionId<\/code> returned by the gateway and move to <code>sent-awaiting-dlr<\/code>.<\/li>\n\n\n\n<li>Poll the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/check-mis-by-transaction-id\/\">DLR by UUID<\/a> endpoint or consume the delivery webhook.<\/li>\n\n\n\n<li>On <code>DELIVERED<\/code>, mark complete.<\/li>\n\n\n\n<li>On a retryable failure code, check retry count against the cap. If under cap, schedule next attempt using the backoff table above and increment retry count. If over cap, move to <code>failed-terminal<\/code> and alert.<\/li>\n\n\n\n<li>On a non-retryable failure code, move directly to <code>failed-terminal<\/code> and alert, skipping the backoff logic entirely.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This is the same queuing discipline described at a systems level in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-gateway-architecture\/\">Understanding SMS Gateway Architecture<\/a>, which covers dead-letter queues for messages that exhaust all retry attempts. That article is the right reference if you&#8217;re designing the infrastructure this retry logic runs on top of; this article is about the decision logic inside it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Code: Retry Logic in PHP, Node.js, Python, Java, and cURL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The examples below use the confirmed Send SMS endpoint (<code>POST https:\/\/unify.smsgateway.center\/SMSApi\/send<\/code>) and the DLR-by-UUID endpoint (<code>https:\/\/unify.smsgateway.center\/SMSApi\/reports\/status<\/code> with <code>method=getDlr<\/code> and a <code>uuId<\/code> parameter), both from the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/\">developer API documentation<\/a>. Each sample implements the retryable\/non-retryable split from the decision matrix above and caps retries at 3 attempts with exponential backoff.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL (single send with duplicatecheck)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/unify.smsgateway.center\/SMSApi\/send\" \\\n  -H \"content-type: application\/x-www-form-urlencoded\" \\\n  --data-urlencode \"userid=YourUsername\" \\\n  --data-urlencode \"password=YourPassword\" \\\n  --data-urlencode \"sendMethod=quick\" \\\n  --data-urlencode \"mobile=919999999999\" \\\n  --data-urlencode \"msg=Your OTP is 482913\" \\\n  --data-urlencode \"senderid=YOURID\" \\\n  --data-urlencode \"msgType=text\" \\\n  --data-urlencode \"duplicatecheck=true\" \\\n  --data-urlencode \"output=json\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">PHP<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\n$retryableCodes = &#91;\"OPERATOR_DOWN\", \"OPERATOR_TEMP_ISSUE\", \"SYSTEM_FAIL\", \"BADNETWORK\"];\n$maxRetries = 3;\n\nfunction sendSms($mobile, $msg, $senderid) {\n    $curl = curl_init();\n    curl_setopt_array($curl, array(\n        CURLOPT_URL =&gt; \"https:\/\/unify.smsgateway.center\/SMSApi\/send\",\n        CURLOPT_RETURNTRANSFER =&gt; true,\n        CURLOPT_TIMEOUT =&gt; 30,\n        CURLOPT_CUSTOMREQUEST =&gt; \"POST\",\n        CURLOPT_POSTFIELDS =&gt; http_build_query(&#91;\n            \"userid\" =&gt; \"YourUsername\",\n            \"password\" =&gt; \"YourPassword\",\n            \"sendMethod\" =&gt; \"quick\",\n            \"mobile\" =&gt; $mobile,\n            \"msg\" =&gt; $msg,\n            \"senderid\" =&gt; $senderid,\n            \"msgType\" =&gt; \"text\",\n            \"duplicatecheck\" =&gt; \"true\",\n            \"output\" =&gt; \"json\"\n        ]),\n        CURLOPT_HTTPHEADER =&gt; &#91;\"content-type: application\/x-www-form-urlencoded\"]\n    ));\n    $response = curl_exec($curl);\n    $err = curl_error($curl);\n    curl_close($curl);\n    if ($err) {\n        \/\/ Transport-layer failure: safe to retry immediately\n        return &#91;\"transport_error\" =&gt; true];\n    }\n    return json_decode($response, true);\n}\n\nfunction checkDlr($uuId) {\n    $curl = curl_init();\n    curl_setopt_array($curl, array(\n        CURLOPT_URL =&gt; \"https:\/\/unify.smsgateway.center\/SMSApi\/reports\/status\",\n        CURLOPT_RETURNTRANSFER =&gt; true,\n        CURLOPT_CUSTOMREQUEST =&gt; \"POST\",\n        CURLOPT_POSTFIELDS =&gt; http_build_query(&#91;\n            \"userid\" =&gt; \"YourUsername\",\n            \"password\" =&gt; \"YourPassword\",\n            \"method\" =&gt; \"getDlr\",\n            \"uuId\" =&gt; $uuId,\n            \"output\" =&gt; \"json\"\n        ]),\n        CURLOPT_HTTPHEADER =&gt; &#91;\"content-type: application\/x-www-form-urlencoded\"]\n    ));\n    $response = curl_exec($curl);\n    curl_close($curl);\n    return json_decode($response, true);\n}\n\nfunction attemptSendWithRetry($mobile, $msg, $senderid) {\n    global $retryableCodes, $maxRetries;\n    $attempt = 0;\n    $backoff = 2; \/\/ seconds\n\n    while ($attempt &lt; $maxRetries) {\n        $attempt++;\n        $result = sendSms($mobile, $msg, $senderid);\n\n        if (isset($result&#91;\"transport_error\"])) {\n            sleep($backoff);\n            $backoff *= 2;\n            continue;\n        }\n\n        $uuId = $result&#91;\"uuId\"] ?? $result&#91;\"transactionId\"] ?? null;\n        if (!$uuId) {\n            \/\/ Compliance-layer or request-level rejection, don't retry\n            return &#91;\"status\" =&gt; \"failed-terminal\", \"reason\" =&gt; $result];\n        }\n\n        sleep(5); \/\/ allow time for DLR to populate\n        $dlr = checkDlr($uuId);\n        $status = $dlr&#91;\"reports_dlrList\"]&#91;0]&#91;\"status\"] ?? null;\n        $cause = $dlr&#91;\"reports_dlrList\"]&#91;0]&#91;\"cause\"] ?? null;\n\n        if ($status === \"DELIVERED\") {\n            return &#91;\"status\" =&gt; \"delivered\"];\n        }\n\n        if (in_array($cause, $retryableCodes)) {\n            sleep($backoff);\n            $backoff *= 2;\n            continue;\n        }\n\n        return &#91;\"status\" =&gt; \"failed-terminal\", \"reason\" =&gt; $cause];\n    }\n\n    return &#91;\"status\" =&gt; \"failed-max-retries\"];\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>const axios = require('axios');\n\nconst RETRYABLE_CODES = &#91;'OPERATOR_DOWN', 'OPERATOR_TEMP_ISSUE', 'SYSTEM_FAIL', 'BADNETWORK'];\nconst MAX_RETRIES = 3;\n\nasync function sendSms(mobile, msg, senderid) {\n  try {\n    const response = await axios.post('https:\/\/unify.smsgateway.center\/SMSApi\/send', new URLSearchParams({\n      userid: 'YourUsername',\n      password: 'YourPassword',\n      sendMethod: 'quick',\n      mobile,\n      msg,\n      senderid,\n      msgType: 'text',\n      duplicatecheck: 'true',\n      output: 'json'\n    }), { timeout: 30000 });\n    return response.data;\n  } catch (err) {\n    return { transportError: true };\n  }\n}\n\nasync function checkDlr(uuId) {\n  const response = await axios.post('https:\/\/unify.smsgateway.center\/SMSApi\/reports\/status', new URLSearchParams({\n    userid: 'YourUsername',\n    password: 'YourPassword',\n    method: 'getDlr',\n    uuId,\n    output: 'json'\n  }));\n  return response.data;\n}\n\nfunction sleep(ms) {\n  return new Promise(resolve =&gt; setTimeout(resolve, ms));\n}\n\nasync function attemptSendWithRetry(mobile, msg, senderid) {\n  let backoff = 2000;\n\n  for (let attempt = 1; attempt &lt;= MAX_RETRIES; attempt++) {\n    const result = await sendSms(mobile, msg, senderid);\n\n    if (result.transportError) {\n      await sleep(backoff);\n      backoff *= 2;\n      continue;\n    }\n\n    const uuId = result.uuId || result.transactionId;\n    if (!uuId) {\n      return { status: 'failed-terminal', reason: result };\n    }\n\n    await sleep(5000);\n    const dlr = await checkDlr(uuId);\n    const record = dlr.reports_dlrList &amp;&amp; dlr.reports_dlrList&#91;0];\n\n    if (record &amp;&amp; record.status === 'DELIVERED') {\n      return { status: 'delivered' };\n    }\n\n    if (record &amp;&amp; RETRYABLE_CODES.includes(record.cause)) {\n      await sleep(backoff);\n      backoff *= 2;\n      continue;\n    }\n\n    return { status: 'failed-terminal', reason: record ? record.cause : 'unknown' };\n  }\n\n  return { status: 'failed-max-retries' };\n}\n\nmodule.exports = { attemptSendWithRetry };\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Python<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import time\nimport requests\n\nRETRYABLE_CODES = {\"OPERATOR_DOWN\", \"OPERATOR_TEMP_ISSUE\", \"SYSTEM_FAIL\", \"BADNETWORK\"}\nMAX_RETRIES = 3\n\ndef send_sms(mobile, msg, senderid):\n    try:\n        response = requests.post(\n            \"https:\/\/unify.smsgateway.center\/SMSApi\/send\",\n            data={\n                \"userid\": \"YourUsername\",\n                \"password\": \"YourPassword\",\n                \"sendMethod\": \"quick\",\n                \"mobile\": mobile,\n                \"msg\": msg,\n                \"senderid\": senderid,\n                \"msgType\": \"text\",\n                \"duplicatecheck\": \"true\",\n                \"output\": \"json\"\n            },\n            timeout=30\n        )\n        return response.json()\n    except requests.exceptions.RequestException:\n        return {\"transport_error\": True}\n\ndef check_dlr(uu_id):\n    response = requests.post(\n        \"https:\/\/unify.smsgateway.center\/SMSApi\/reports\/status\",\n        data={\n            \"userid\": \"YourUsername\",\n            \"password\": \"YourPassword\",\n            \"method\": \"getDlr\",\n            \"uuId\": uu_id,\n            \"output\": \"json\"\n        }\n    )\n    return response.json()\n\ndef attempt_send_with_retry(mobile, msg, senderid):\n    backoff = 2\n    for attempt in range(1, MAX_RETRIES + 1):\n        result = send_sms(mobile, msg, senderid)\n\n        if result.get(\"transport_error\"):\n            time.sleep(backoff)\n            backoff *= 2\n            continue\n\n        uu_id = result.get(\"uuId\") or result.get(\"transactionId\")\n        if not uu_id:\n            return {\"status\": \"failed-terminal\", \"reason\": result}\n\n        time.sleep(5)\n        dlr = check_dlr(uu_id)\n        records = dlr.get(\"reports_dlrList\", &#91;])\n        record = records&#91;0] if records else None\n\n        if record and record.get(\"status\") == \"DELIVERED\":\n            return {\"status\": \"delivered\"}\n\n        if record and record.get(\"cause\") in RETRYABLE_CODES:\n            time.sleep(backoff)\n            backoff *= 2\n            continue\n\n        return {\"status\": \"failed-terminal\", \"reason\": record.get(\"cause\") if record else \"unknown\"}\n\n    return {\"status\": \"failed-max-retries\"}\n<\/code><\/pre>\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.time.Duration;\nimport java.util.Set;\n\npublic class SmsRetryClient {\n\n    private static final Set&lt;String&gt; RETRYABLE_CODES = Set.of(\n        \"OPERATOR_DOWN\", \"OPERATOR_TEMP_ISSUE\", \"SYSTEM_FAIL\", \"BADNETWORK\"\n    );\n    private static final int MAX_RETRIES = 3;\n\n    private final HttpClient client = HttpClient.newBuilder()\n        .connectTimeout(Duration.ofSeconds(30))\n        .build();\n\n    public String sendSms(String mobile, String msg, String senderid) throws Exception {\n        String form = \"userid=YourUsername&amp;password=YourPassword&amp;sendMethod=quick\"\n            + \"&amp;mobile=\" + mobile + \"&amp;msg=\" + msg + \"&amp;senderid=\" + senderid\n            + \"&amp;msgType=text&amp;duplicatecheck=true&amp;output=json\";\n\n        HttpRequest request = HttpRequest.newBuilder()\n            .uri(URI.create(\"https:\/\/unify.smsgateway.center\/SMSApi\/send\"))\n            .header(\"content-type\", \"application\/x-www-form-urlencoded\")\n            .POST(HttpRequest.BodyPublishers.ofString(form))\n            .build();\n\n        HttpResponse&lt;String&gt; response = client.send(request, HttpResponse.BodyHandlers.ofString());\n        return response.body();\n    }\n\n    public String checkDlr(String uuId) throws Exception {\n        String form = \"userid=YourUsername&amp;password=YourPassword&amp;method=getDlr\"\n            + \"&amp;uuId=\" + uuId + \"&amp;output=json\";\n\n        HttpRequest request = HttpRequest.newBuilder()\n            .uri(URI.create(\"https:\/\/unify.smsgateway.center\/SMSApi\/reports\/status\"))\n            .header(\"content-type\", \"application\/x-www-form-urlencoded\")\n            .POST(HttpRequest.BodyPublishers.ofString(form))\n            .build();\n\n        HttpResponse&lt;String&gt; response = client.send(request, HttpResponse.BodyHandlers.ofString());\n        return response.body();\n    }\n\n    \/\/ Wrap sendSms\/checkDlr calls in your own retry loop using RETRYABLE_CODES\n    \/\/ and exponential backoff, mirroring the PHP\/Node.js\/Python state machine above.\n    \/\/ Parse the JSON response with your JSON library of choice (Jackson, Gson) to\n    \/\/ extract \"cause\" and \"status\" from reports_dlrList before deciding whether to retry.\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Exponential Backoff Without Overloading the Gateway<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Exponential backoff means each retry waits longer than the last: 2 seconds, then 4, then 8, and so on, rather than hammering the endpoint at a fixed interval. This matters for two reasons that are specific to SMS delivery rather than generic API retries. First, operator-side outages (<code>OPERATOR_DOWN<\/code>) typically resolve on the order of minutes, not seconds, so retrying every 2 seconds for the first minute accomplishes nothing except adding load. Second, if you&#8217;re sending at volume over <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/smpp-connectivity\/\">SMPP<\/a>, aggressive uncoordinated retries across many failed messages at once can itself contribute to throughput problems, which is the same throughput-management concern covered in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/smpp-rate-limits-throughput-tuning\/\">SMPP Rate Limits and Throughput Tuning<\/a>. Add jitter, a small random offset to each backoff interval, so that a batch of messages that all failed at the same moment don&#8217;t all retry at exactly the same moment again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cap total retries per message at a small number, typically 3, and set a maximum total retry window (for example, don&#8217;t keep retrying an OTP message past 5 minutes; the OTP is worthless to the user by then regardless of whether it eventually delivers). For non-time-sensitive transactional messages, a longer window with more attempts may make sense, but the cap should still exist. Uncapped retry loops are the single most common cause of runaway SMS costs in poorly built integrations, a concern also discussed for HTTP APIs generally in <a href=\"https:\/\/www.twilio.com\/en-us\/blog\/best-practices-retry-logic-sms-2fa\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Twilio&#8217;s retry logic guidance for SMS 2FA<\/a> and in general software engineering literature on the <a href=\"https:\/\/martinfowler.com\/bliki\/CircuitBreaker.html\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">circuit breaker pattern<\/a>, which is worth adopting alongside backoff: if a large percentage of recent sends are failing with the same carrier-layer code, stop sending new messages to that route entirely for a cooldown period rather than retrying each one individually.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">When Retrying Makes Things Worse<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Retrying is not free, and treating it as a default safety net causes specific, predictable problems:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retrying a compliance-layer failure changes nothing about the message&#8217;s validity and just consumes API calls and, depending on your account&#8217;s cost model, potentially credits, while delaying the moment someone notices the actual configuration problem (wrong sender ID, missing DLT template mapping, scrubber match). Fix the input, don&#8217;t retry the output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retrying without checking for <code>IN_RETRY<\/code> status first creates duplicate sends, because the gateway is already handling the retry on your behalf. This is the single most avoidable mistake in home-grown retry logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retrying past a message&#8217;s useful lifetime for the use case wastes effort on a message the recipient no longer needs. A payment confirmation retried 20 minutes later after already failing has usually been superseded by the customer contacting support directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retrying at a fixed short interval across a large batch of failed messages at once can look, from the receiving operator&#8217;s side, like abnormal traffic, and is worth avoiding for account health reasons independent of the retry&#8217;s success probability.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Monitoring Your Retry Pipeline<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Track, at minimum, retry rate by failure code (not just an aggregate retry rate), average number of attempts before final resolution, and the ratio of messages that resolve to <code>DELIVERED<\/code> after a retry versus those that exhaust all attempts. A sudden spike in <code>SYSTEM_FAIL<\/code> or <code>OPERATOR_DOWN<\/code> retry volume across many messages at once is usually a sign of a genuine upstream operator issue rather than something your retry logic can fix by trying harder, and it&#8217;s the trigger to open a support ticket rather than widen your retry cap.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I retry every failed SMS the same number of times?<\/strong> No. Retry count and backoff should depend on the specific failure code. Transport-layer failures can retry quickly. Carrier-layer failures need exponential backoff with a cap. Compliance-layer failures shouldn&#8217;t be retried at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I avoid sending the same message twice on retry?<\/strong> Use the gateway&#8217;s <code>duplicatecheck<\/code> parameter as a safeguard, but the primary defense is application-level state tracking: don&#8217;t fire a new send attempt until you&#8217;ve confirmed the previous attempt&#8217;s delivery report shows a genuinely retryable terminal failure, not a pending or <code>IN_RETRY<\/code> state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What&#8217;s the difference between IN_RETRY and my own retry logic?<\/strong> <code>IN_RETRY<\/code> means the gateway itself is still attempting delivery to the operator on the original send. Firing your own application-level retry while a message is in this state creates a duplicate send once both attempts eventually succeed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there a standard number of retries I should use?<\/strong> Three attempts with exponential backoff is a reasonable default for carrier-layer failures on time-sensitive transactional messages. There&#8217;s no universal number; it should be tuned against your own delivery success data by failure code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should retries use a different sender ID or route?<\/strong> That&#8217;s a fallback strategy, not a retry strategy, and it introduces its own compliance considerations (a message resent under a different registered sender ID may need its own DLT template mapping in India). Treat it as a separate, deliberate decision rather than an automatic retry behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does the DLT registration status affect retry behavior?<\/strong> Yes. If a message fails because of a DLT template mismatch or an unregistered entity, retrying the exact same request will fail identically every time, because the cause is a configuration issue, not a transient one. If you haven&#8217;t completed the DLT registration for your route, that&#8217;s the underlying issue to resolve first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Production-Ready Delivery Starts Here<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Get your delivery report and error code mapping right before scaling retry logic across your message volume. Explore the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/sms-delivery-report\/\">Delivery Report API<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-delivery-error-code-list\/\">Delivery Error Codes reference<\/a>, or <a href=\"https:\/\/unify.smsgateway.center\/signup\/\">sign up<\/a> to test retry logic against a live sandbox account.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A practical retry framework for SMS APIs: which delivery failures are worth retrying, which aren&#8217;t, and how to build idempotent retry logic against real delivery report data instead of guessing.<\/p>\n","protected":false},"author":118,"featured_media":2529,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[86,2016,1994,404,2017,2018,2015,460,481,27,445,597],"class_list":["post-2528","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-bulk-sms","tag-delivery-reports","tag-dlr","tag-dlt","tag-error-handling","tag-idempotency","tag-retry-logic","tag-smpp","tag-sms-api","tag-sms-gateway-center","tag-smsgatewaycenter","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2528","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=2528"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2528\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2529"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2528"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2528"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2528"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}