<?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: Mike Davis</title>
    <description>The latest articles on DEV Community by Mike Davis (@sms-activate).</description>
    <link>https://dev.to/sms-activate</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3753357%2F014bebc1-4528-4bc4-a501-3e37347c5d74.png</url>
      <title>DEV Community: Mike Davis</title>
      <link>https://dev.to/sms-activate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sms-activate"/>
    <language>en</language>
    <item>
      <title>SMS-Activate Shut Down — Finding a Real Alternative for My CI/CD Pipeline</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Mon, 06 Apr 2026 15:12:18 +0000</pubDate>
      <link>https://dev.to/sms-activate/sms-activate-shut-down-finding-a-real-alternative-for-my-cicd-pipeline-4mma</link>
      <guid>https://dev.to/sms-activate/sms-activate-shut-down-finding-a-real-alternative-for-my-cicd-pipeline-4mma</guid>
      <description>&lt;h2&gt;
  
  
  After SMS-Activate closed in December 2025, I tested five SMS activate alternatives to automate phone verification in E2E tests. Code examples and honest results.
&lt;/h2&gt;

&lt;p&gt;Last October, our QA pipeline broke at 2 AM. The reason? My personal phone number — the one hardcoded into 14 different Playwright specs — got rate-limited by Twilio's verification flow. Every test that touched 2FA went red. PagerDuty woke me up, and I spent the next hour manually rotating a number from a prepaid SIM I kept in my desk drawer.&lt;/p&gt;

&lt;p&gt;I'd been meaning to set up a proper virtual number provider for months. We had an SMS-Activate account that one of our QA guys used for manual testing, but I never got around to integrating its API into the pipeline. Then, on December 29, SMS-Activate shut down entirely — and suddenly I had no choice but to find a real solution.&lt;/p&gt;

&lt;p&gt;This post is about the five SMS-Activate alternatives I tested, what worked, what didn't, and the setup we ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;If you've ever built anything with phone-based auth, you know the pain. You need real phone numbers that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accept OTP codes from services like Google, Telegram, or your own Twilio/Vonage setup&lt;/li&gt;
&lt;li&gt;can be provisioned programmatically (no human in the loop)&lt;/li&gt;
&lt;li&gt;don't get flagged as VoIP by strict verification systems&lt;/li&gt;
&lt;li&gt;cost less than the mass of prepaid SIMs gathering dust in your office&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our stack is a Django REST backend + Next.js frontend, deployed via GitHub Actions. We run ~200 E2E tests per push to &lt;code&gt;main&lt;/code&gt;, and about 30 of them involve SMS-based flows: sign-up, password reset, 2FA toggle, phone number change. Each one needs a unique, working phone number.&lt;/p&gt;

&lt;p&gt;I spent roughly three weeks testing different SMS-Activate alternatives. Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Was Looking For
&lt;/h2&gt;

&lt;p&gt;Before I get into specific services, here's the criteria I used. This isn't a generic "best tools" list — these are the things that mattered for our CI/CD context:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API-first.&lt;/strong&gt; If I can't call it from a shell script or a pytest fixture, it doesn't exist for me. I need to request a number, poll for an incoming SMS, and release the number — all via HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number freshness.&lt;/strong&gt; Reused numbers are the #1 cause of failed verifications. If the number was already used to register a Google account last week, it'll get rejected. I need services that cycle through large pools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Country coverage.&lt;/strong&gt; We test geo-specific onboarding flows for US, DE, ID, and BR users. I need numbers from at least these four countries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost at scale.&lt;/strong&gt; At 30 tests × ~5 CI runs per day × 22 workdays, that's 3,300 numbers/month. Price per number matters a lot at this volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; If SMS delivery takes more than 20 seconds, my test times out. I set a hard ceiling of 15s for OTP receipt.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Testing Setup
&lt;/h2&gt;

&lt;p&gt;Here's the simplified pytest fixture I ended up with. The actual implementation varies per provider, but the interface is the same:&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;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SMSProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Abstract base for virtual number providers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Provision a temporary number, yield it, then release.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_number&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this abstraction, switching providers is a one-line config change. Which came in handy — because my first choice didn't work out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Services I Actually Tested
&lt;/h2&gt;

&lt;p&gt;I originally tried seven, but two had APIs that returned 500s on the first call, so I'm not going to waste your time with those. Here are the five that at least functioned.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. HeroSMS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://hero-sms.com/" rel="noopener noreferrer"&gt;hero-sms.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This ended up being our daily driver, so I'll go deeper here.&lt;/p&gt;

&lt;p&gt;I found HeroSMS while digging through a Telegram group where people discuss automation tooling. The API is straightforward — REST with JSON responses, no OAuth dance, just an API key in the header.&lt;/p&gt;

&lt;p&gt;What convinced me to keep testing it past the trial phase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number pool size.&lt;/strong&gt; They claim 500K+ fresh numbers daily, and in practice, I've never hit a "no numbers available" wall, even for Google verifications from US numbers at peak hours. Over three months of daily CI runs, our success rate for OTP receipt sits at 94.7% — the remaining ~5% are timeouts on the service side (Google being slow), not HeroSMS failing to deliver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing at our volume.&lt;/strong&gt; Numbers start at fractions of a cent. For our mix of US/DE/ID/BR numbers across Google, Telegram, and our own app, the average cost came out to ~$0.03/verification. At 3,300/month, that's under $100/month for the entire test suite. Before this, we were spending ~$40/month on prepaid SIMs and still running out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API response time.&lt;/strong&gt; Number provisioning takes 1–3 seconds. SMS delivery averages 4–8 seconds in my logs. Well within the 15-second budget.&lt;/p&gt;

&lt;p&gt;Here's the wrapper I wrote:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HeroSMSProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SMSProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hero-sms.com/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/get-number&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&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="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&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;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
        &lt;span class="k"&gt;while&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;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/get-status&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activation_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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;2&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;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No OTP received within &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; payments are crypto-only. We had to set up a company crypto wallet just for this, which was a minor bureaucratic headache. If your finance team is allergic to crypto, this might be a dealbreaker. For us, it was a 30-minute setup and then we forgot about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SMSPool
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://www.smspool.net/" rel="noopener noreferrer"&gt;smspool.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMSPool was my second choice and the one I recommend if you need &lt;strong&gt;non-VoIP numbers specifically&lt;/strong&gt;. Some services (looking at you, Google Voice and Cash App) reject VoIP numbers outright. SMSPool has a pool of real-SIM-backed numbers that pass these checks.&lt;/p&gt;

&lt;p&gt;The API is decent — not as fast as HeroSMS, but reliable. Average OTP delivery was 8–12 seconds in my tests. They accept Stripe payments, which made our finance team happier.&lt;/p&gt;

&lt;p&gt;Where it fell short for us: number availability for Indonesian numbers was inconsistent. Some days we'd get them instantly, other days the queue would timeout. For US and DE numbers, no issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; teams that primarily need US/EU non-VoIP numbers and want to pay with a regular credit card.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 5SIM
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://5sim.net/" rel="noopener noreferrer"&gt;5sim.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5SIM is the cheapest option I tested — numbers from $0.008. The coverage is wide (180+ countries), and for low-volume or manual testing, it's genuinely fine.&lt;/p&gt;

&lt;p&gt;The problem showed up at scale. Out of 500 test runs over two weeks, about 12% of numbers failed to receive OTPs. That's significantly higher than what I saw with HeroSMS (5%) or SMSPool (7%). The failures were inconsistent — sometimes it was a US Google verification, sometimes a German Telegram code. Hard to debug, harder to trust in CI.&lt;/p&gt;

&lt;p&gt;I still keep a 5SIM balance for ad-hoc manual testing when I need a number from an unusual country (say, Bangladesh or Peru). It's great for that. I wouldn't run my pipeline on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; manual testing, exploratory work, tight budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. SMS-Man
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://sms-man.com/" rel="noopener noreferrer"&gt;sms-man.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMS-Man has the broadest service catalog I've seen — 1,000+ supported services. If you need to verify some obscure Chinese app or a niche social network, they probably have it.&lt;/p&gt;

&lt;p&gt;The API works, the Telegram bot is a nice touch for quick manual grabs, and the pricing is reasonable ($0.05+). But for my use case (automated CI), the speed wasn't there. Average OTP delivery was 10–15 seconds, and about 8% of requests hit my timeout ceiling. The free shared numbers are a cool idea for quick one-off tests but obviously not suitable for anything serious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; wide service coverage, manual testing via Telegram bot, migrating from SMS-Activate (similar workflow).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Veritel
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://www.veritel.io/" rel="noopener noreferrer"&gt;veritel.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Veritel's selling point is that every number is backed by a physical SIM card connected to actual hardware. This means near-carrier-grade reliability — and their stats back it up. In my tests, OTP delivery success was 96%, the highest of any service I tried.&lt;/p&gt;

&lt;p&gt;The trade-off is price. At ~$0.10+ per number, Veritel is roughly 3x more expensive than HeroSMS for our volume. At 3,300 numbers/month, that's ~$330 vs ~$100. We couldn't justify the difference given that HeroSMS was already clearing our success threshold.&lt;/p&gt;

&lt;p&gt;I'd use Veritel for a specific scenario: if you're testing against a platform that's extremely aggressive about rejecting virtual numbers (think: banking apps, fintech verification), Veritel's physical SIM approach is worth the premium.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; fintech/banking verification testing, platforms that reject VoIP aggressively.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Settled On
&lt;/h2&gt;

&lt;p&gt;HeroSMS runs our daily CI. SMSPool is our fallback for the rare case when we need a guaranteed non-VoIP US number. 5SIM sits in my personal toolkit for ad-hoc exploration.&lt;/p&gt;

&lt;p&gt;The total monthly cost went from "$40/month on SIMs that still didn't cover our test matrix" to "~$100/month with full automation and 95% success rates." More importantly, no one has been woken up at 2 AM because a hardcoded phone number got rate-limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Tips If You're Setting This Up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't hardcode the provider.&lt;/strong&gt; Use the abstraction pattern from my fixture above. You &lt;em&gt;will&lt;/em&gt; switch providers at some point — make it painless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set a timeout and retry with a different number.&lt;/strong&gt; Not every number works every time. My fixture has a retry wrapper that requests a new number if OTP doesn't arrive within 15 seconds. Two retries max, then fail the test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track success rates.&lt;/strong&gt; I log every verification attempt with provider, country, service, time-to-OTP, and success/failure. This data is what told me HeroSMS was outperforming the others — gut feeling alone would've kept me on 5SIM because it's cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use country-specific numbers for geo-tests.&lt;/strong&gt; Don't just grab "any available number." If your test checks that a German user sees the German onboarding flow, use a DE number. It matters for some services' fraud detection.&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;# In your CI config (e.g., .github/workflows/e2e.yml)
&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;SMS_PROVIDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;herosms&lt;/span&gt;
  &lt;span class="n"&gt;SMS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HEROSMS_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="n"&gt;SMS_FALLBACK_PROVIDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;smspool&lt;/span&gt;
  &lt;span class="n"&gt;SMS_FALLBACK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SMSPOOL_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The SMS-Activate shutdown caught a lot of people off guard, but honestly, the market for SMS-Activate alternatives in 2026 is more competitive and mature than what we had before. Services now compete on API reliability and delivery speed, not just price.&lt;/p&gt;

&lt;p&gt;Virtual number services aren't glamorous tooling. Nobody's giving a conference talk about their SMS verification pipeline. But if you ship anything with phone-based auth — and in 2026, that's most of us — having a reliable, automated verification flow in your tests is the kind of infrastructure investment that pays for itself the first time it prevents a broken release.&lt;/p&gt;

&lt;p&gt;If you've solved this differently — maybe with a mock layer, or a self-hosted SMS gateway — I'd be curious to hear about it. My approach works, but I'm sure there are better ones.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The code examples above are simplified for readability. In production, add proper error handling, rate limiting on your side, and secret management. Don't commit API keys to your repo — use your CI platform's secret store.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>The Hidden Cost of Phone-Based Auth: What I Learned After 18 Months</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Wed, 01 Apr 2026 10:35:45 +0000</pubDate>
      <link>https://dev.to/sms-activate/the-hidden-cost-of-phone-based-auth-what-i-learned-after-18-months-55mc</link>
      <guid>https://dev.to/sms-activate/the-hidden-cost-of-phone-based-auth-what-i-learned-after-18-months-55mc</guid>
      <description>&lt;p&gt;We added SMS verification to our app expecting it to be simple. 18 months later, I have opinions about carrier filtering, Twilio bills, and abuse bots.&lt;/p&gt;




&lt;p&gt;Eighteen months ago, our PM walked into standup and said: "We need phone verification for sign-up. Users are creating fake accounts with disposable emails. Should be a weekend project, right?"&lt;/p&gt;

&lt;p&gt;It was not a weekend project.&lt;/p&gt;

&lt;p&gt;What followed was a year and a half of unexpected Twilio invoices, ghost messages eaten by carrier filters, an abuse wave that nearly bankrupted our SMS budget, and a slow realization that phone-based auth is one of those things that looks simple on a whiteboard and gets complicated the moment real humans in real countries try to use it.&lt;/p&gt;

&lt;p&gt;This isn't a tutorial on how to implement 2FA. There are plenty of those. This is the stuff nobody warned me about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Twilio Bill That Made Finance Call Me
&lt;/h2&gt;

&lt;p&gt;Our app is a B2B SaaS tool — think project management for small agencies. We launched phone verification for sign-up in March 2024. The plan was straightforward: user enters phone number, we send a 6-digit code via Twilio, user enters code, done.&lt;/p&gt;

&lt;p&gt;I estimated the cost based on our existing user growth: ~800 new sign-ups per month, $0.0079 per SMS to US numbers. That's about $6.30/month. I told finance to expect $10–15/month with some buffer.&lt;/p&gt;

&lt;p&gt;The first month's bill was $47.&lt;/p&gt;

&lt;p&gt;Here's what I didn't account for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries.&lt;/strong&gt; Users don't always receive the first SMS. They click "resend." On average, each successful verification took 1.4 messages — some users hit resend 3–4 times. That's a 40% overhead on every estimate you'll ever make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;International numbers.&lt;/strong&gt; We're US-based, but about 30% of our users aren't. Sending an SMS to India costs $0.0262. Germany is $0.0637. UAE is $0.0587. That "30% international" slice was eating 70% of the budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed deliveries you still pay for.&lt;/strong&gt; Twilio charges you when the message is accepted by the carrier, not when the user receives it. If the carrier silently drops it — and they do, more often than you'd think — you still pay.&lt;/p&gt;

&lt;p&gt;By month three, we were spending $120/month. By month eight, after a Product Hunt launch bumped sign-ups, we hit $340 in a single month. Not catastrophic, but a far cry from the "$10–15" I promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carrier Filtering: The Silent Killer
&lt;/h2&gt;

&lt;p&gt;The cost was annoying. The delivery failures were worse.&lt;/p&gt;

&lt;p&gt;Around month four, our verification success rate dropped from ~92% to ~71% over two weeks. No code changes. No infrastructure issues. Support tickets flooded in: "I never received the code."&lt;/p&gt;

&lt;p&gt;It took me three days to figure out what happened. The answer: carrier filtering.&lt;/p&gt;

&lt;p&gt;Mobile carriers run spam filters on application-to-person (A2P) SMS traffic. If your messages look like they're coming from a bot — same content, high volume, short codes — carriers can silently drop them. No error code. No bounce notification. The message just vanishes.&lt;/p&gt;

&lt;p&gt;What triggered it for us was a combination of factors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message template.&lt;/strong&gt; Our OTP message was: &lt;code&gt;Your verification code is: 482901&lt;/code&gt;. That's basically the platonic ideal of an A2P message. Every spam filter in the world is trained to recognize this pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume spike.&lt;/strong&gt; The Product Hunt traffic tripled our daily SMS volume overnight. Sudden spikes are a classic spam signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared short code.&lt;/strong&gt; We were using Twilio's shared short code pool. Our messages were being sent from the same numbers that other Twilio customers used — some of whom were probably sending actual spam.&lt;/p&gt;

&lt;p&gt;The fix involved three changes:&lt;/p&gt;

&lt;p&gt;First, I switched to a dedicated Twilio Messaging Service with a registered A2P 10DLC campaign. This is Twilio's system for whitelisting your traffic with US carriers. The registration process took two weeks and cost $15 — but delivery rates jumped back to 91%.&lt;/p&gt;

&lt;p&gt;Second, I added slight variations to the message body. Instead of the same template every time, I rotate between a few versions and include the app name. Small thing, but it helps avoid pattern-based filtering.&lt;/p&gt;

&lt;p&gt;Third — and this was the expensive one — I added Twilio Verify as a fallback. When our primary SMS channel fails, we fall back to Twilio's managed verification service, which handles carrier negotiation on their end. It costs more per message ($0.05 vs $0.0079), but the delivery rate is significantly higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Abuse Wave
&lt;/h2&gt;

&lt;p&gt;Month eleven. A Wednesday morning. I'm looking at our dashboard and notice something odd: 2,400 new accounts created in the last 6 hours. Our normal daily sign-up rate is about 40.&lt;/p&gt;

&lt;p&gt;Someone was bot-registering accounts using virtual phone numbers.&lt;/p&gt;

&lt;p&gt;The irony is not lost on me — I now use virtual numbers myself for testing (I wrote about that in &lt;a href="https://dev.to/"&gt;my previous post about mocking 2FA&lt;/a&gt;). But being on the receiving end of it was educational.&lt;/p&gt;

&lt;p&gt;Here's what the attack looked like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated script hits our registration API&lt;/li&gt;
&lt;li&gt;Provides a phone number from a virtual number pool (we later identified the numbers as VoIP-based, mostly from Southeast Asian providers)&lt;/li&gt;
&lt;li&gt;Receives OTP via the virtual number service&lt;/li&gt;
&lt;li&gt;Completes registration&lt;/li&gt;
&lt;li&gt;Immediately uses the account to post spam in our public project boards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attacker was spending maybe $0.02 per virtual number. We were spending $0.05–0.10 per verification attempt (including retries and Twilio Verify fallback). They were literally making us pay to get spammed.&lt;/p&gt;

&lt;p&gt;We burned through $180 in Twilio credits in a single morning before I shut it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Did About It
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Short-term:&lt;/strong&gt; I added rate limiting by IP and by phone number prefix. More than 5 registrations from the same /24 IP block in an hour → CAPTCHA. More than 3 attempts with the same country code + first 4 digits → block for 30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium-term:&lt;/strong&gt; I integrated a phone number intelligence API (Twilio Lookup) to check whether a number is VoIP, landline, or mobile before sending the OTP. VoIP numbers get an extra verification step (email confirmation + CAPTCHA). This costs $0.005 per lookup but saved us far more in prevented abuse.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_number_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check if a phone number is mobile, landline, or VoIP.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;twilio_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;phone_numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line_type_intelligence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;line_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line_type_intelligence&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;line_type&lt;/span&gt;  &lt;span class="c1"&gt;# "mobile", "landline", "voip", "unknown"
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_require_extra_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;number_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_number_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;number_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Long-term:&lt;/strong&gt; We added passkey support as the primary auth method and made phone verification optional for users who set up a passkey. More on this below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Country Problem
&lt;/h2&gt;

&lt;p&gt;I mentioned international SMS costs earlier, but there's a deeper issue: SMS doesn't work the same way everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;India&lt;/strong&gt; requires pre-registered DLT templates. If you're sending OTP messages to Indian numbers, you need to register your message template with the Telecom Regulatory Authority of India through a DLT portal. Twilio can handle this, but the registration takes 1–3 weeks and requires a local business entity or partner. We didn't know this until Indian users started reporting zero delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;China&lt;/strong&gt; is effectively off-limits for most Western SMS providers. Carrier restrictions, content filtering, and regulatory requirements make it nearly impossible to reliably deliver A2P SMS from a US-based Twilio account. We ended up not supporting Chinese phone numbers for verification and offering email + TOTP as an alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brazil&lt;/strong&gt; has specific regulations around SMS consent and a DNC (Do Not Call) registry that applies to certain types of SMS. We had to add explicit opt-in language to our verification flow for Brazilian numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Several African countries&lt;/strong&gt; have delivery rates below 60% through Twilio. We tested Vonage as a secondary provider for specific routes and saw marginally better results for Nigerian and Kenyan numbers, but it's still not great.&lt;/p&gt;

&lt;p&gt;The lesson: if your user base is international, budget 2–3x what you'd estimate for US-only, and expect to spend significant engineering time on country-specific edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Auth Method Comparison I Wish I'd Done Earlier
&lt;/h2&gt;

&lt;p&gt;After 18 months of dealing with all of the above, I sat down and compared the actual cost and reliability of every auth method we could reasonably offer. Here's the real-world data from our app:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SMS OTP&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup effort: Medium (Twilio integration, A2P registration, carrier troubleshooting)&lt;/li&gt;
&lt;li&gt;Per-verification cost: $0.02–0.08 (varies wildly by country)&lt;/li&gt;
&lt;li&gt;Delivery success: ~91% US, ~75–85% international&lt;/li&gt;
&lt;li&gt;Abuse resistance: Low without additional checks (Lookup API, CAPTCHA)&lt;/li&gt;
&lt;li&gt;User friction: Medium (wait for SMS, type code)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Email magic links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup effort: Low (you already have an email provider)&lt;/li&gt;
&lt;li&gt;Per-verification cost: ~$0.001 (practically free with any transactional email service)&lt;/li&gt;
&lt;li&gt;Delivery success: ~97% (spam folders are the main failure mode)&lt;/li&gt;
&lt;li&gt;Abuse resistance: Medium (disposable email services exist, but they're easier to block)&lt;/li&gt;
&lt;li&gt;User friction: Medium-High (switch to email app, find message, click link, switch back)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TOTP (authenticator app)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup effort: Low (pyotp + QR code generation)&lt;/li&gt;
&lt;li&gt;Per-verification cost: $0.00 (runs locally on user's device)&lt;/li&gt;
&lt;li&gt;Delivery success: 100% (no network dependency)&lt;/li&gt;
&lt;li&gt;Abuse resistance: High (requires a real device with an authenticator app)&lt;/li&gt;
&lt;li&gt;User friction: High for setup, Low for subsequent use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Passkeys / WebAuthn&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup effort: High (browser API, credential storage, fallback flows)&lt;/li&gt;
&lt;li&gt;Per-verification cost: $0.00&lt;/li&gt;
&lt;li&gt;Delivery success: 100% (local biometric/PIN)&lt;/li&gt;
&lt;li&gt;Abuse resistance: Very High (tied to physical device)&lt;/li&gt;
&lt;li&gt;User friction: Very Low once set up (fingerprint/face)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looking at this table, SMS is the worst option on almost every metric except one: &lt;strong&gt;user familiarity&lt;/strong&gt;. Everyone knows how to receive a text message. Not everyone knows what an authenticator app is. Passkeys are amazing technically, but try explaining them to a non-technical user who just wants to sign up for a project management tool.&lt;/p&gt;

&lt;p&gt;That familiarity is worth a lot. It's the reason SMS verification isn't going anywhere despite being expensive, unreliable, and abuse-prone. People understand it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Landed
&lt;/h2&gt;

&lt;p&gt;Today, our auth flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Primary:&lt;/strong&gt; Email magic link for registration (cheap, reliable, good enough for initial sign-up)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional upgrade:&lt;/strong&gt; Passkey enrollment after first login (we nudge users with a dismissible banner)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone verification:&lt;/strong&gt; Required only for specific actions (inviting team members, accessing billing, exporting data) — not for basic sign-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TOTP:&lt;/strong&gt; Available as an alternative to phone verification for users who prefer it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This reduced our Twilio bill from $340/month at peak to about $80/month, because we're only sending SMS for high-value actions, not every single registration. The abuse problem essentially disappeared because bots can't use email magic links to get to the phone-gated features without maintaining a persistent session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with email-first auth, add phone later.&lt;/strong&gt; We went straight to phone verification because it felt more "secure." It is, marginally — but the operational cost is 10–50x higher than email, and the security improvement is negligible for most apps that aren't handling financial transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget for international SMS from day one.&lt;/strong&gt; If you have any international users, multiply your US-only SMS cost estimate by 3x. If you have users in India, China, or parts of Africa, multiply by 5x and add a line item for "country-specific compliance headaches."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implement phone type checking before sending.&lt;/strong&gt; The $0.005 Twilio Lookup call pays for itself many times over by filtering VoIP numbers, catching typos (landline numbers), and preventing abuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run your verification flow through real E2E tests.&lt;/strong&gt; I wrote about this in my &lt;a href="https://dev.to/"&gt;previous post&lt;/a&gt; — mocking your OTP layer hides bugs that only show up when real carriers are involved. I'm currently setting up a nightly test suite that uses actual virtual numbers to test our verification flow end-to-end. Still iterating on which providers work best for this — will share the results once I have enough data.&lt;/p&gt;




&lt;p&gt;Phone-based auth is one of those features that's deceptively simple. The happy path takes a day to build. Everything else takes months. If you're about to add SMS verification to your app, I hope this saves you some of the surprises I ran into.&lt;/p&gt;

&lt;p&gt;If you've been through this — especially the carrier filtering and international delivery issues — I'd genuinely like to hear how you handled it. My solutions work, but they're held together with more duct tape than I'd like to admit.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why I Stopped Mocking 2FA in Tests (And What I Do Instead)</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Wed, 01 Apr 2026 10:31:47 +0000</pubDate>
      <link>https://dev.to/sms-activate/why-i-stopped-mocking-2fa-in-tests-and-what-i-do-instead-1epk</link>
      <guid>https://dev.to/sms-activate/why-i-stopped-mocking-2fa-in-tests-and-what-i-do-instead-1epk</guid>
      <description>&lt;p&gt;I mocked OTP verification for two years. Then a bug hit production that my test suite should have caught. Here's what went wrong and how I fixed the approach.&lt;/p&gt;




&lt;p&gt;I'll start with the bug that changed my mind.&lt;/p&gt;

&lt;p&gt;Last summer, we pushed a release that broke the phone number change flow in our app. A user would enter their new number, receive an OTP, type it in — and get a 400 error. The code was correct in every way except one: someone refactored the verification endpoint and accidentally changed the OTP field name from &lt;code&gt;otp_code&lt;/code&gt; to &lt;code&gt;verification_code&lt;/code&gt; in the request body. The frontend still sent &lt;code&gt;otp_code&lt;/code&gt;. The backend no longer recognized it.&lt;/p&gt;

&lt;p&gt;Our test suite had 100% coverage on this flow. Every test passed. Green across the board.&lt;/p&gt;

&lt;p&gt;Why? Because we were mocking the entire OTP layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mock That Lies
&lt;/h2&gt;

&lt;p&gt;Here's roughly what our test setup looked 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;# conftest.py — the old way
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mock_otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Skip OTP verification entirely in tests.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKIP_OTP_VERIFICATION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In the actual verification view:
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SKIP_OTP_VERIFICATION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;otp_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;otp_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... actual verification logic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You see the problem. When &lt;code&gt;SKIP_OTP_VERIFICATION&lt;/code&gt; is set, the function returns early. It never reads &lt;code&gt;request.data&lt;/code&gt;. It never checks field names. It never calls the downstream SMS provider. The entire code path that actually matters — the one users hit in production — is invisible to the test suite.&lt;/p&gt;

&lt;p&gt;We weren't testing our verification flow. We were testing a shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But Everyone Does This"
&lt;/h2&gt;

&lt;p&gt;I know, I know. Mocking external dependencies is Testing 101. And to be fair, there are legitimate reasons people mock OTP:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Waiting for a real SMS adds 5–15 seconds per test. If you have 30 verification tests, that's up to 7 extra minutes per CI run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Services like Twilio charge per SMS. At $0.0079/message for US numbers, 30 tests × 5 CI runs/day × 22 days = $25/month just for tests. Not huge, but it adds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flakiness.&lt;/strong&gt; Real SMS delivery is non-deterministic. Networks hiccup. Carrier filtering eats messages. Timeouts happen. Nobody wants a red build because Vodafone's gateway in Frankfurt had a bad morning.&lt;/p&gt;

&lt;p&gt;These are valid concerns. I'm not going to pretend otherwise. But there's a spectrum between "mock everything" and "mock nothing," and I was way too far on the wrong end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Mock Was Actually Hiding
&lt;/h2&gt;

&lt;p&gt;After the &lt;code&gt;otp_code&lt;/code&gt; → &lt;code&gt;verification_code&lt;/code&gt; incident, I did an audit. I went through every test that involved OTP and asked: "What production behavior is this test actually exercising?"&lt;/p&gt;

&lt;p&gt;The answer was depressing. Here's what our mocks were hiding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field name mismatches.&lt;/strong&gt; The bug that started this whole thing. Mock returns early, never reads the request body, never catches field renames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing issues.&lt;/strong&gt; Our OTP codes expire after 5 minutes. We had a bug where the expiry check used &lt;code&gt;datetime.now()&lt;/code&gt; instead of &lt;code&gt;datetime.utcnow()&lt;/code&gt;, causing failures for users in certain timezones. The mock bypassed the expiry check entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; We rate-limit OTP requests to 3 per phone number per hour. A refactor accidentally reset the counter on every request instead of incrementing it. No test caught it — the mock never touched the rate limiter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error handling.&lt;/strong&gt; What happens when the SMS provider returns a 503? When the phone number format is invalid? When the country code doesn't match the number? All of these paths were invisible because the mock replaced the entire function.&lt;/p&gt;

&lt;p&gt;I counted seven distinct bugs in the previous six months that our tests should have caught but didn't, all because of the OTP mock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Middle Ground: Mock the Network, Not the Logic
&lt;/h2&gt;

&lt;p&gt;My first instinct was to rip out all mocks and hit a real SMS provider in every test. That's the other extreme, and it's not great either — you get slow, flaky, expensive tests that fail for reasons unrelated to your code.&lt;/p&gt;

&lt;p&gt;Instead, I landed on a layered approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Mock at the HTTP boundary (unit/integration tests)
&lt;/h3&gt;

&lt;p&gt;Instead of skipping verification logic, I mock the HTTP call to the SMS provider. The key difference: all of our code still runs. Field parsing, validation, rate limiting, expiry checks — everything executes normally. Only the final &lt;code&gt;httpx.post()&lt;/code&gt; to Twilio (or whoever) gets intercepted.&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;# conftest.py — the new way
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;respx&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mock_sms_provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mock the external SMS API, but run all our verification logic.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;respx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Mock the "send OTP" endpoint
&lt;/span&gt;        &lt;span class="n"&gt;respx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.twilio.com/2010-04-01/Accounts/*/Messages.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SM_test_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;known_otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock_sms_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fix the OTP value so tests can &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;enter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; it, but keep all logic.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp.auth.otp.generate_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;482901&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;482901&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test actually sends a request with the correct field names, our view parses &lt;code&gt;request.data&lt;/code&gt;, validates the format, checks the rate limiter, calls &lt;code&gt;generate_code()&lt;/code&gt; (which returns a fixed value), and stores the OTP in Redis with a TTL. The only thing that doesn't happen is a real HTTP call to Twilio.&lt;/p&gt;

&lt;p&gt;The verification side works symmetrically — the test submits the known OTP value, and our code runs the full check: look up the stored code, compare, check expiry, invalidate after use.&lt;/p&gt;

&lt;p&gt;This approach caught three of the seven bugs I mentioned within the first week of switching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Real SMS in a nightly E2E suite
&lt;/h3&gt;

&lt;p&gt;For the full end-to-end confidence, I run a smaller test suite once per night that actually sends and receives real SMS messages. This suite covers the critical paths: sign-up, password reset, phone change, 2FA enable/disable.&lt;/p&gt;

&lt;p&gt;I won't go deep into the provider setup here — that's a whole separate post I'm working on — but the key idea is:&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="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;real_phone_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get a real temporary number from an external provider.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_number&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_full_signup_with_real_otp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;real_phone_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;E2E: complete signup with actual SMS delivery.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;real_phone_number&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[name=phone]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button[type=submit]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Wait for real OTP from the provider
&lt;/span&gt;    &lt;span class="n"&gt;otp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sms_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[name=otp]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;otp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button#verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.welcome-message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to_be_visible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs against staging, uses real phone numbers, and receives real OTP codes. It's slow (~45 seconds per test) and costs a few cents per run. But it runs once a day, covers 8 critical paths, and has caught two carrier-related delivery issues that no amount of mocking would have revealed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Split in Practice
&lt;/h3&gt;

&lt;p&gt;Here's how it breaks down in our CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ci.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unit-and-integration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Runs on every push — fast, mocked at HTTP boundary&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/ -m "not e2e" --timeout=30&lt;/span&gt;

  &lt;span class="na"&gt;nightly-e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Runs once per day — slow, real SMS&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'schedule'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest tests/e2e/ -m "e2e" --timeout=120&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daily CI takes ~4 minutes. The nightly E2E takes ~12 minutes. Both are stable — the nightly suite has had 3 flaky failures in the last two months, all due to SMS delivery timeouts on the provider side, not our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Refactoring Trick That Made This Possible
&lt;/h2&gt;

&lt;p&gt;One thing that made the transition easier: I extracted all OTP logic into a single module. Before, verification code was scattered across three views, two serializers, and a middleware. Mocking was the only sane option because there was no single seam to intercept.&lt;/p&gt;

&lt;p&gt;After refactoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp/
  auth/
    otp.py          # generate, store, verify, expire — all OTP logic lives here
    providers/
      base.py       # abstract SMS provider interface
      twilio.py     # production provider
      mock.py       # test provider (for Layer 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;otp.py&lt;/code&gt; module exposes four functions: &lt;code&gt;generate_code()&lt;/code&gt;, &lt;code&gt;send_code(phone, code)&lt;/code&gt;, &lt;code&gt;verify_code(phone, code)&lt;/code&gt;, and &lt;code&gt;check_rate_limit(phone)&lt;/code&gt;. Every view calls these functions instead of implementing its own OTP handling. This means I have exactly one place to mock (&lt;code&gt;send_code&lt;/code&gt;'s HTTP call) and one place to fix when something breaks.&lt;/p&gt;

&lt;p&gt;If your OTP logic is spread across your codebase, start here. The mock cleanup becomes trivial once the logic is consolidated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Wish Someone Had Told Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mocking isn't the problem — mocking at the wrong layer is.&lt;/strong&gt; There's a massive difference between "skip this entire feature in tests" and "intercept the external HTTP call but run everything else." The first gives you false confidence. The second gives you real coverage with controlled dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;SKIP_*&lt;/code&gt; env var pattern is a code smell.&lt;/strong&gt; If your production code has &lt;code&gt;if TEST_MODE: return early&lt;/code&gt;, you're not testing your production code. You're testing a fork of it that only exists in CI. Treat any such pattern as tech debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't need real SMS for every test.&lt;/strong&gt; Layer 1 (mock at HTTP boundary) catches 90% of bugs. Layer 2 (real SMS) catches the remaining carrier/delivery edge cases. Running real SMS on every push is overkill and will make your team hate the test suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track what your mocks hide.&lt;/strong&gt; After every production bug, ask: "Would our test suite have caught this?" If the answer is "no, because the mock bypasses that code path," you have a mock problem. I keep a running doc of these incidents — it's been the best argument for investing in better test infrastructure.&lt;/p&gt;




&lt;p&gt;Next up, I'm going to write about the practical side of integrating real SMS providers into a CI/CD pipeline — which services work, which ones don't, and the pytest fixtures that tie it all together. If you've dealt with this problem, I'd love to hear how you approached it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All code examples are simplified. In reality, you'll want proper async handling, retry logic, and secret management. But the architecture holds.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>cicd</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>SMS Activate Alternatives: Top 10 Virtual Number Services in 2026</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Sat, 07 Feb 2026 15:36:06 +0000</pubDate>
      <link>https://dev.to/sms-activate/sms-activate-alternatives-top-10-virtual-number-services-in-2026-5fbp</link>
      <guid>https://dev.to/sms-activate/sms-activate-alternatives-top-10-virtual-number-services-in-2026-5fbp</guid>
      <description>&lt;p&gt;If you've ever built a registration flow, tested two-factor authentication, or simply needed a throwaway number for a sign-up, chances are you've heard of SMS-Activate. For nearly a decade, it was one of the go-to platforms for developers, QA engineers, and marketers who needed temporary phone numbers to receive verification codes.&lt;/p&gt;

&lt;p&gt;That era ended on &lt;strong&gt;December 29, 2025&lt;/strong&gt;, when SMS-Activate officially shut down. The announcement caught many users off guard — some still had active balances, open API integrations, and automation scripts tied to the platform. The hunt for reliable &lt;strong&gt;SMS activate alternatives&lt;/strong&gt; began immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Need Virtual Number Services
&lt;/h2&gt;

&lt;p&gt;Before diving into the list, let's clarify why these services matter beyond just "getting a code":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QA &amp;amp; Testing&lt;/strong&gt; — You can't test SMS-based auth flows with a single personal number. Virtual numbers let you simulate hundreds of sign-ups across different countries without buying a drawer full of SIM cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; — Signing up for third-party APIs, SaaS trials, or beta programs shouldn't require exposing your real phone number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo-targeting&lt;/strong&gt; — Need to test how your app handles a user from Indonesia vs. Germany? Virtual numbers from specific countries let you do exactly that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Pipelines&lt;/strong&gt; — Many services offer APIs that plug directly into automated testing workflows, making OTP verification part of your test suite rather than a manual bottleneck.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that context, here are the &lt;strong&gt;10 best SMS activate alternatives&lt;/strong&gt; worth your attention in 2026, ranked by overall value for developer workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 🏆 HeroSMS
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://hero-sms.com/" rel="noopener noreferrer"&gt;https://hero-sms.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hero-sms.com/" rel="noopener noreferrer"&gt;HeroSMS&lt;/a&gt; takes the top spot as the strongest all-around &lt;strong&gt;SMS activate alternative&lt;/strong&gt; available right now. The platform provides temporary phone numbers from &lt;strong&gt;180+ countries&lt;/strong&gt; for &lt;strong&gt;700+ services and applications&lt;/strong&gt;, making it one of the broadest offerings on the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Massive daily volume&lt;/strong&gt; — The platform adds over 500,000 new numbers daily, which means fresh, unused numbers are consistently available even for high-demand services like Telegram, WhatsApp, and Google.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing starts extremely low&lt;/strong&gt; — Numbers begin at fractions of a cent (e.g., $0.01 for TikTok/Douyin, $0.0125 for Tinder, $0.015 for WeChat). This makes it viable for bulk testing scenarios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crypto-only payments&lt;/strong&gt; — HeroSMS operates on cryptocurrency top-ups, which adds a layer of anonymity. For developers in regions with limited access to international payment methods, this is a practical advantage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API available&lt;/strong&gt; — The platform provides an API for developers building registration software or automated testing tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wholesale pricing&lt;/strong&gt; — A five-tier discount system based on top-up volume, making it cost-effective for teams and agencies running high-volume operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partner integrations&lt;/strong&gt; — HeroSMS numbers can be purchased through automation tools like Telegram Expert, GoogleReger, and InstaReg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Register → top up via crypto → select your service and country → purchase a number → receive the OTP code directly in your dashboard. The whole flow takes under a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers and teams who need high-volume, low-cost virtual numbers across a wide range of countries and services with API access.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. SMSPool
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://www.smspool.net/" rel="noopener noreferrer"&gt;https://smspool.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMSPool has built a solid reputation as a versatile &lt;strong&gt;SMS activate alternative&lt;/strong&gt;, especially among users who need non-VoIP phone numbers. Non-VoIP numbers are tied to physical SIM cards, which means they're accepted by services that reject virtual/VoIP numbers (looking at you, Google).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts both crypto and Visa/Mastercard (via Stripe)&lt;/li&gt;
&lt;li&gt;Comprehensive API for automation&lt;/li&gt;
&lt;li&gt;Offers SIM card rentals for up to 30 days&lt;/li&gt;
&lt;li&gt;Customizable pricing per service based on available number pools&lt;/li&gt;
&lt;li&gt;Free tier available (limited countries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; High demand can sometimes result in reused numbers. Support response times may take up to 24 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who specifically need non-VoIP numbers for services with strict verification policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 5SIM
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://5sim.net/" rel="noopener noreferrer"&gt;https://5sim.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;5SIM is one of the most recognizable names in the virtual number space, offering numbers from &lt;strong&gt;180+ countries&lt;/strong&gt; with pricing starting at &lt;strong&gt;$0.008 per number&lt;/strong&gt;. It's been around for years, and the platform is heavily automated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extremely low entry price&lt;/li&gt;
&lt;li&gt;Wide country and service coverage&lt;/li&gt;
&lt;li&gt;Instant number delivery via automation&lt;/li&gt;
&lt;li&gt;Active community and frequent number replenishment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Mixed reviews on reliability — some users report numbers that don't receive codes, and customer support experiences vary. Always test with a small balance first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Budget-conscious developers and those who need numbers from less common countries.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. SMS-Man
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://sms-man.com/" rel="noopener noreferrer"&gt;https://sms-man.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMS-Man positions itself as a direct successor to the gap left by SMS-Activate. The platform supports &lt;strong&gt;1,000+ services&lt;/strong&gt; and offers numbers from a wide selection of countries. Pricing starts at $0.05, and the interface is clean and straightforward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time use and rental number options (up to 3 months)&lt;/li&gt;
&lt;li&gt;Telegram bot for quick number requests&lt;/li&gt;
&lt;li&gt;Free numbers available for testing (public, shared)&lt;/li&gt;
&lt;li&gt;Multiple payment methods&lt;/li&gt;
&lt;li&gt;Dedicated blog with tutorials and use-case guides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Stock can run out for popular service-country combinations. The Telegram bot helps increase your chances of snagging a number during peak times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users migrating from SMS-Activate who want a familiar workflow and broad service compatibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. TextVerified
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://textverified.com/" rel="noopener noreferrer"&gt;https://textverified.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TextVerified focuses specifically on &lt;strong&gt;non-VoIP numbers&lt;/strong&gt;, primarily from the &lt;strong&gt;United States&lt;/strong&gt;. If your use case revolves around US-based verifications (Cash App, Google Voice, Venmo, etc.), this is a strong &lt;strong&gt;SMS activate alternative&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Non-VoIP numbers tied to physical SIM cards&lt;/li&gt;
&lt;li&gt;High acceptance rate on strict platforms&lt;/li&gt;
&lt;li&gt;API integration available&lt;/li&gt;
&lt;li&gt;Voice verification support (not just SMS)&lt;/li&gt;
&lt;li&gt;Bulk discounts for scaling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Limited to US numbers primarily. Pricing is higher than VoIP-based alternatives, but you're paying for acceptance rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who need US-specific non-VoIP numbers with high success rates for strict platforms.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. SMSPVA
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://smspva.com/" rel="noopener noreferrer"&gt;https://smspva.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMSPVA has been in the game for a long time, offering both &lt;strong&gt;real SIM-based and virtual numbers&lt;/strong&gt; from 60+ countries. The platform provides both temporary and rental modes with a REST API for integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dual offering: real SIM numbers + virtual numbers&lt;/li&gt;
&lt;li&gt;REST API with JSON responses&lt;/li&gt;
&lt;li&gt;SMS typically received in 1–3 seconds&lt;/li&gt;
&lt;li&gt;Real SIM numbers boast a 99% delivery success rate&lt;/li&gt;
&lt;li&gt;Rental numbers available for days, weeks, or months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; The interface can feel a bit dated. Prices for real SIM numbers are naturally higher than VoIP alternatives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who need the reliability of real SIM cards combined with the convenience of an online platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. OnlineSIM
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://onlinesim.io/" rel="noopener noreferrer"&gt;https://onlinesim.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OnlineSIM provides a clean, user-friendly interface with both short-term and long-term rental options. With numbers from &lt;strong&gt;90+ countries&lt;/strong&gt; and over &lt;strong&gt;one million unique numbers&lt;/strong&gt; in their pool, it's a reliable mid-range option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-term and long-term rental plans&lt;/li&gt;
&lt;li&gt;90+ countries available&lt;/li&gt;
&lt;li&gt;New numbers added daily&lt;/li&gt;
&lt;li&gt;Free trial numbers available before committing&lt;/li&gt;
&lt;li&gt;Simple, intuitive interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Smaller country coverage compared to HeroSMS or 5SIM. However, number quality tends to be more consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who value a clean UX and want the option to test numbers before buying.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Blacktel
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://www.blacktel.io/" rel="noopener noreferrer"&gt;https://blacktel.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blacktel is different from most services on this list — it functions more like a &lt;strong&gt;virtual phone&lt;/strong&gt;, not just an SMS receiver. You get a dedicated number that can make calls, send SMS, and receive messages. It also offers eSIMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full virtual phone functionality (calls + SMS + MMS)&lt;/li&gt;
&lt;li&gt;Dedicated numbers (not shared)&lt;/li&gt;
&lt;li&gt;Mobile apps for Android and iOS&lt;/li&gt;
&lt;li&gt;Crypto payments accepted&lt;/li&gt;
&lt;li&gt;eSIM support for data connectivity&lt;/li&gt;
&lt;li&gt;Free first number on registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Pricing structure is subscription-based, which is different from the pay-per-use model of most competitors. Numbers need to stay active, or you lose access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who need a persistent virtual number for ongoing use rather than one-time verifications.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. PVAPins
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://pvapins.com/" rel="noopener noreferrer"&gt;https://pvapins.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PVAPins markets itself heavily as an &lt;strong&gt;SMS activate alternative&lt;/strong&gt; with a focus on OTP reliability and API stability. The platform supports major services like WhatsApp, Gmail, Telegram, TikTok, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High OTP success rate&lt;/li&gt;
&lt;li&gt;Supports 200+ countries&lt;/li&gt;
&lt;li&gt;Multiple payment options (Crypto, Binance Pay, Payeer, GCash, Skrill, Payoneer)&lt;/li&gt;
&lt;li&gt;Clean API documentation&lt;/li&gt;
&lt;li&gt;Responsive support team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; The platform is relatively newer, so the track record isn't as established as some competitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who prioritize API stability and need flexible global payment options.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Veritel
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://www.veritel.io/" rel="noopener noreferrer"&gt;https://veritel.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Veritel takes a unique approach — all their numbers are backed by &lt;strong&gt;physical SIM cards&lt;/strong&gt; connected to proprietary hardware. This means you get the reliability of a real carrier number with the convenience of a virtual interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Physical SIM-backed numbers (not VoIP)&lt;/li&gt;
&lt;li&gt;180+ countries, 350+ services&lt;/li&gt;
&lt;li&gt;95% deliverability rate with automatic refunds on failure&lt;/li&gt;
&lt;li&gt;One-time use per service (number never reassigned for the same service)&lt;/li&gt;
&lt;li&gt;Pay-as-you-go, zero subscription&lt;/li&gt;
&lt;li&gt;Private messages — never shared after receipt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; Higher prices compared to pure VoIP services, but the trade-off is significantly higher acceptance rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Developers who need carrier-grade reliability without wanting to manage physical SIM cards.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Countries&lt;/th&gt;
&lt;th&gt;Services&lt;/th&gt;
&lt;th&gt;Starting Price&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Payment Methods&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HeroSMS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;180+&lt;/td&gt;
&lt;td&gt;700+&lt;/td&gt;
&lt;td&gt;~$0.01&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMSPool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100+&lt;/td&gt;
&lt;td&gt;500+&lt;/td&gt;
&lt;td&gt;~$0.01&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Crypto, Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5SIM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;180+&lt;/td&gt;
&lt;td&gt;500+&lt;/td&gt;
&lt;td&gt;$0.008&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMS-Man&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100+&lt;/td&gt;
&lt;td&gt;1,000+&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TextVerified&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;US-focused&lt;/td&gt;
&lt;td&gt;80+&lt;/td&gt;
&lt;td&gt;~$0.50&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMSPVA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60+&lt;/td&gt;
&lt;td&gt;300+&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OnlineSIM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;90+&lt;/td&gt;
&lt;td&gt;500+&lt;/td&gt;
&lt;td&gt;~$0.02&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blacktel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50+&lt;/td&gt;
&lt;td&gt;100+&lt;/td&gt;
&lt;td&gt;€0.40/mo&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Crypto, Cards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PVAPins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200+&lt;/td&gt;
&lt;td&gt;300+&lt;/td&gt;
&lt;td&gt;~$0.05&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Crypto, Payeer, Skrill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Veritel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;180+&lt;/td&gt;
&lt;td&gt;350+&lt;/td&gt;
&lt;td&gt;~$0.10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How to Choose the Right SMS Activate Alternative
&lt;/h2&gt;

&lt;p&gt;Picking the right service depends on your specific use case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-volume testing or bulk registrations?&lt;/strong&gt; Go with &lt;strong&gt;HeroSMS&lt;/strong&gt; — the combination of 500K+ daily numbers, rock-bottom pricing, and API access makes it the most scalable option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need non-VoIP numbers for strict platforms?&lt;/strong&gt; &lt;strong&gt;TextVerified&lt;/strong&gt; (US-focused) or &lt;strong&gt;Veritel&lt;/strong&gt; (global) are your best bets since they use physical SIM-backed numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a tight budget?&lt;/strong&gt; &lt;strong&gt;5SIM&lt;/strong&gt; offers the lowest entry point at $0.008 per number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need a persistent number?&lt;/strong&gt; &lt;strong&gt;Blacktel&lt;/strong&gt; gives you a full virtual phone experience with dedicated numbers, or look at &lt;strong&gt;SMSPVA&lt;/strong&gt; for long-term SIM rentals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API-first workflow?&lt;/strong&gt; Most services on this list offer APIs, but &lt;strong&gt;SMSPool&lt;/strong&gt;, &lt;strong&gt;HeroSMS&lt;/strong&gt;, and &lt;strong&gt;PVAPins&lt;/strong&gt; are frequently praised by developers for their API documentation and reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The shutdown of SMS-Activate created a real gap in the market, but 2026 has no shortage of capable SMS activate alternatives. The space has actually matured — services now compete on number quality, delivery speed, API stability, and geographic coverage rather than just price.&lt;/p&gt;

&lt;p&gt;If you're migrating from SMS-Activate, the transition to any of these platforms should be straightforward. Most follow a similar flow: register, top up, select service and country, get your number, receive the code. The main variables are pricing, number quality, and how well the API plays with your existing automation.&lt;/p&gt;

&lt;p&gt;Start with a small test balance on whichever service fits your use case, verify it works for your target platforms, and then scale up. The days of depending on a single provider are over — having two or three SMS activate alternatives in your toolkit is just good operational hygiene.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What SMS verification service are you using after the SMS-Activate shutdown? Drop your experience in the comments — especially if you've tested any of these with tricky platforms like Google or WhatsApp.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tools</category>
      <category>devops</category>
    </item>
    <item>
      <title>My Favorite Websites &amp; Tools for Everyday Development</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Thu, 05 Feb 2026 08:52:39 +0000</pubDate>
      <link>https://dev.to/sms-activate/my-favorite-websites-tools-for-everyday-development-474b</link>
      <guid>https://dev.to/sms-activate/my-favorite-websites-tools-for-everyday-development-474b</guid>
      <description>&lt;p&gt;Every developer has their secret toolkit — those bookmarked gems that make daily work faster and less frustrating. After years of collecting, testing, and abandoning hundreds of tools, here's my curated list of the ones that actually stuck.&lt;/p&gt;

&lt;p&gt;No fluff. Just tools I genuinely use.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Documentation &amp;amp; Learning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://devdocs.io" rel="noopener noreferrer"&gt;DevDocs.io&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Multiple API documentations in one fast, searchable, offline-capable interface. I have it open in a pinned tab at all times. Covers everything from JavaScript to Redis to Docker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Press &lt;code&gt;/&lt;/code&gt; to focus search, enable only the docs you need to reduce noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.mozilla.org" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The definitive source for HTML, CSS, and JavaScript. If Stack Overflow gives you the answer, MDN explains &lt;em&gt;why&lt;/em&gt; it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://caniuse.com" rel="noopener noreferrer"&gt;Can I Use&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Before using that shiny new CSS property or JS API, check browser support here. Saved me from countless "works on my machine" moments.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://web.dev" rel="noopener noreferrer"&gt;web.dev&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Google's resource for modern web development best practices. Especially useful for performance optimization and Core Web Vitals.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎮 Online Sandboxes &amp;amp; Playgrounds
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://codesandbox.io" rel="noopener noreferrer"&gt;CodeSandbox&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Full-fledged development environment in the browser. Perfect for prototyping React/Vue/Angular apps or sharing reproducible bug reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://stackblitz.com" rel="noopener noreferrer"&gt;StackBlitz&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Similar to CodeSandbox but runs Node.js natively in the browser. Boots instantly. I use it for quick Node/Express experiments.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://codepen.io" rel="noopener noreferrer"&gt;CodePen&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The classic. Best for HTML/CSS/JS snippets and front-end experiments. Great community for inspiration too.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://replit.com" rel="noopener noreferrer"&gt;Replit&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Supports 50+ languages. Need to quickly test a Python script, Go function, or Rust snippet? This is the place.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.typescriptlang.org/play" rel="noopener noreferrer"&gt;TypeScript Playground&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Official TS playground with great error explanations. Essential for understanding how TypeScript compiles to JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://regex101.com" rel="noopener noreferrer"&gt;Regex101&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Write, test, and debug regex with real-time explanations. The "explanation" panel has taught me more about regex than any tutorial.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 AI Assistants &amp;amp; Coding Helpers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;My go-to for complex coding questions, debugging, code reviews, and explaining unfamiliar codebases. Particularly strong at understanding context and nuance.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;AI pair programmer right in your IDE. Autocompletes functions, writes tests, and occasionally reads your mind. Worth every penny.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://phind.com" rel="noopener noreferrer"&gt;Phind&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;AI search engine optimized for developers. Gives direct answers with sources instead of making you click through 10 links.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://perplexity.ai" rel="noopener noreferrer"&gt;Perplexity&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Another AI-powered search. Great for researching technical topics when you need cited sources.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔌 API Development &amp;amp; Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://postman.com" rel="noopener noreferrer"&gt;Postman&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The industry standard for API testing. Collections, environments, automated tests — it does everything. The free tier is generous.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://insomnia.rest" rel="noopener noreferrer"&gt;Insomnia&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Lighter alternative to Postman. Cleaner UI, great for REST and GraphQL. I switch between both depending on the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://hoppscotch.io" rel="noopener noreferrer"&gt;Hoppscotch&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Open-source, runs entirely in the browser. No installation needed. Perfect for quick API tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://httpie.io" rel="noopener noreferrer"&gt;HTTPie&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Beautiful command-line HTTP client. Also has a web version. When &lt;code&gt;curl&lt;/code&gt; feels too cryptic, HTTPie is there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instead of curl with all its flags:&lt;/span&gt;
http POST api.example.com/users &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;John &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;john@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://webhook.site" rel="noopener noreferrer"&gt;Webhook.site&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Generates a unique URL that captures all incoming requests. Invaluable for debugging webhooks and OAuth callbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://jsonplaceholder.typicode.com" rel="noopener noreferrer"&gt;JSON Placeholder&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Free fake API for testing and prototyping. When you need dummy data and don't want to spin up a backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎨 Design &amp;amp; CSS Tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://nerdcave.com/tailwind-cheat-sheet" rel="noopener noreferrer"&gt;Tailwind CSS Cheat Sheet&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;All Tailwind classes on one searchable page. Faster than digging through docs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://coolors.co" rel="noopener noreferrer"&gt;Coolors&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Color palette generator. Press spacebar to generate new palettes. Export to various formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://realtimecolors.com" rel="noopener noreferrer"&gt;Realtime Colors&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Visualize your color palette on a real website template. Helps catch bad color combinations before coding.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://fonts.google.com" rel="noopener noreferrer"&gt;Google Fonts&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Free, fast, and huge library. Filter by properties to find the perfect typeface.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://fontsource.org" rel="noopener noreferrer"&gt;Fontsource&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Self-host Google Fonts via npm. Better performance, no external requests, GDPR-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://cssgrid-generator.netlify.app" rel="noopener noreferrer"&gt;CSS Grid Generator&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Visual grid builder. Draw your layout, copy the CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://flexboxfroggy.com" rel="noopener noreferrer"&gt;Flexbox Froggy&lt;/a&gt; &amp;amp; &lt;a href="https://cssgridgarden.com" rel="noopener noreferrer"&gt;Grid Garden&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Learn Flexbox and Grid through games. Fun way to solidify fundamentals.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://neumorphism.io" rel="noopener noreferrer"&gt;Neumorphism.io&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Generate soft UI shadows. Because getting neumorphic shadows right manually is tedious.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://bennettfeely.com/clippy/" rel="noopener noreferrer"&gt;Clippy&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;CSS &lt;code&gt;clip-path&lt;/code&gt; generator. Create complex shapes visually.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Generators &amp;amp; Utilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://readme.so" rel="noopener noreferrer"&gt;readme.so&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Drag-and-drop README generator. Produces clean, professional READMEs in minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://gitignore.io" rel="noopener noreferrer"&gt;gitignore.io&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Generate &lt;code&gt;.gitignore&lt;/code&gt; files for any tech stack. Just type your languages/frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://transform.tools" rel="noopener noreferrer"&gt;Transform Tools&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Convert anything to anything: JSON to TypeScript, SVG to React, HTML to JSX, and dozens more.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://mockaroo.com" rel="noopener noreferrer"&gt;Mockaroo&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Generate realistic test data in CSV, JSON, SQL, and more. Customizable fields and data types.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://picsum.photos" rel="noopener noreferrer"&gt;Lorem Picsum&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Random placeholder images. Just use &lt;code&gt;https://picsum.photos/400/300&lt;/code&gt; for a 400x300 image.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://favicon.io" rel="noopener noreferrer"&gt;Favicon.io&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Generate favicons from text, images, or emojis. Outputs all the sizes you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://squoosh.app" rel="noopener noreferrer"&gt;Squoosh&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Image compression by Google. Compare different formats and quality settings side by side.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://jakearchibald.github.io/svgomg/" rel="noopener noreferrer"&gt;SVGOMG&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Optimize SVG files. Dramatically reduces file size while keeping quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Performance &amp;amp; Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;PageSpeed Insights&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Google's official performance testing tool. Core Web Vitals, suggestions, and metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developers.google.com/web/tools/lighthouse" rel="noopener noreferrer"&gt;Lighthouse&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Built into Chrome DevTools, but also available as CLI. Audits performance, accessibility, SEO, and best practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://webpagetest.org" rel="noopener noreferrer"&gt;WebPageTest&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Advanced performance testing from multiple locations and devices. Film strip view shows exactly what users see during load.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://browserstack.com" rel="noopener noreferrer"&gt;BrowserStack&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Test on real devices and browsers. Essential when you don't own 50 different phones.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://responsively.app" rel="noopener noreferrer"&gt;Responsively&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Free app that shows your site on multiple screen sizes simultaneously. Huge time-saver for responsive development.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Cheat Sheets &amp;amp; References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://overapi.com" rel="noopener noreferrer"&gt;OverAPI&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Cheat sheets for almost every language. Clean, printable format.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://cheatography.com" rel="noopener noreferrer"&gt;Cheatography&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;User-contributed cheat sheets on everything from Vim to Docker to Excel.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://devhints.io" rel="noopener noreferrer"&gt;Devhints&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Beautiful, well-organized cheat sheets. My favorite for quick syntax lookups.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://quickref.me" rel="noopener noreferrer"&gt;QuickRef.me&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Another excellent cheat sheet collection with clean design.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://gitexplorer.com" rel="noopener noreferrer"&gt;Git Explorer&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Don't know the right Git command? Describe what you want to do, get the command.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://haveibeenpwned.com" rel="noopener noreferrer"&gt;Have I Been Pwned&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Check if your email was in a data breach. Also has an API for password checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://securityheaders.com" rel="noopener noreferrer"&gt;Security Headers&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Scan your site's HTTP security headers. Quick way to spot misconfigurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://ssllabs.com/ssltest" rel="noopener noreferrer"&gt;SSL Labs&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Deep analysis of your SSL configuration. Grades your setup and suggests improvements.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Bonus: Browser Extensions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wappalyzer&lt;/strong&gt; — Identify technologies used on any website&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Viewer&lt;/strong&gt; — Pretty-print JSON in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ColorZilla&lt;/strong&gt; — Pick colors from any webpage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatFont&lt;/strong&gt; — Identify fonts on any site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vimium&lt;/strong&gt; — Navigate the web with Vim keybindings&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Typical Day
&lt;/h2&gt;

&lt;p&gt;To give you an idea of how these fit together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Morning:&lt;/strong&gt; Check DevDocs for API reference while coding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging:&lt;/strong&gt; Test endpoints in Hoppscotch, ask Claude when stuck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Generate palette in Coolors, reference Tailwind cheat sheet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR Review:&lt;/strong&gt; Use Transform Tools to convert JSON schemas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before deploy:&lt;/strong&gt; Run Lighthouse audit, compress images with Squoosh&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Your Turn
&lt;/h2&gt;

&lt;p&gt;This list reflects my workflow — yours might be completely different. The best toolkit is the one you actually use.&lt;/p&gt;

&lt;p&gt;What are your daily essentials? I'm always looking to add new tools to the collection. Drop your favorites in the comments!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found a new tool here? Give this post a ❤️ and follow for more practical dev content!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>tools</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Git Commands I Wish I Knew Earlier</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Thu, 05 Feb 2026 08:16:47 +0000</pubDate>
      <link>https://dev.to/sms-activate/git-commands-i-wish-i-knew-earlier-32gl</link>
      <guid>https://dev.to/sms-activate/git-commands-i-wish-i-knew-earlier-32gl</guid>
      <description>&lt;p&gt;When I started using Git, I knew exactly five commands: &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;. And honestly? That was enough — until my first serious merge conflict, lost changes, and a Friday evening panic attack.&lt;/p&gt;

&lt;p&gt;Over the years, I've collected a set of commands that I genuinely wish someone had shown me on day one. Here they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗃️ git stash — Your Temporary Pocket
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The situation:&lt;/strong&gt; You're deep into coding a feature when your teammate asks you to urgently review their PR. Your changes aren't ready to commit, but you need a clean working directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The old me:&lt;/strong&gt; Would create a commit with the message "WIP temp don't push" (spoiler: I pushed it).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enlightened me:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Save all your changes to the stash&lt;/span&gt;
git stash

&lt;span class="c"&gt;# Do your other work...&lt;/span&gt;

&lt;span class="c"&gt;# Bring your changes back&lt;/span&gt;
git stash pop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stash Pro Tips
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stash with a descriptive message (you'll thank yourself later)&lt;/span&gt;
git stash save &lt;span class="s2"&gt;"halfway through login refactor"&lt;/span&gt;

&lt;span class="c"&gt;# View all your stashes&lt;/span&gt;
git stash list

&lt;span class="c"&gt;# Apply a specific stash without removing it from the list&lt;/span&gt;
git stash apply stash@&lt;span class="o"&gt;{&lt;/span&gt;2&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Stash only specific files&lt;/span&gt;
git stash push &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"navbar changes"&lt;/span&gt; src/components/Navbar.jsx

&lt;span class="c"&gt;# Stash including untracked files&lt;/span&gt;
git stash &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔍 git bisect — Find the Bug with Binary Search
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The situation:&lt;/strong&gt; Something broke. The tests were passing a week ago, now they're failing. Somewhere in 80 commits hides the culprit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The old me:&lt;/strong&gt; Would manually checkout commits one by one, losing half a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enlightened me:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start the bisect session&lt;/span&gt;
git bisect start

&lt;span class="c"&gt;# Mark the current (broken) commit as bad&lt;/span&gt;
git bisect bad

&lt;span class="c"&gt;# Mark a known good commit (e.g., from last week)&lt;/span&gt;
git bisect good a1b2c3d

&lt;span class="c"&gt;# Git will now checkout the middle commit&lt;/span&gt;
&lt;span class="c"&gt;# Test it, then tell Git:&lt;/span&gt;
git bisect good  &lt;span class="c"&gt;# if this commit works&lt;/span&gt;
&lt;span class="c"&gt;# or&lt;/span&gt;
git bisect bad   &lt;span class="c"&gt;# if this commit is broken&lt;/span&gt;

&lt;span class="c"&gt;# Repeat until Git finds the exact commit that introduced the bug&lt;/span&gt;
&lt;span class="c"&gt;# When done:&lt;/span&gt;
git bisect reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git uses binary search, so even with 1000 commits, you'll find the bug in about 10 steps. Magic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fully Automated Bisect
&lt;/h3&gt;

&lt;p&gt;If you have a test script that returns 0 for success and non-zero for failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git bisect start HEAD a1b2c3d
git bisect run npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git will find the broken commit completely automatically. Go grab a coffee.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Interactive Rebase — Rewrite History Like a Pro
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The situation:&lt;/strong&gt; Your branch has 15 commits including "fix typo", "fix typo again", "actually fix typo this time", and "forgot a semicolon". You want to merge, but your tech lead will not be pleased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The enlightened me:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rebase the last 5 commits interactively&lt;/span&gt;
git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; HEAD~5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens an editor with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pick a1b2c3d Add user authentication
pick b2c3d4e Fix typo in auth
pick c3d4e5f Fix typo again
pick d4e5f6g Add logout feature
pick e5f6g7h Forgot semicolon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pick a1b2c3d Add user authentication
squash b2c3d4e Fix typo in auth
squash c3d4e5f Fix typo again
pick d4e5f6g Add logout feature
fixup e5f6g7h Forgot semicolon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;squash (s):&lt;/strong&gt; Merge commit with previous, combine messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fixup (f):&lt;/strong&gt; Merge commit with previous, discard this message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reword (r):&lt;/strong&gt; Keep commit, but edit the message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;drop (d):&lt;/strong&gt; Delete the commit entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;edit (e):&lt;/strong&gt; Pause to amend the commit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; Never rebase commits that have been pushed to a shared branch. You'll create a parallel universe that your teammates won't appreciate.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 Useful Aliases That Save Keystrokes
&lt;/h2&gt;

&lt;p&gt;Add these to your &lt;code&gt;~/.gitconfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[alias]&lt;/span&gt;
    &lt;span class="c"&gt;# Shorter status
&lt;/span&gt;    &lt;span class="py"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;status -sb&lt;/span&gt;

    &lt;span class="c"&gt;# Pretty log graph
&lt;/span&gt;    &lt;span class="py"&gt;lg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;log --oneline --graph --decorate --all&lt;/span&gt;

    &lt;span class="c"&gt;# Undo the last commit but keep changes
&lt;/span&gt;    &lt;span class="py"&gt;undo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;reset --soft HEAD~1&lt;/span&gt;

    &lt;span class="c"&gt;# Amend without editing message
&lt;/span&gt;    &lt;span class="py"&gt;oops&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;commit --amend --no-edit&lt;/span&gt;

    &lt;span class="c"&gt;# Show what you did today
&lt;/span&gt;    &lt;span class="py"&gt;today&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;log --since='midnight' --author='Your Name' --oneline&lt;/span&gt;

    &lt;span class="c"&gt;# Delete all merged branches
&lt;/span&gt;    &lt;span class="py"&gt;cleanup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"!git branch --merged | grep -v '&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;|main&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;|master' | xargs -n 1 git branch -d"&lt;/span&gt;

    &lt;span class="c"&gt;# Quick commit with message
&lt;/span&gt;    &lt;span class="py"&gt;cm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;commit -m&lt;/span&gt;

    &lt;span class="c"&gt;# Create and switch to new branch
&lt;/span&gt;    &lt;span class="py"&gt;cob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;checkout -b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now instead of &lt;code&gt;git status --short --branch&lt;/code&gt; you just type &lt;code&gt;git s&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 More Commands Worth Knowing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  git commit --amend
&lt;/h3&gt;

&lt;p&gt;Forgot to add a file? Typo in the commit message?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add forgotten file to the last commit&lt;/span&gt;
git add forgotten-file.js
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;

&lt;span class="c"&gt;# Or just fix the message&lt;/span&gt;
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Better commit message"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  git reflog — Your Safety Net
&lt;/h3&gt;

&lt;p&gt;Accidentally deleted a branch? Reset too hard? &lt;code&gt;reflog&lt;/code&gt; remembers everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog

&lt;span class="c"&gt;# You'll see something like:&lt;/span&gt;
&lt;span class="c"&gt;# a1b2c3d HEAD@{0}: reset: moving to HEAD~3&lt;/span&gt;
&lt;span class="c"&gt;# b2c3d4e HEAD@{1}: commit: Add feature&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# Recover by:&lt;/span&gt;
git checkout b2c3d4e
&lt;span class="c"&gt;# or&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; b2c3d4e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  git cherry-pick
&lt;/h3&gt;

&lt;p&gt;Need one specific commit from another branch?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git cherry-pick a1b2c3d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  git log with Search
&lt;/h3&gt;

&lt;p&gt;Find who wrote that questionable line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Search commit messages&lt;/span&gt;
git log &lt;span class="nt"&gt;--grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bug fix"&lt;/span&gt;

&lt;span class="c"&gt;# Search code changes&lt;/span&gt;
git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"functionName"&lt;/span&gt;

&lt;span class="c"&gt;# Show commits that touched a specific file&lt;/span&gt;
git log &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; path/to/file.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  git clean
&lt;/h3&gt;

&lt;p&gt;Remove all untracked files (be careful!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dry run first — see what would be deleted&lt;/span&gt;
git clean &lt;span class="nt"&gt;-n&lt;/span&gt;

&lt;span class="c"&gt;# Actually delete untracked files&lt;/span&gt;
git clean &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Delete untracked files AND directories&lt;/span&gt;
git clean &lt;span class="nt"&gt;-fd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📋 Quick Reference Cheat Sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git stash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Save changes temporarily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git stash pop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restore stashed changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git bisect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary search for bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git rebase -i HEAD~n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interactive rebase last n commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git commit --amend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modify the last commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git reflog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View history of all HEAD changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git cherry-pick &amp;lt;hash&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copy a specific commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git log -S "text"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find commits changing specific text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git clean -fd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove untracked files and dirs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Git has a steep learning curve, but mastering these commands transformed my workflow. I went from fearing Git to actually enjoying version control (most of the time).&lt;/p&gt;

&lt;p&gt;My advice: pick one command from this list and start using it this week. Once it becomes muscle memory, add another.&lt;/p&gt;

&lt;p&gt;What Git commands do you wish you'd learned earlier? Drop them in the comments!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped you, consider following for more practical dev tips. Happy coding! 🚀&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 VS Code Extensions That Actually Speed Up My Workflow</title>
      <dc:creator>Mike Davis</dc:creator>
      <pubDate>Wed, 04 Feb 2026 18:39:42 +0000</pubDate>
      <link>https://dev.to/sms-activate/5-vs-code-extensions-that-actually-speed-up-my-workflow-43oc</link>
      <guid>https://dev.to/sms-activate/5-vs-code-extensions-that-actually-speed-up-my-workflow-43oc</guid>
      <description>&lt;p&gt;Let's be honest — there are thousands of VS Code extensions out there, and most "top extensions" lists include the same obvious picks.&lt;/p&gt;

&lt;p&gt;Today I want to share 5 extensions that genuinely changed how I code. These aren't just "nice to have" — they save me real time every single day.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Error Lens
&lt;/h2&gt;

&lt;p&gt;What it does: Displays errors and warnings inline, right next to your code.&lt;br&gt;
Instead of hovering over red squiggles or checking the Problems panel, you see the error message immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const user = { name: "Alex" };
console.log(user.age); // Error: Property 'age' does not exist on type...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error appears right there on the same line. No extra clicks.&lt;br&gt;
Why I love it: I catch typos and type errors instantly. It feels like having a second pair of eyes.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens" rel="noopener noreferrer"&gt;Error Lens on Marketplace&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Auto Rename Tag
&lt;/h2&gt;

&lt;p&gt;What it does: When you rename an HTML/JSX opening tag, it automatically renames the closing tag.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div&amp;gt;Content&amp;lt;/div&amp;gt;
&amp;lt;!-- You change "div" to "section"... --&amp;gt;
&amp;lt;!-- Now you have to manually find and change &amp;lt;/div&amp;gt; too --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (with extension):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;section&amp;gt;Content&amp;lt;/section&amp;gt;
&amp;lt;!-- Both tags update simultaneously --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why I love it: Sounds small, but in React/Vue components with nested elements, this saves dozens of manual edits per day.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag" rel="noopener noreferrer"&gt;Auto Rename Tag on Marketplace&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. GitLens
&lt;/h2&gt;

&lt;p&gt;What it does: Shows who changed each line of code, when, and why — right inside the editor.&lt;br&gt;
Hover over any line and see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The commit message&lt;/li&gt;
&lt;li&gt;The author&lt;/li&gt;
&lt;li&gt;How long ago it was changed
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// You see a weird piece of code and wonder "why is this here?"
// GitLens shows: "Fixed edge case for empty arrays" — John, 3 months ago
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why I love it: Perfect for understanding legacy code or figuring out why something was written a certain way. No need to open GitHub or run git blame manually.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" rel="noopener noreferrer"&gt;GitLens on Marketplace&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Prettier
&lt;/h2&gt;

&lt;p&gt;What it does: Automatically formats your code on save.&lt;br&gt;
Tabs vs spaces? Semicolons or not? Single or double quotes? Prettier handles it all with zero effort.&lt;/p&gt;

&lt;p&gt;Before save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const data={name:"test",value:42,active:true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const data = {
  name: "test",
  value: 42,
  active: true,
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why I love it: I stopped thinking about formatting entirely. Just write code, hit save, done.&lt;br&gt;
Pro tip: Add a &lt;code&gt;.prettierrc&lt;/code&gt; file to your project so the whole team uses the same style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔗 &lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" rel="noopener noreferrer"&gt;Prettier on Marketplace&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Todo Tree
&lt;/h2&gt;

&lt;p&gt;What it does: Finds all &lt;code&gt;TODO&lt;/code&gt;, &lt;code&gt;FIXME&lt;/code&gt;, &lt;code&gt;BUG&lt;/code&gt; comments in your codebase and displays them in a sidebar tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// TODO: Add error handling here
// FIXME: This breaks on mobile
// BUG: Returns null for empty input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All these comments are collected in one place. Click on any item — jump straight to that line.&lt;br&gt;
Why I love it: I actually remember to fix things now. Before this extension, my TODOs just disappeared into the void.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree" rel="noopener noreferrer"&gt;Todo Tree on Marketplace&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus Tip
&lt;/h2&gt;

&lt;p&gt;Don't install 50 extensions at once. Add them one by one, use each for a week, and keep only what actually helps.&lt;br&gt;
A cluttered VS Code is a slow VS Code.&lt;/p&gt;

&lt;p&gt;What extensions do you use daily? Drop them in the comments — I'm always looking for new tools to try!&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
