<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[William QiBin]]></title><description><![CDATA[William QiBin]]></description><link>https://williamqibin.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 15:13:35 GMT</lastBuildDate><atom:link href="https://williamqibin.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your OTP SMS Didn't Fail at Sending — It Failed at Delivery: 4 Checks Developers Miss]]></title><description><![CDATA[Hi, I'm William. I work in CPaaS / A2P messaging, mostly on international SMS delivery for verification, notifications, and alerts.]]></description><link>https://williamqibin.hashnode.dev/your-otp-sms-didn-t-fail-at-sending-it-failed-at-delivery-4-checks-developers-miss</link><guid isPermaLink="true">https://williamqibin.hashnode.dev/your-otp-sms-didn-t-fail-at-sending-it-failed-at-delivery-4-checks-developers-miss</guid><category><![CDATA[sms]]></category><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[William QiBin]]></dc:creator><pubDate>Fri, 23 Oct 2026 09:18:08 GMT</pubDate><content:encoded><![CDATA[<p>Hi, I'm William. I work in CPaaS / A2P messaging, mostly on international SMS delivery for verification, notifications, and alerts.</p>
<p>The most common bug report I hear from developers is:</p>
<blockquote>
<p>"The API returned success, but the user never got the code."</p>
</blockquote>
<p>That <code>success</code> usually only means the message was <em>accepted</em>. It says almost nothing about whether it was <em>delivered</em> — or delivered in time to matter. For OTP, a code that arrives after 60 seconds is effectively a failed code.</p>
<p>Here are the 4 checks I run first.</p>
<h2>1. Route: Is there a direct route to that carrier?</h2>
<p>A cheap route is not a cheap message if it never lands.</p>
<p>For international OTP, ask:</p>
<ul>
<li>Is this a direct carrier connection, or is it hopping through aggregators?</li>
<li>Is the sender ID allowed in the destination country? Many markets reject or rewrite unregistered sender IDs.</li>
<li>Is this route built for OTP traffic, or for bulk marketing traffic? They fail in different ways.</li>
</ul>
<p><strong>Developer check:</strong> Don't test with one number in one country and call it done. Test your top 3 user countries separately. Delivery behaviour is per-country, per-carrier — not global.</p>
<h2>2. Speed: Measure delivery time, not send time</h2>
<p>Log two timestamps, not one:</p>
<pre><code class="language-text">t0 = when your system requested the send
t1 = when the delivery receipt (DLR) says "delivered"
delivery_time = t1 - t0
</code></pre>
<p>If you only log your API call, you're measuring your own server — not the user's experience.</p>
<p>A practical alert I like:</p>
<pre><code class="language-python"># Pseudocode: flag slow OTP delivery for investigation
if delivery_time_seconds &gt; 30:
    log_warning("slow_otp_delivery", country=country, carrier=carrier)
if delivery_status != "DELIVERED":
    trigger_fallback()  # voice OTP, another route, or a retry with a new code
</code></pre>
<h2>3. Receipts: Treat the delivery receipt as part of your product</h2>
<p>No DLR handling means you're flying blind. At minimum, handle:</p>
<table>
<thead>
<tr>
<th>Receipt state</th>
<th>What it usually means</th>
<th>What to do</th>
</tr>
</thead>
<tbody><tr>
<td><code>DELIVERED</code></td>
<td>Handset received it</td>
<td>Stop retrying, mark verification as sent</td>
</tr>
<tr>
<td><code>UNDELIVERED</code> / <code>FAILED</code></td>
<td>Carrier or handset rejected it</td>
<td>Check number format, sender ID, and route</td>
</tr>
<tr>
<td><code>EXPIRED</code></td>
<td>Sat too long in transit</td>
<td>Your route is too slow for OTP — change it</td>
</tr>
<tr>
<td>No receipt at all</td>
<td>Provider gives you no visibility</td>
<td>That's a provider problem, not a user problem</td>
</tr>
</tbody></table>
<p>Wire DLR webhooks into the same dashboard where you watch signup conversion. A sudden drop in delivered rate in one country almost always shows up there <em>before</em> users start complaining.</p>
<h2>4. Support: Can a human look at one message ID?</h2>
<p>When a single important user can't log in, "check our status page" is not support.</p>
<p>Before you commit to a provider, test this in the trial — not after you sign:</p>
<ol>
<li>Send a real message.</li>
<li>Give support that one message ID.</li>
<li>See if they can tell you the carrier, the delivery state, and <em>why</em> it failed.</li>
</ol>
<p>If they can't trace one message, they can't help you debug a country-wide drop either.</p>
<h2>A quick pre-launch checklist</h2>
<ul>
<li><input disabled type="checkbox" /> Phone numbers normalised to E.164 (<code>+</code> + country code) before sending</li>
<li><input disabled type="checkbox" /> Delivery time (not just send time) logged per country</li>
<li><input disabled type="checkbox" /> DLR webhook handled, including <code>EXPIRED</code> and <code>UNDELIVERED</code></li>
<li><input disabled type="checkbox" /> Fallback ready: second route, voice OTP, or WhatsApp where it makes sense</li>
<li><input disabled type="checkbox" /> One-message trace tested with support using a real message ID</li>
</ul>
<hr />
<p><em>Disclosure: I work at Chuanglan (a CPaaS provider). If you want a sandbox to run the checks above, new accounts currently get 100 free SMS messages + a small API credit to test with: <a href="https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU">https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU</a> — no card needed for the trial. Either way, run the 4 checks on whatever provider you use now. Most "mystery" OTP failures turn out to be check #1 or #3.</em></p>
<p>What does your OTP failure dashboard look like today — do you track delivery time per country, or just send success rate? I'd genuinely like to compare notes in the comments.</p>
]]></content:encoded></item></channel></rss>