<?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: ZelNum</title>
    <description>The latest articles on DEV Community by ZelNum (@zelnum).</description>
    <link>https://dev.to/zelnum</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176627%2F61e63926-7ed7-4e93-a719-5e1befbcd4fe.png</url>
      <title>DEV Community: ZelNum</title>
      <link>https://dev.to/zelnum</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zelnum"/>
    <language>en</language>
    <item>
      <title>Carrier Lookup for SMS Routing: Save 40% on Costs (2026)</title>
      <dc:creator>ZelNum</dc:creator>
      <pubDate>Sat, 31 Oct 2026 12:01:31 +0000</pubDate>
      <link>https://dev.to/zelnum/carrier-lookup-for-sms-routing-save-40-on-costs-2026-557i</link>
      <guid>https://dev.to/zelnum/carrier-lookup-for-sms-routing-save-40-on-costs-2026-557i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zelnum.com/blog/carrier-lookup-sms-routing/" rel="noopener noreferrer"&gt;zelnum.com&lt;/a&gt;. Republished here with a canonical link to the original.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four steps to optimize SMS routing with carrier lookup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run carrier lookup on every number&lt;/strong&gt; before sending&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Segment your list by destination carrier&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match each segment to the cheapest gateway&lt;/strong&gt; for that carrier and country&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor delivery rates&lt;/strong&gt; to catch route degradation early&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done well, this saves 30–40% on outbound SMS spend with zero impact on delivery.&lt;/p&gt;

&lt;p&gt;SMS routing is where messaging budgets quietly leak. Aggregators and gateway operators charge based on destination carrier, country, and route quality — three variables that vary by orders of magnitude. A US-to-Indonesia message can cost 5–10× more on a premium route than on a wholesale route. Carrier lookup is what makes intelligent routing possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SMS Routing Actually Works
&lt;/h2&gt;

&lt;p&gt;Every outbound SMS goes through three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sender&lt;/strong&gt; (your application or marketing tool)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregator/Gateway&lt;/strong&gt; (Twilio, MessageBird, Plivo, Sinch, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destination Carrier&lt;/strong&gt; (T-Mobile, Vodafone, Airtel, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each aggregator offers multiple "routes" to each destination carrier. Routes vary by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; — premium vs wholesale, sometimes 10× difference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — direct routes are fast, multi-hop routes add seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery rate&lt;/strong&gt; — premium routes hit 98%+, wholesale may drop to 80–90%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt; — sender ID support, two-way messaging, delivery receipts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without carrier lookup, you can't choose the right route. Because you don't know what carrier you're routing to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Carrier Detection Matters for Routing
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Mobile Number Portability (MNP) breaks prefix routing.&lt;/strong&gt; In the US, 30%+ of mobile numbers have been ported. A number with a Verizon prefix may now be on T-Mobile's network. Routing based on prefix sends messages to the wrong carrier: they fail or bill incorrectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Different carriers have different gateway prices.&lt;/strong&gt; Sending to T-Mobile US through a premium aggregator costs roughly double what a wholesale aggregator charges for the same destination. Same number, same network — big price difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. International routing is more dramatic.&lt;/strong&gt; Regional specialist aggregators in markets like India often price a fraction of global premium gateways. A 100K-message campaign can save thousands of dollars with the right route.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math
&lt;/h2&gt;

&lt;p&gt;A 100K-message campaign:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without carrier-based routing (single-route pricing):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100K × $0.0075 average = $750&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With carrier-based routing (segmented):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;60K (US Tier-1 carriers via wholesale) × $0.003 = $180&lt;/li&gt;
&lt;li&gt;25K (US Tier-2 carriers via mid-tier) × $0.005 = $125&lt;/li&gt;
&lt;li&gt;15K (international via specialized routes) × $0.012 = $180&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total: $485&lt;/strong&gt; (35% savings = $265)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Annual impact for a team running 4 campaigns per month: &lt;strong&gt;$12,720 saved/year&lt;/strong&gt; on a 100K-message recurring campaign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Run Carrier Lookup on Entire List
&lt;/h2&gt;

&lt;p&gt;Before any campaign, run all numbers through a &lt;a href="https://zelnum.com/carrier-lookup/" rel="noopener noreferrer"&gt;bulk carrier lookup&lt;/a&gt;. For lists under 100K, CSV upload is fastest. For real-time scenarios (transactional SMS, signup verification), use the API.&lt;/p&gt;

&lt;p&gt;The lookup returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;carrier_name&lt;/code&gt; — destination carrier&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;country&lt;/code&gt; — ISO country code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;line_type&lt;/code&gt; — mobile / landline / VoIP / toll-free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule to keep: trust real-time HLR data, not prefix-based lookups. MNP makes prefix routing wrong 30%+ of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Segment by Destination Carrier
&lt;/h2&gt;

&lt;p&gt;After lookup, group numbers into segments. A practical segmentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Premium destinations&lt;/strong&gt; — small carriers, edge cases requiring premium routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-volume Tier 1&lt;/strong&gt; — major carriers (T-Mobile, AT&amp;amp;T, Verizon, Vodafone) with wholesale options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;International by region&lt;/strong&gt; — Asia, EMEA, LATAM each routed via regional specialists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landlines and toll-free&lt;/strong&gt; — moved to voice campaigns or removed entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VoIP and disposable&lt;/strong&gt; — flagged for review (high fraud risk, high failure rate)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The segmentation logic depends on your aggregator's pricing structure. Most teams find 3–5 segments capture 90% of the savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Match Each Segment to Cheapest Gateway
&lt;/h2&gt;

&lt;p&gt;Different aggregators specialize in different routes. Build a routing table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Best Gateway&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US Tier 1 (T-Mobile, AT&amp;amp;T, Verizon)&lt;/td&gt;
&lt;td&gt;Wholesale aggregator&lt;/td&gt;
&lt;td&gt;Cheapest per SMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;US Tier 2 + small carriers&lt;/td&gt;
&lt;td&gt;Premium aggregator (e.g., Twilio)&lt;/td&gt;
&lt;td&gt;Better delivery rates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EU (Vodafone, O2, Orange)&lt;/td&gt;
&lt;td&gt;EU-specialized aggregator&lt;/td&gt;
&lt;td&gt;Direct connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asia (China Mobile, Airtel, NTT)&lt;/td&gt;
&lt;td&gt;Asia-specialized aggregator&lt;/td&gt;
&lt;td&gt;Local carrier relationships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latin America&lt;/td&gt;
&lt;td&gt;LATAM specialist&lt;/td&gt;
&lt;td&gt;Regional pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most SMS platforms allow custom routing rules. Build the rules table once, then update quarterly as aggregator pricing shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Monitor Delivery Rates
&lt;/h2&gt;

&lt;p&gt;After routing optimization, track delivery rates per segment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Healthy&lt;/strong&gt; — 95%+ delivery rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch&lt;/strong&gt; — 85–94% (route may be degrading)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action needed&lt;/strong&gt; — &amp;lt;85% (switch routes immediately)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set up automated alerts. Aggregators occasionally degrade routes without notice. Without monitoring, you might lose 20% of deliveries before noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MNP not handled.&lt;/strong&gt; If your routing logic uses original carrier assignment, you're routing 30% of US mobile traffic to the wrong network. Always re-check carriers periodically (quarterly minimum) since MNP doesn't trigger any notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale carrier data.&lt;/strong&gt; Cached carrier data expires fast. For high-stakes routing, always do real-time HLR lookups, not cached data older than 30 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wholesale routes for transactional SMS.&lt;/strong&gt; Wholesale routes are great for marketing (cheap) but may have higher latency. For OTP/2FA — always use premium routes. A 10-second delay loses the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring landlines.&lt;/strong&gt; Landlines can't receive SMS at all. Sending to them is 100% wasted spend. Always filter &lt;code&gt;line_type = mobile&lt;/code&gt; before campaigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  For SMS Aggregators (Specialized Advice)
&lt;/h2&gt;

&lt;p&gt;If you're an SMS aggregator yourself (rather than an end customer), three more considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time HLR is mandatory.&lt;/strong&gt; Cached data is not acceptable for routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity to 200+ carriers globally.&lt;/strong&gt; Build direct relationships, not via reseller chains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MNP handling at aggregator level.&lt;/strong&gt; Provide MNP-corrected routing as a feature to your customers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For high-volume aggregators, see our &lt;a href="https://zelnum.com/carrier-lookup-api/" rel="noopener noreferrer"&gt;carrier lookup API&lt;/a&gt; — built for batch carrier enrichment and large CSV/API jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools Recommendation
&lt;/h2&gt;

&lt;p&gt;For SMS routing optimization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://zelnum.com/carrier-lookup/" rel="noopener noreferrer"&gt;ZelNum Carrier Lookup&lt;/a&gt;&lt;/strong&gt; — bulk + API, real-time HLR data, international coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twilio Lookup&lt;/strong&gt; — API only, more expensive (see &lt;a href="https://zelnum.com/twilio-lookup/" rel="noopener noreferrer"&gt;alternatives&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom HLR provider&lt;/strong&gt; — for very high-volume aggregators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math is consistent: bulk validation costs a fraction of a cent per lookup, and route savings compound from the first campaign.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zelnum.com/carrier-lookup-api/" rel="noopener noreferrer"&gt;Optimize your SMS routing now →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does carrier-based SMS routing work for international SMS?
&lt;/h3&gt;

&lt;p&gt;Yes, and the savings are larger internationally. US routing saves 30–40%; international routing can save 60–80% by using regional specialist aggregators instead of generic global gateways. Always check carrier first to determine the destination network and pick the right regional route.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I refresh carrier data for SMS routing?
&lt;/h3&gt;

&lt;p&gt;For high-volume routing, refresh on every send (real-time HLR lookup). For lower-volume marketing campaigns, refresh quarterly is sufficient. Mobile Number Portability rates of 30%+ in the US mean cached data older than 90 days is unreliable for cost-optimal routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about Mobile Number Portability (MNP) — does it really matter?
&lt;/h3&gt;

&lt;p&gt;Yes. In mature markets (US, EU, UK), 30%+ of mobile numbers have been ported. Routing based on the original prefix sends 30% of your traffic to the wrong network — leading to failed deliveries, billing disputes, or higher-cost routes. Live HLR lookup catches this automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is carrier-based routing worth it for low SMS volumes?
&lt;/h3&gt;

&lt;p&gt;For under 10K messages/month, the savings (~$30–$50/month) often don't justify the implementation effort. Above 50K messages/month, savings exceed $200/month and quickly cover the validation cost. Above 1M messages/month, carrier-based routing typically saves $5,000+ per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I bypass carrier lookup by using a single high-quality gateway?
&lt;/h3&gt;

&lt;p&gt;You can, but you'll pay 2–10× the optimal price. Premium gateways like Twilio offer simplicity by using premium routes for everything. Carrier-based routing trades that simplicity for major savings, especially at international scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's HLR lookup and why is it essential for routing?
&lt;/h3&gt;

&lt;p&gt;HLR (Home Location Register) is the carrier-level database that tracks which carrier currently owns each phone number. Real-time HLR lookup catches Mobile Number Portability — when users keep their numbers but switch carriers. Without HLR data, you can't route accurately to the current carrier.&lt;/p&gt;

</description>
      <category>sms</category>
      <category>telecom</category>
      <category>api</category>
      <category>marketing</category>
    </item>
    <item>
      <title>How to Clean a WhatsApp Contact List: 2026 Guide</title>
      <dc:creator>ZelNum</dc:creator>
      <pubDate>Tue, 27 Oct 2026 11:28:53 +0000</pubDate>
      <link>https://dev.to/zelnum/how-to-clean-a-whatsapp-contact-list-2026-guide-2dmg</link>
      <guid>https://dev.to/zelnum/how-to-clean-a-whatsapp-contact-list-2026-guide-2dmg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://zelnum.com/blog/clean-whatsapp-list/" rel="noopener noreferrer"&gt;zelnum.com&lt;/a&gt;. Republished here with a canonical link to the original.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A dirty WhatsApp contact list is the #1 cause of WhatsApp Business account bans. Sending campaigns to non-existent or inactive numbers triggers WhatsApp's spam systems within 24–72 hours. List cleaning isn't optional. It's the survival workflow for any B2B WhatsApp marketing operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Clean a WhatsApp Contact List in 5 Steps
&lt;/h2&gt;

&lt;p&gt;Five steps. The full workflow takes about 15 minutes for a 10K-number list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export your contact list&lt;/strong&gt; as CSV with country codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run bulk WhatsApp validation&lt;/strong&gt; to flag non-WhatsApp numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categorize results&lt;/strong&gt; into keep/remove/flag buckets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-import the clean list&lt;/strong&gt; to your CRM or marketing platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule periodic re-validation&lt;/strong&gt; every 30–60 days&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Real Cost of Not Cleaning
&lt;/h2&gt;

&lt;p&gt;A 10K-number WhatsApp marketing list usually contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;25–35% non-WhatsApp numbers&lt;/strong&gt; — number doesn't exist on WhatsApp&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5–10% inactive accounts&lt;/strong&gt; — registered but unused for months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3–8% recently created&lt;/strong&gt; — accounts under 30 days old (potential spam targets)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending campaigns to that raw list costs more than it saves. Accounts get banned within one or two campaigns, and every conversation the WhatsApp Business API charges goes to numbers that will never reply. A flagged account gets reviewed on WhatsApp's schedule (Meta's own FAQ says most reviews finish within 24 hours, if the account comes back at all), and some bans are permanent with no appeal path. Bans also force you onto a new business number, which breaks every established conversation and puts you back at the start of the warm-up cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Issues with WhatsApp Contact Lists
&lt;/h2&gt;

&lt;p&gt;Four issues recur in every WhatsApp list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-WhatsApp numbers&lt;/strong&gt; — collected via web forms without WhatsApp validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inactive WhatsApp accounts&lt;/strong&gt; — registered but no recent activity (often abandoned)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand-new accounts&lt;/strong&gt; — under 30 days old, often spam/scam accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format inconsistency&lt;/strong&gt; — numbers without country codes break validators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each needs different handling in the cleaning workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Export Your Contact List
&lt;/h2&gt;

&lt;p&gt;Pull your contact list from your CRM, WhatsApp Business platform, or marketing tool. Save as CSV with one phone number per row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One column with the phone number (other columns can stay)&lt;/li&gt;
&lt;li&gt;Country code prefix on every number (&lt;code&gt;+13125550731&lt;/code&gt;, not &lt;code&gt;(555) 123-4567&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Remove obvious junk (empty rows, all-zeros, test numbers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your numbers don't have country codes, normalize them first. Most countries have a default country code — for US/Canada add &lt;code&gt;+1&lt;/code&gt;, for UK add &lt;code&gt;+44&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Run Bulk WhatsApp Validation
&lt;/h2&gt;

&lt;p&gt;Upload the CSV to a bulk WhatsApp checking tool. Processing 10K numbers typically takes 5–10 minutes.&lt;/p&gt;

&lt;p&gt;Output adds these columns to each row:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is_whatsapp&lt;/code&gt; — yes/no&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;account_age_days&lt;/code&gt; — how long registered (if applicable)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_active_estimate&lt;/code&gt; — recent activity indicator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;risk_flag&lt;/code&gt; — spam/disposable indicators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Critical: &lt;strong&gt;don't send any messages during validation&lt;/strong&gt;. See &lt;a href="https://zelnum.com/blog/verify-whatsapp-without-message/" rel="noopener noreferrer"&gt;silent WhatsApp verification&lt;/a&gt; for why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Categorize Results
&lt;/h2&gt;

&lt;p&gt;Don't blindly delete every flagged record. Sort into four buckets:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket 1: Active, Established Accounts → Keep
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is_whatsapp = yes&lt;/code&gt; AND &lt;code&gt;account_age_days &amp;gt; 30&lt;/code&gt;. These are your usable contacts. Re-import them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket 2: Not on WhatsApp → Remove
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is_whatsapp = no&lt;/code&gt;. These numbers can't receive WhatsApp messages. Don't try to send. Move them to a separate list for SMS or email outreach instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket 3: Brand-New Accounts → Flag
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is_whatsapp = yes&lt;/code&gt; AND &lt;code&gt;account_age_days &amp;lt; 30&lt;/code&gt;. These may be real users or spam/burner accounts. For B2B sales, keep but flag for manual review. For high-volume campaigns, exclude until age increases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bucket 4: Possibly Inactive → Flag for Re-validation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;is_whatsapp = yes&lt;/code&gt; BUT &lt;code&gt;last_active_estimate&lt;/code&gt; shows no recent activity. Send carefully. Start with one warm-up message and watch delivery receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Re-import the Clean List
&lt;/h2&gt;

&lt;p&gt;Import the filtered "keep" list back into your WhatsApp Business platform or CRM. Best practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tag the imported records with the validation date&lt;/li&gt;
&lt;li&gt;Keep the removed list separate (don't delete entirely — you may want it for SMS/email re-engagement)&lt;/li&gt;
&lt;li&gt;Update CRM custom fields with WhatsApp status if your tools support it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5 — Schedule Periodic Re-validation
&lt;/h2&gt;

&lt;p&gt;Cleaning once isn't enough. WhatsApp accounts deactivate every day, and &lt;a href="https://faq.whatsapp.com/3347469605523961" rel="noopener noreferrer"&gt;recycled numbers get reassigned to new owners&lt;/a&gt;, so a list that was valid in March quietly rots by September. Re-validate on a schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active marketing campaigns&lt;/strong&gt; — re-validate every 30–45 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead nurture lists&lt;/strong&gt; — re-validate every 60–90 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-value VIP lists&lt;/strong&gt; — re-validate before any major campaign send&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set a calendar reminder. Skipping re-validation for 6 months typically lets 15–20% bad data accumulate again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Step Cleaning Workflow at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1 – Export&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Export contacts as CSV with country codes&lt;/td&gt;
&lt;td&gt;Clean, formatted CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 – Run validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bulk WhatsApp validation (10K in 5-10 min)&lt;/td&gt;
&lt;td&gt;is_whatsapp, account age, risk flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3 – Categorize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Split into keep / remove / flag buckets&lt;/td&gt;
&lt;td&gt;Segmented list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4 – Re-import&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push clean list back to CRM or platform&lt;/td&gt;
&lt;td&gt;Campaign-ready list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5 – Schedule&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Re-validate every 30-60 days&lt;/td&gt;
&lt;td&gt;Continuously clean data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tips for WhatsApp Business Account Owners
&lt;/h2&gt;

&lt;p&gt;If you operate a WhatsApp Business account (rather than an end-user), three more considerations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm up new business numbers gradually.&lt;/strong&gt; Don't blast a 10K-number campaign on day one. Start with 100/day and grow over 2–3 weeks. WhatsApp's anti-spam systems are stricter on new accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch your "Quality Rating".&lt;/strong&gt; WhatsApp Business shows a quality rating (Green/Yellow/Red). Yellow or Red = your sending reputation is degrading. Almost always caused by sending to bad lists. Pause and re-clean before continuing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid send-and-pray verification.&lt;/strong&gt; Some teams send "Hi" to test if a number is on WhatsApp. This is the fastest way to get banned. Use the silent verification approach from Step 2 instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Periodic Re-validation Strategy
&lt;/h2&gt;

&lt;p&gt;For mature B2B WhatsApp programs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly batch revalidation&lt;/strong&gt; of records older than 30 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time API validation&lt;/strong&gt; at lead capture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-campaign validation&lt;/strong&gt; 24 hours before major sends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly full database audit&lt;/strong&gt; to catch slow drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination keeps databases under 5% bad rate at all times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Tools
&lt;/h2&gt;

&lt;p&gt;For most B2B teams cleaning WhatsApp lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp Business API contacts/check&lt;/strong&gt; — official but more expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free single-number tool&lt;/strong&gt; — &lt;a href="https://zelnum.com/check-whatsapp-online/" rel="noopener noreferrer"&gt;zelnum.com/check-whatsapp-online&lt;/a&gt; for quick spot-checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For deeper guidance on the verification step, see &lt;a href="https://zelnum.com/blog/how-to-check-whatsapp-number/" rel="noopener noreferrer"&gt;how to check if a phone number is on WhatsApp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zelnum.com/whatsapp-number-checker/" rel="noopener noreferrer"&gt;Clean your WhatsApp list now →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How often should I clean my WhatsApp contact list?
&lt;/h3&gt;

&lt;p&gt;For active marketing campaigns, every 30–45 days. Lead nurture lists can stretch to 60–90. High-value lists get a fresh check before every major send. Skip re-validation for six months and 15–20% of the list is bad again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if a number was once on WhatsApp but is now invalid?
&lt;/h3&gt;

&lt;p&gt;This is common: WhatsApp accounts deactivate routinely. The cleaning workflow catches these on each pass. Re-validate every 30–60 days for active campaigns. Once flagged invalid, treat it as removed until a future re-validation says otherwise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can WhatsApp tell I'm cleaning my list?
&lt;/h3&gt;

&lt;p&gt;No. Silent verification methods (bulk CSV upload, third-party APIs) never message the numbers being checked, so there is nothing for WhatsApp's spam systems to react to. Cleaning your list is the opposite of suspicious behavior. It's what a careful sender looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does cleaning a 10K-number WhatsApp list take?
&lt;/h3&gt;

&lt;p&gt;The actual validation takes 5–10 minutes. The full workflow (export → format → upload → categorize → re-import) typically takes 15–30 minutes once you've done it once. After the first run, ongoing maintenance is mostly automated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I delete numbers that aren't on WhatsApp from my CRM?
&lt;/h3&gt;

&lt;p&gt;Don't delete — segment. Numbers that aren't on WhatsApp may still be reachable via SMS or email. Keep them in your CRM but tag them as "non-WhatsApp" so your WhatsApp campaigns automatically exclude them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about new WhatsApp accounts (under 30 days old)?
&lt;/h3&gt;

&lt;p&gt;Treat them carefully. New accounts are sometimes legitimate (real new users) but often signal spam, burner accounts, or test numbers. For B2B sales, keep them but flag for manual review before sending. For high-volume marketing, exclude them until account age exceeds 30 days.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>marketing</category>
      <category>business</category>
      <category>crm</category>
    </item>
    <item>
      <title>Bulk WhatsApp Checks: Async Jobs, Four States, and the Cost of a Wrong Answer</title>
      <dc:creator>ZelNum</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:17:37 +0000</pubDate>
      <link>https://dev.to/zelnum/bulk-whatsapp-checks-async-jobs-four-states-and-the-cost-of-a-wrong-answer-2f0o</link>
      <guid>https://dev.to/zelnum/bulk-whatsapp-checks-async-jobs-four-states-and-the-cost-of-a-wrong-answer-2f0o</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a bulk WhatsApp check is not a loop around a single-number endpoint. Three properties of the job model decide whether your pipeline is cheap or expensive: the check is &lt;strong&gt;asynchronous&lt;/strong&gt;, the answer is &lt;strong&gt;four states rather than a boolean&lt;/strong&gt;, and the cost is &lt;strong&gt;deducted when you submit, not when the job finishes&lt;/strong&gt;. Get any of them wrong and the invoice is what tells you, months later.&lt;/p&gt;

&lt;p&gt;This is about the machinery around the check, not the check itself. The per-lookup price is the least interesting number in the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The submit call returns a job, not an answer
&lt;/h2&gt;

&lt;p&gt;A batch request is accepted almost immediately and returns no verdicts. The documented shape takes a product slug, a country hint and an array of numbers, and returns a job handle plus a cost line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.zelnum.com/api/v1/detect
X-API-Key: YOUR_API_KEY

{
  "product": "wsValid",
  "country": "US",
  "phones": ["+14155550198", "+442071838750"]
}

// 500
{
  "mode": "async",
  "task_id": "xKj9aP3qLm7v",
  "message": "Task queued.",
  "summary": { "total": 5000, "cost_deducted": 0.500000 }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;summary&lt;/code&gt;. It is populated on a response whose own &lt;code&gt;message&lt;/code&gt; reads "Task queued", which means the charge exists before a single number has been checked. Five thousand numbers at &lt;code&gt;0.5&lt;/code&gt; is the documented $1 per ten thousand rate, applied at submit.&lt;/p&gt;

&lt;p&gt;You are buying the attempt, not the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A retried submit is a second charge
&lt;/h2&gt;

&lt;p&gt;Async jobs plus submit-time billing have a consequence that tends to surface in production rather than in review. A retried submit is a second job and a second charge. The documented WhatsApp request carries no idempotency key, which may not be the whole contract but is enough to plan around: assume that anything you send twice, you pay for twice.&lt;/p&gt;

&lt;p&gt;The guard has to live on your side. Hash the batch before sending, store that hash next to the &lt;code&gt;task_id&lt;/code&gt; the server hands back, and treat a repeated hash as already submitted. Without it you cannot tell a timeout where the server accepted the request from a timeout where it never arrived, and those two cases need opposite responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four states, not a boolean
&lt;/h2&gt;

&lt;p&gt;The question the check answers is binary in English: is this number on WhatsApp. The product's own export does not treat it as binary. Clean results are separated into four segments: registered, inactive, unknown and review.&lt;/p&gt;

&lt;p&gt;Two segments would be enough if the underlying answer really had two outcomes. Four is a statement that the query can also come back undecided, in more than one way.&lt;/p&gt;

&lt;p&gt;We kept those four segments as four columns rather than collapsing them into a single flag, because the flag throws away the one distinction that changes what your code should do next.&lt;/p&gt;

&lt;p&gt;The documentation names the segments without spelling out the rule that assigns a number to one, so the useful thing to carry into your code is the treatment, not the label:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Safe to treat as&lt;/th&gt;
&lt;th&gt;Why this is not the same as "no"&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;registered&lt;/td&gt;
&lt;td&gt;present, usable&lt;/td&gt;
&lt;td&gt;The only segment that asserts anything positive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inactive&lt;/td&gt;
&lt;td&gt;Not usable now, re-checkable later&lt;/td&gt;
&lt;td&gt;An account that is quiet is not an account that is gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;Undecided, queue for another pass&lt;/td&gt;
&lt;td&gt;A lookup with no definite answer has not answered "no"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;review&lt;/td&gt;
&lt;td&gt;Undecided, hand to a human&lt;/td&gt;
&lt;td&gt;A held-back answer is not a rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bug this prevents is a one-liner, and it is common because it looks correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The documentation does not publish per-number response field names,&lt;/span&gt;
&lt;span class="c1"&gt;// so "state" stands in for whichever field carries the verdict.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onWhatsApp&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offWhatsApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// swallows unknown and review&lt;/span&gt;

&lt;span class="c1"&gt;// what the four-way export is asking you to do instead&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usable&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;held&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dropped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inactive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second filter in the first block is the whole problem. &lt;code&gt;unknown&lt;/code&gt; and &lt;code&gt;review&lt;/code&gt; are not negatives, but they are not &lt;code&gt;registered&lt;/code&gt; either, so a &lt;code&gt;!==&lt;/code&gt; comparison files them with the dead numbers. The list looks cleaner and the losses are invisible: you dropped reachable contacts and the only symptom is a smaller send.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rate limit is the real constraint
&lt;/h2&gt;

&lt;p&gt;The documented default is 10,000 numbers per minute, and the documented batch ceiling is the same 10,000 per request. Two equal numbers read like "one request per minute". The actual constraint is throughput you have to pace.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Monthly volume&lt;/th&gt;
&lt;th&gt;At the documented rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;10K checks&lt;/td&gt;
&lt;td&gt;Under a minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth&lt;/td&gt;
&lt;td&gt;1M checks&lt;/td&gt;
&lt;td&gt;About 100 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;10M+ checks&lt;/td&gt;
&lt;td&gt;About 17 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A million numbers is one hundred minutes of paced submitting. Ten million is most of a day. The arithmetic is dull and it changes the design: an integration that assumes "submit and wait" works for a small list and stops working exactly when the list gets valuable.&lt;/p&gt;

&lt;p&gt;Pace with a token bucket sized to the documented rate rather than to your connection pool. Opening twenty parallel batch jobs does not check numbers faster, it moves the failure from "slow" to "rate limited", and now you have two problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quiet check is still a shared service
&lt;/h2&gt;

&lt;p&gt;The check sends nothing visible to the number being looked up, and that is a real guarantee worth having. It is not a statement about how hard you may hit the lookup. An endpoint that is quiet and cheap per call is still a shared service.&lt;/p&gt;

&lt;p&gt;Back off with jitter rather than a fixed interval. A thousand clients retrying on the same one-second boundary is a traffic spike you built yourself; the same clients with randomised delays are just traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the receipt alongside the verdict
&lt;/h2&gt;

&lt;p&gt;A verdict has a shelf life. A number that reads &lt;code&gt;unknown&lt;/code&gt; in March can read &lt;code&gt;registered&lt;/code&gt; in June, and a number that was registered can be reassigned by December. A result without a timestamp is an assertion with no expiry.&lt;/p&gt;

&lt;p&gt;Four columns make a result auditable: the number in E.164, the state you received, the &lt;code&gt;task_id&lt;/code&gt; that produced it, and the UTC time of the lookup. The &lt;code&gt;task_id&lt;/code&gt; is the only thing tying a row back to a line on your bill, and without it, reconciling an unexpected invoice means reconstructing what you sent.&lt;/p&gt;

&lt;p&gt;That is the same discipline the lower validation layers need. A number that fails a prefix check fails against a dataset with a version, not against the number itself; an &lt;a href="https://zelnum.com/blog/phone-number-validation-possible-vs-valid/" rel="noopener noreferrer"&gt;invalid result is a claim about your data&lt;/a&gt;, and the fix is to record which data.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal batch shape
&lt;/h2&gt;

&lt;p&gt;Put together, the smallest pipeline that survives contact with real lists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Normalise to E.164 and drop anything that will not parse before you send it. Paying to check a string that was never a number is the first cost you can avoid.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fingerprint the batch, store the fingerprint, submit, and keep the returned &lt;code&gt;task_id&lt;/code&gt; next to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Submit behind a token bucket sized to the documented rate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait on the webhook, or poll the task result, then write all four states to their own column.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-queue &lt;code&gt;unknown&lt;/code&gt; on a schedule, route &lt;code&gt;review&lt;/code&gt; to a human, and keep &lt;code&gt;inactive&lt;/code&gt; out of the send.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reconcile charges against stored &lt;code&gt;task_id&lt;/code&gt;s rather than against your send log.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a one-off check, the &lt;a href="https://zelnum.com/whatsapp-number-checker/" rel="noopener noreferrer"&gt;WhatsApp number checker&lt;/a&gt; runs the lookup server-side and returns the state alongside the line type. For lists, the &lt;a href="https://zelnum.com/bulk-whatsapp-checker/" rel="noopener noreferrer"&gt;bulk WhatsApp checker&lt;/a&gt; takes CSV and TXT uploads, and the &lt;a href="https://zelnum.com/whatsapp-api/" rel="noopener noreferrer"&gt;WhatsApp API reference&lt;/a&gt; documents the detect, file and task-result endpoints, the rate, and the account-age product if campaign filtering needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is there a single-number endpoint, or is everything a batch?
&lt;/h3&gt;

&lt;p&gt;A single number and a batch of ten thousand go through the same detect endpoint; what changes is the payload size and how you collect the result. For interactive, one-at-a-time checking, the hosted tool does the server-side lookup for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does "unknown" actually mean?
&lt;/h3&gt;

&lt;p&gt;The documentation does not define the rule that produces it, and that is the honest answer. What is safe to say is what it is not: a lookup returning &lt;code&gt;unknown&lt;/code&gt; produced no verdict, so it is not a negative. Treat it as work still owed and re-queue it instead of filtering it out.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does a one-million-number job take?
&lt;/h3&gt;

&lt;p&gt;At the documented default of 10,000 numbers per minute, about one hundred minutes, or roughly 1.7 hours. Plan the pacing and the retry budget for that window rather than assuming a job returns in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a WhatsApp Business account to run these checks?
&lt;/h3&gt;

&lt;p&gt;No. This is a verification and list-hygiene API, separate from the messaging API. WhatsApp Business API is for sending approved messages; a check answers whether a number is registered and sends nothing to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I avoid paying twice for the same list?
&lt;/h3&gt;

&lt;p&gt;Fingerprint the batch before sending and store the fingerprint with the returned &lt;code&gt;task_id&lt;/code&gt;. If a submit times out you cannot tell whether the server accepted it, so the stored fingerprint is what stops a retry from becoming a second job and a second charge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the export split results into four lists instead of two?
&lt;/h3&gt;

&lt;p&gt;Because a two-state answer would hide the cases the pipeline handles differently. Reachable contacts, quiet accounts, unresolved lookups and held-back results need opposite actions, and collapsing them into yes and no is how good records get deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/whatsapp-api/" rel="noopener noreferrer"&gt;WhatsApp checker API reference&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/whatsapp-number-checker/" rel="noopener noreferrer"&gt;WhatsApp number checker&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/bulk-whatsapp-checker/" rel="noopener noreferrer"&gt;Bulk WhatsApp checker&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/blog/how-to-filter-whatsapp-numbers-from-a-list/" rel="noopener noreferrer"&gt;How to filter WhatsApp numbers from a list&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/blog/phone-number-validation-possible-vs-valid/" rel="noopener noreferrer"&gt;Phone number validation: possible, valid and reachable&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>integration</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>Phone Number Validation: possible vs valid vs reachable</title>
      <dc:creator>ZelNum</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:17:33 +0000</pubDate>
      <link>https://dev.to/zelnum/phone-number-validation-possible-vs-valid-vs-reachable-2pk1</link>
      <guid>https://dev.to/zelnum/phone-number-validation-possible-vs-valid-vs-reachable-2pk1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; when you ask "is this phone number valid?" you are actually asking three separate questions. &lt;strong&gt;Possible&lt;/strong&gt; means the digits fit the length range of the numbering plan. &lt;strong&gt;Valid&lt;/strong&gt; means the prefix matches a range a regulator has assigned. &lt;strong&gt;Reachable&lt;/strong&gt; means there is a live line on that number right now, and only a network lookup can answer it. A regex answers none of the three, and a &lt;code&gt;true&lt;/code&gt; from a validation library only means the library's dataset agrees with the prefix.&lt;/p&gt;

&lt;p&gt;Confusing these layers produces two failure modes that look nothing alike. Numbers that pass your check and then bounce forever, and real numbers that your own form rejects. Both cost money, and both become fixable once you know which layer is failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What each layer actually proves
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;The question it answers&lt;/th&gt;
&lt;th&gt;Typical tool&lt;/th&gt;
&lt;th&gt;What a &lt;code&gt;false&lt;/code&gt; usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Possible&lt;/td&gt;
&lt;td&gt;Do the digits fit the plan's length range?&lt;/td&gt;
&lt;td&gt;Length check, &lt;code&gt;isPossibleNumber()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Almost always a parse problem, not a bad number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Valid&lt;/td&gt;
&lt;td&gt;Is the prefix inside an assigned range?&lt;/td&gt;
&lt;td&gt;Prefix metadata, &lt;code&gt;isValidNumber()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Usually a dataset that predates the range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Reachable&lt;/td&gt;
&lt;td&gt;Is a line allocated and in service?&lt;/td&gt;
&lt;td&gt;HLR or carrier lookup&lt;/td&gt;
&lt;td&gt;The number really is not in service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The order matters because each layer costs more than the one below it. Layer 1 is arithmetic. Layer 2 is a lookup in a file shipped inside your library. Layer 3 is a network round trip against carrier data, which is the only layer worth paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: "possible" is a length check and nothing else
&lt;/h2&gt;

&lt;p&gt;Most engineers carry an assumption that a phone number is 10 digits, or 11 once you add the country code. That assumption holds for the United States and almost nowhere else. Here are the length ranges recorded in the metadata of &lt;code&gt;phonenumbers&lt;/code&gt; 9.0.41, read directly from the library:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Country code&lt;/th&gt;
&lt;th&gt;Mobile lengths&lt;/th&gt;
&lt;th&gt;All lengths in the general descriptor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;+1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;+1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7, 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United Kingdom&lt;/td&gt;
&lt;td&gt;+44&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7, 9, 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;+49&lt;/td&gt;
&lt;td&gt;10, 11&lt;/td&gt;
&lt;td&gt;4 through 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;+33&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;+39&lt;/td&gt;
&lt;td&gt;9, 10&lt;/td&gt;
&lt;td&gt;6 through 12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spain&lt;/td&gt;
&lt;td&gt;+34&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;td&gt;+31&lt;/td&gt;
&lt;td&gt;9, 11&lt;/td&gt;
&lt;td&gt;5 through 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazil&lt;/td&gt;
&lt;td&gt;+55&lt;/td&gt;
&lt;td&gt;10, 11&lt;/td&gt;
&lt;td&gt;8 through 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mexico&lt;/td&gt;
&lt;td&gt;+52&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;India&lt;/td&gt;
&lt;td&gt;+91&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8 through 13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indonesia&lt;/td&gt;
&lt;td&gt;+62&lt;/td&gt;
&lt;td&gt;9 through 12&lt;/td&gt;
&lt;td&gt;7 through 17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nigeria&lt;/td&gt;
&lt;td&gt;+234&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10 through 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japan&lt;/td&gt;
&lt;td&gt;+81&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8 through 17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Singapore&lt;/td&gt;
&lt;td&gt;+65&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8, 10, 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Australia&lt;/td&gt;
&lt;td&gt;+61&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;5 through 12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mobile numbers cluster at 9 or 10 digits, but Indonesia spans 9 to 12 and the Netherlands ships both 9 and 11. The general descriptor is wider still, because it also covers service numbers, short codes and corporate ranges. Any pattern that demands a fixed digit count will reject valid numbers the moment your list leaves North America.&lt;/p&gt;

&lt;p&gt;This is what &lt;code&gt;isPossibleNumber()&lt;/code&gt; does. It counts digits and compares them against these ranges. Nothing about the prefix is consulted, which is why it stays fast and why it can only say "maybe".&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: "valid" is a prefix lookup against a dataset with a release date
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;isValidNumber()&lt;/code&gt; goes further and checks the prefix against ranges that the library ships as metadata. That metadata is a snapshot taken when the package was built. When a regulator assigns a new range, every installed copy keeps rejecting it until somebody upgrades.&lt;/p&gt;

&lt;p&gt;In January 2026 a thread on the libphonenumber discussion group reported three North American area codes, 357, 837 and 679, that &lt;code&gt;isValidNumber()&lt;/code&gt; flagged as invalid even though they had been allocated. A Google engineer replied in the same thread that the codes were assigned to their regions but not yet assigned to operators, and that anyone running metadata older than the update would see them fail.&lt;/p&gt;

&lt;p&gt;We reproduced it on both sides before writing this, against &lt;code&gt;phonenumbers&lt;/code&gt; 9.0.41 in Python and &lt;code&gt;libphonenumber-js&lt;/code&gt; 1.13.15 in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// libphonenumber-js 1.13.15&lt;/span&gt;
&lt;span class="nf"&gt;parsePhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+14155550123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// true  - 415 is an old assignment&lt;/span&gt;
&lt;span class="nf"&gt;parsePhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+13575550100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isPossible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nf"&gt;parsePhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+13575550100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// false - 2025 assignment, missing from metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isPossible()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;isValid()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+14155550123&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Assigned long ago, present in the metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+13575550100&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;2025 allocation, absent from the metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+18375550100&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Same cause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+16795550100&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Same cause&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+10000000000&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Correct NANP shape, area code 000 does not exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+11234567890&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Correct NANP shape, area code 123 is unassigned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+1234567&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Below the minimum length&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things follow from that table. A number can be possible and invalid at the same time, which is the whole difference between the two functions. And an invalid verdict is a statement about your dataset version, not about the number.&lt;/p&gt;

&lt;p&gt;The most common attempted fix is switching &lt;code&gt;libphonenumber-js&lt;/code&gt; from its default metadata to the full &lt;code&gt;libphonenumber-js/max&lt;/code&gt; build. It does not help. We ran both. The &lt;code&gt;max&lt;/code&gt; metadata adds precise patterns for ranges that already exist in the dataset, so it catches more malformed numbers inside known ranges. It cannot add a range the dataset never had. Upgrading the package is the only thing that clears a false negative caused by a new assignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: "reachable" is the only layer that talks to the network
&lt;/h2&gt;

&lt;p&gt;Layers 1 and 2 run offline against static data. Layer 3 asks the network itself whether the number is allocated, whether the line is in service, and what kind of line it is. That is the job of an HLR lookup or a carrier lookup, and it is the difference between "this number could exist" and "this number exists on a switch right now".&lt;/p&gt;

&lt;p&gt;Line type comes from this layer: mobile, fixed, VoIP, toll-free, pager. It is the field that decides whether an SMS can arrive at all, because a fixed line cannot receive one no matter how valid its prefix is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leading-zero trap
&lt;/h2&gt;

&lt;p&gt;National formats carry a trunk prefix in front of the number. International numbers do not. ITU-T E.164 defines the maximum length at 15 digits including the country code, and the trunk prefix is explicitly not part of the international number. Most libraries handle this for you when you parse with the right country context. Italy is the exception.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;As written nationally&lt;/th&gt;
&lt;th&gt;National significant number&lt;/th&gt;
&lt;th&gt;E.164&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;td&gt;030 123456&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30123456&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+4930123456&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United Kingdom&lt;/td&gt;
&lt;td&gt;0121 234 5678&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1212345678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+441212345678&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;td&gt;01 23 45 67 89&lt;/td&gt;
&lt;td&gt;&lt;code&gt;123456789&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+33123456789&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Italy&lt;/td&gt;
&lt;td&gt;02 1234 5678&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0212345678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+390212345678&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Germany, the United Kingdom and France drop the zero. Italy keeps it, because the zero belongs to the subscriber number rather than to the dialling plan. A normaliser that runs &lt;code&gt;digits.replace(/^0/, "")&lt;/code&gt; will corrupt every Italian number it touches, and the corrupted value will often still pass a length check afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a regex is the right tool, and where it is not
&lt;/h2&gt;

&lt;p&gt;Regex is useful in exactly one place: cheap input hygiene. It can strip brackets and dashes, catch an obvious typo before a request is sent, and keep junk out of your database. What it cannot do is decide whether a number is real, because that answer does not exist in the string.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want to do&lt;/th&gt;
&lt;th&gt;Right tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reject obviously mistyped input at the form&lt;/td&gt;
&lt;td&gt;Character and length check&lt;/td&gt;
&lt;td&gt;Cheap, forgiving, and does not block real numbers you cannot verify yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store numbers in one consistent form&lt;/td&gt;
&lt;td&gt;Parse to E.164 with a library&lt;/td&gt;
&lt;td&gt;Handles trunk prefixes and country context that a regex does not know about&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirm the prefix is assigned&lt;/td&gt;
&lt;td&gt;Prefix metadata, layer 2&lt;/td&gt;
&lt;td&gt;Requires the assigned-range tables, which change every year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decide whether to spend money on an SMS&lt;/td&gt;
&lt;td&gt;Network lookup, layer 3&lt;/td&gt;
&lt;td&gt;Only this layer knows whether the line can receive a message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which layer does your use case actually need
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Minimum layer&lt;/th&gt;
&lt;th&gt;Reasoning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Form validation before submit&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Stop typos, do not reject real numbers. Blocking a sign-up on layer 2 is how you lose customers who own valid numbers in recently opened ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleaning an imported CRM list&lt;/td&gt;
&lt;td&gt;2, then 3&lt;/td&gt;
&lt;td&gt;Layer 2 removes the bulk locally for free, then layer 3 is worth paying for on the remainder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sending SMS or OTP codes&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A valid fixed line that cannot receive SMS burns credit and reads as a delivery failure downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud and risk review&lt;/td&gt;
&lt;td&gt;3, plus line type&lt;/td&gt;
&lt;td&gt;VoIP, pager and non-fixed ranges behave differently from mobile ranges under the same geographies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De-duplicating a database&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Two spellings of one number collapse into a single record once both are parsed to E.164&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What none of these layers can tell you
&lt;/h2&gt;

&lt;p&gt;Layer 3 tells you a line exists. It does not tell you who uses it, whether they want your message, or whether they will answer a call. Push any of these tools past that line and you are guessing.&lt;/p&gt;

&lt;p&gt;Number types also move. A fixed line can become a VoIP line after porting, and number portability means the range data that originally described a block does not always describe the network serving it today. A type field is a strong hint with a real error rate, not a fact about the future.&lt;/p&gt;

&lt;p&gt;The practical consequence for your logs: record the library version and the lookup timestamp next to every result. When a number you validated six months ago now fails, you need to know whether the number changed or your data did.&lt;/p&gt;

&lt;p&gt;Our own API sits at layer 3. It reads allocation, line type and reachability from carrier data, and it returns a reason code for each verdict. It cannot repair a number that was typed wrong, and it cannot tell you whether the person behind the line will respond. Anyone selling you that answer is selling you something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running this in bulk
&lt;/h2&gt;

&lt;p&gt;The ordering that survives real data is: normalise everything to E.164 first, run layer 2 locally to shrink the list, then send only the survivors to a network lookup. Doing it in that order means you pay for layer 3 only on numbers that already passed the free checks.&lt;/p&gt;

&lt;p&gt;For a single number, the free &lt;a href="https://zelnum.com/phone-validator/" rel="noopener noreferrer"&gt;phone validator&lt;/a&gt; runs all three layers and returns the line type alongside the verdict. For lists, &lt;a href="https://zelnum.com/blog/how-to-validate-phone-numbers-bulk/" rel="noopener noreferrer"&gt;bulk validation methods compared&lt;/a&gt; walks through the ordering and the cost trade-offs, the &lt;a href="https://zelnum.com/carrier-lookup-api/" rel="noopener noreferrer"&gt;carrier lookup API&lt;/a&gt; and &lt;a href="https://zelnum.com/hlr-lookup-api/" rel="noopener noreferrer"&gt;HLR lookup API&lt;/a&gt; pages document the layer 3 endpoints, and the &lt;a href="https://zelnum.com/blog/how-to-find-phone-carrier/" rel="noopener noreferrer"&gt;carrier data guide&lt;/a&gt; explains what each returned field means before you route on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does isValidNumber() return false for a real phone number?
&lt;/h3&gt;

&lt;p&gt;Usually because the library's metadata predates the range. New area codes, reopened ranges and new mobile prefixes are rejected until the package is upgraded. In January 2026 three North American codes, 357, 837 and 679, were still failing on current installs, and switching to larger metadata builds did not fix it. Upgrade the library, and log its version next to each result so stale data is visible instead of silent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a regex enough to validate a phone number?
&lt;/h3&gt;

&lt;p&gt;No. A regex can confirm that a string looks like a phone number and strip formatting. It cannot know which prefixes a country has assigned, and it cannot know whether a line exists. Every pattern strict enough to be useful will also reject real numbers from open numbering plans such as Germany or Japan.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between isPossibleNumber and isValidNumber?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;isPossibleNumber()&lt;/code&gt; compares the digit count against the length range for that country. &lt;code&gt;isValidNumber()&lt;/code&gt; also checks the prefix against assigned ranges. A number can be possible and invalid at the same time, which is exactly what happens to a correctly shaped number carrying an unassigned area code.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long can an international phone number be?
&lt;/h3&gt;

&lt;p&gt;ITU-T E.164 caps an international number at 15 digits including the country code. The national part is shorter and its length depends on the country: 9 or 10 digits for most mobile numbers, 8 in Singapore, up to 12 in Indonesia. That is why you cannot validate a national number on digit count alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do some countries keep a leading zero and others drop it?
&lt;/h3&gt;

&lt;p&gt;The leading zero is a national trunk prefix, and the trunk prefix is not part of the international number. Most countries drop it when converting to E.164. Italy keeps it because the zero belongs to the subscriber number itself. This is why hard-coded zero-stripping damages Italian data while leaving the value looking plausible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does phone number validation tell you anything about messaging apps?
&lt;/h3&gt;

&lt;p&gt;No. Messaging presence is a separate check against each platform, and the answer changes from week to week as accounts are created and deleted. Validation answers whether the line exists and what type it is. Those are different questions with different sources, and a tool that claims both in one call is usually bolting two systems together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/blog/how-to-validate-phone-numbers-bulk/" rel="noopener noreferrer"&gt;How to validate phone numbers in bulk: methods compared&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/blog/how-to-find-phone-carrier/" rel="noopener noreferrer"&gt;How to find a phone number's carrier&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/blog/how-to-validate-phone-numbers-step-by-step/" rel="noopener noreferrer"&gt;Phone number validation, step by step&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zelnum.com/e164-phone-number-format/" rel="noopener noreferrer"&gt;E.164 phone number format&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>php</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
