<?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: Receive SMS</title>
    <description>The latest articles on DEV Community by Receive SMS (@receivesmss).</description>
    <link>https://dev.to/receivesmss</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%2F4246042%2F5657796d-00ef-49e9-a5b1-7524e05c46ee.png</url>
      <title>DEV Community: Receive SMS</title>
      <link>https://dev.to/receivesmss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/receivesmss"/>
    <language>en</language>
    <item>
      <title>Testing SMS OTP sign-up flows with real phone numbers (Python example)</title>
      <dc:creator>Receive SMS</dc:creator>
      <pubDate>Sun, 15 Nov 2026 14:00:09 +0000</pubDate>
      <link>https://dev.to/receivesmss/testing-sms-otp-sign-up-flows-with-real-phone-numbers-python-example-1d3n</link>
      <guid>https://dev.to/receivesmss/testing-sms-otp-sign-up-flows-with-real-phone-numbers-python-example-1d3n</guid>
      <description>&lt;p&gt;If your app sends a one-time code by SMS, sooner or later you need to test that flow with a real number: does the message arrive, in what format, how fast, and does it work for users in other countries? Buying local SIM cards for every country you support doesn't scale.&lt;/p&gt;

&lt;p&gt;One way to do it is with online one-time numbers. You request a number for a country and an app, trigger the SMS from your own sign-up form, and read the code back over HTTP.&lt;/p&gt;

&lt;p&gt;Below is a small Python script that does this with the &lt;a href="https://receive-smss.net" rel="noopener noreferrer"&gt;Receive SMS&lt;/a&gt; API. Full disclosure: I run that service, but the same pattern works with any provider that has a buy-number / poll-for-SMS API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get_number&lt;/code&gt; buys a number for an app and country. Your balance is charged, and refunded automatically if no SMS arrives before the number expires (15 minutes).&lt;/li&gt;
&lt;li&gt;Enter the number in the sign-up form you are testing.&lt;/li&gt;
&lt;li&gt;Poll &lt;code&gt;get_sms&lt;/code&gt; every 5 seconds until the code arrives (it returns HTTP 404 until then).&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;finish&lt;/code&gt; when you are done, or &lt;code&gt;reject_code&lt;/code&gt; to cancel and get refunded if nothing came.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://receive-smss.net/api/v1"&lt;/span&gt;
&lt;span class="no"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bearer YOUR_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{API}/get_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"whatsapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;number_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Use this number:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ... trigger the SMS from the app you are testing ...&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# up to 15 minutes&lt;/span&gt;
    &lt;span class="n"&gt;sms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{API}/get_sms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"n_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;number_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&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;sms&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Code:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sms&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{API}/finish"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"n_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;number_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few useful extras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;max_price&lt;/code&gt; on &lt;code&gt;get_number&lt;/code&gt; makes the call fail instead of paying more than you expect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_rates?country=US&lt;/code&gt; lists apps and prices for a country, so a test suite can pick a country that has stock.&lt;/li&gt;
&lt;li&gt;HTTP 503 means no numbers are available right now, and you are not charged.&lt;/li&gt;
&lt;li&gt;The rate limit is 60 requests a minute; a 429 comes with a &lt;code&gt;Retry-After&lt;/code&gt; header.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things to keep in mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use these numbers for test accounts only. A one-time number is gone after 15 minutes, so never attach it to an account you need to recover later.&lt;/li&gt;
&lt;li&gt;Keep the API key in an environment variable or your CI secrets, not in the repo. Send it in the &lt;code&gt;Authorization&lt;/code&gt; header rather than the query string, because URLs end up in logs.&lt;/li&gt;
&lt;li&gt;Some apps block virtual numbers. That's useful to know too: if your own app is meant to block them, this is an easy way to test that it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full API reference is at &lt;a href="https://receive-smss.net/api-docs" rel="noopener noreferrer"&gt;receive-smss.net/api-docs&lt;/a&gt;. Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>api</category>
      <category>security</category>
    </item>
  </channel>
</rss>
