<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: William QiBin</title>
    <description>The latest articles on DEV Community by William QiBin (@william_qibin).</description>
    <link>https://dev.to/william_qibin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4201913%2F25eb6ff5-a2bd-4507-aedd-b12080910076.png</url>
      <title>DEV Community: William QiBin</title>
      <link>https://dev.to/william_qibin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/william_qibin"/>
    <language>en</language>
    <item>
      <title>International SMS OTP: 4 checks before you ship</title>
      <dc:creator>William QiBin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:34:40 +0000</pubDate>
      <link>https://dev.to/william_qibin/international-sms-otp-4-checks-before-you-ship-13k8</link>
      <guid>https://dev.to/william_qibin/international-sms-otp-4-checks-before-you-ship-13k8</guid>
      <description>&lt;p&gt;Getting an SMS OTP flow working in one market is only the first step. Before launching across countries, I’d check four things: number formatting, message status, retry behavior, and the business funnel.&lt;/p&gt;

&lt;p&gt;I work at Chuanglan Yunzhi, a CPaaS company. This is a practical checklist from an employee’s perspective, not an independent vendor comparison. The article was drafted with AI assistance and reviewed by the author.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalize numbers, but don’t confuse format with reachability&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For APIs that require E.164, a number is generally represented as “+” followed by the country calling code and subscriber number, without punctuation. For example, Twilio’s documentation shows +14155551234 as an E.164-format example.&lt;/p&gt;

&lt;p&gt;A valid-looking format does not prove that a number is active or can receive SMS. Keep country/region selection explicit, avoid applying one market’s fixed-length rules globally, and distinguish format validation from actual delivery.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://www.twilio.com/docs/phone-numbers/api/incomingphonenumber-resource" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/phone-numbers/api/incomingphonenumber-resource&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Separate “accepted” from “delivered”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An API response may only mean that a request was accepted or queued. In Twilio’s Messages API, for example, “queued” and “delivered” are distinct states. Other providers may use different names or expose different status callbacks, so use the documentation for the service you actually integrate.&lt;/p&gt;

&lt;p&gt;For troubleshooting, track three separate stages:&lt;/p&gt;

&lt;p&gt;Request accepted: request time, your correlation ID, provider message ID, and error code.&lt;/p&gt;

&lt;p&gt;Delivery status: available receipt, its timestamp, and any failure reason.&lt;/p&gt;

&lt;p&gt;Business result: whether the user completed OTP verification, how long it took, and how many resend attempts occurred.&lt;/p&gt;

&lt;p&gt;A delivery receipt is not proof that a person read the message or completed signup.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://www.twilio.com/docs/messaging/api/message-resource" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/messaging/api/message-resource&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put retry controls on the backend too&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a message is delayed, users may tap “resend” repeatedly. That can create duplicate messages, extra cost, and abuse risk. A frontend countdown alone is not enough.&lt;/p&gt;

&lt;p&gt;Before launch, decide how your backend handles rate limits, duplicate requests, unknown outcomes after a timeout, code expiry, and repeated incorrect attempts. Avoid logging OTP values or full sensitive identifiers in ordinary application logs. Retry limits and backoff should follow your product’s risk and the provider’s documented behavior; one provider’s defaults are not universal.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://www.twilio.com/docs/verify/developer-best-practices" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/verify/developer-best-practices&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure the whole signup path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Track the flow as separate events:&lt;/p&gt;

&lt;p&gt;OTP requested → request accepted → delivery status available → code verified → signup completed.&lt;/p&gt;

&lt;p&gt;When conversion drops, compare the stages before attributing the problem to the SMS route. Test the countries or regions you actually plan to support, using the relevant provider documentation and applicable local requirements.&lt;/p&gt;

&lt;p&gt;A short pre-launch review:&lt;/p&gt;

&lt;p&gt;1) Are target markets and message use cases defined?&lt;br&gt;
2) Does number handling match the integrated API’s requirements?&lt;br&gt;
3) Can you correlate requests, receipts, and signup outcomes?&lt;br&gt;
4) Are timeout, retry, and rate-limit behaviors explicit?&lt;br&gt;
5) Have local requirements, pricing, and billing terms been checked for each target market?&lt;/p&gt;

&lt;p&gt;If you’re evaluating international SMS for an application, you can review Chuanglan’s registration page here:&lt;br&gt;
&lt;a href="https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU" rel="noopener noreferrer"&gt;https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I work at Chuanglan Yunzhi. The link contains the referral code LKFHZWBU; I have not verified whether it confers any reward or other benefit. Product capabilities, availability, and terms should be confirmed from official materials and your own evaluation.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>security</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Your OTP SMS Didn't Fail at Sending — It Failed at Delivery: 4 Checks Developers Miss</title>
      <dc:creator>William QiBin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:29:01 +0000</pubDate>
      <link>https://dev.to/william_qibin/your-otp-sms-didnt-fail-at-sending-it-failed-at-delivery-4-checks-developers-miss-1ijd</link>
      <guid>https://dev.to/william_qibin/your-otp-sms-didnt-fail-at-sending-it-failed-at-delivery-4-checks-developers-miss-1ijd</guid>
      <description>&lt;p&gt;Hi, I'm William. I work in CPaaS / A2P messaging, mostly on international SMS delivery for verification, notifications, and alerts.&lt;/p&gt;

&lt;p&gt;The most common bug report I hear from developers is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The API returned success, but the user never got the code."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That &lt;code&gt;success&lt;/code&gt; usually only means the message was &lt;em&gt;accepted&lt;/em&gt;. It says almost nothing about whether it was &lt;em&gt;delivered&lt;/em&gt; — or delivered in time to matter. For OTP, a code that arrives after 60 seconds is effectively a failed code.&lt;/p&gt;

&lt;p&gt;Here are the 4 checks I run first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Route: Is there a direct route to that carrier?
&lt;/h2&gt;

&lt;p&gt;A cheap route is not a cheap message if it never lands.&lt;/p&gt;

&lt;p&gt;For international OTP, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a direct carrier connection, or is it hopping through aggregators?&lt;/li&gt;
&lt;li&gt;Is the sender ID allowed in the destination country? Many markets reject or rewrite unregistered sender IDs.&lt;/li&gt;
&lt;li&gt;Is this route built for OTP traffic, or for bulk marketing traffic? They fail in different ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Developer check:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Speed: Measure delivery time, not send time
&lt;/h2&gt;

&lt;p&gt;Log two timestamps, not one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t0 = when your system requested the send
t1 = when the delivery receipt (DLR) says "delivered"
delivery_time = t1 - t0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only log your API call, you're measuring your own server — not the user's experience.&lt;/p&gt;

&lt;p&gt;A practical alert I like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudocode: flag slow OTP delivery for investigation
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delivery_time_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;log_warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slow_otp_delivery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delivery_status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELIVERED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;trigger_fallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# voice OTP, another route, or a retry with a new code
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Receipts: Treat the delivery receipt as part of your product
&lt;/h2&gt;

&lt;p&gt;No DLR handling means you're flying blind. At minimum, handle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Receipt state&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELIVERED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handset received it&lt;/td&gt;
&lt;td&gt;Stop retrying, mark verification as sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;UNDELIVERED&lt;/code&gt; / &lt;code&gt;FAILED&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Carrier or handset rejected it&lt;/td&gt;
&lt;td&gt;Check number format, sender ID, and route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sat too long in transit&lt;/td&gt;
&lt;td&gt;Your route is too slow for OTP — change it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No receipt at all&lt;/td&gt;
&lt;td&gt;Provider gives you no visibility&lt;/td&gt;
&lt;td&gt;That's a provider problem, not a user problem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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 &lt;em&gt;before&lt;/em&gt; users start complaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Support: Can a human look at one message ID?
&lt;/h2&gt;

&lt;p&gt;When a single important user can't log in, "check our status page" is not support.&lt;/p&gt;

&lt;p&gt;Before you commit to a provider, test this in the trial — not after you sign:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a real message.&lt;/li&gt;
&lt;li&gt;Give support that one message ID.&lt;/li&gt;
&lt;li&gt;See if they can tell you the carrier, the delivery state, and &lt;em&gt;why&lt;/em&gt; it failed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If they can't trace one message, they can't help you debug a country-wide drop either.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick pre-launch checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Phone numbers normalised to E.164 (&lt;code&gt;+&lt;/code&gt; + country code) before sending&lt;/li&gt;
&lt;li&gt;[ ] Delivery time (not just send time) logged per country&lt;/li&gt;
&lt;li&gt;[ ] DLR webhook handled, including &lt;code&gt;EXPIRED&lt;/code&gt; and &lt;code&gt;UNDELIVERED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Fallback ready: second route, voice OTP, or WhatsApp where it makes sense&lt;/li&gt;
&lt;li&gt;[ ] One-message trace tested with support using a real message ID&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;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: &lt;a href="https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU" rel="noopener noreferrer"&gt;https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU&lt;/a&gt; — 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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>sms</category>
      <category>api</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
