Back to Blog
How to Validate US Phone Numbers in Your App: A Developer's Guide
Best Practices
May 15, 2026
14 min

How to Validate US Phone Numbers in Your App: A Developer's Guide

PT

PilotLookup Team

Author

1. Why US Phone Validation Is Harder Than It Looks

Most developers' first instinct is to write a quick regex — ^\d{10}$ — and call it done. That catches obvious garbage like letters and symbols. But it tells you almost nothing useful. A number that passes a format check might be:

  • Disconnected and unassigned
  • A VoIP number created in seconds and disposable after use
  • A valid landline that will never receive an SMS
  • A number that has been ported to a different carrier since it was collected
  • A toll-free number someone entered by mistake

Each of these cases has different implications depending on your use case. A fraud-prevention system cares deeply about VoIP vs. mobile. An SMS marketing platform cares about landlines that can't receive texts. A CRM cleanup job cares about disconnected numbers. Real US phone validation means handling all of these layers — not just shape-checking the string.

This guide is structured as a layered approach. You'll start with client-side format validation (cheap, instant, zero API calls), then add server-side carrier lookup (the layer that actually gives you signal). We'll cover every layer with production-ready code examples.

2. US Phone Number Format Rules

Before writing any validation code, it helps to understand the North American Numbering Plan (NANP) — the standard that governs US (and Canada/Caribbean) numbers. All US phone numbers follow a consistent structure:

PartDigitsExampleConstraints
Area Code (NPA)3203First digit must be 2–9. Second digit was historically 0 or 1, but modern NPAs can be any digit. Cannot start with 0 or 1.
Central Office Code (NXX)3214First digit must be 2–9. Cannot be 555-0100–555-0199 (reserved for fiction). N11 codes (211, 311, etc.) are service codes, not valid NXX.
Subscriber Number45454Any four digits.

Users submit phone numbers in wildly inconsistent formats. Your validation code needs to handle all of these as equivalent inputs:

Input formatNormalized E.164Valid?
2032145454+12032145454
(203) 214-5454+12032145454
+1 203 214 5454+12032145454
1-203-214-5454+12032145454
203.214.5454+12032145454
0002032145454✗ (too many digits)
1234567890✗ (NPA starts with 1)
5551234567✗ (NPA starts with 5 — technically valid format but NXX 555-0100 range is fictional)
💡 Pro tip
Strip all non-digit characters first, then apply NANP rules. Never try to match against formatted strings directly — the variation is infinite.

3. Layer 1 — Format Validation with Regex

Client-side and server-side format validation is your first gate. It's free, instantaneous, and catches garbage input before it ever touches your database or API budget. Here's a production-ready approach for each common language.

The normalization pattern

The key insight: normalize first, validate second. Strip everything non-digit, handle the optional leading 1 country code, then apply NANP rules.

JavaScript / Node.js
/**
 * Normalize and validate a US phone number.
 * Returns the E.164 string (+1XXXXXXXXXX) or null if invalid.
 */
function normalizeUSPhone(input) {
  // Strip everything except digits
  const digits = input.replace(/\D/g, '');

  // Handle optional leading country code
  let local = digits;
  if (digits.startsWith('1') && digits.length === 11) {
    local = digits.slice(1);
  }

  // Must be exactly 10 digits
  if (local.length !== 10) return null;

  const npa = local.slice(0, 3);   // area code
  const nxx = local.slice(3, 6);   // central office code

  // NANP: first digit of NPA and NXX must be 2–9
  if (/^[01]/.test(npa) || /^[01]/.test(nxx)) return null;

  // Reject N11 service codes as NXX (211, 311, 411, 511, 611, 711, 811, 911)
  if (/^[2-9]11$/.test(nxx)) return null;

  return `+1${local}`;
}

// Usage
console.log(normalizeUSPhone('(203) 214-5454'));   // +12032145454
console.log(normalizeUSPhone('1-800-555-0199'));   // +18005550199 (toll-free)
console.log(normalizeUSPhone('123-456-7890'));    // null (NPA starts with 1)
Python
import re

def normalize_us_phone(raw: str) -> str | None:
    """
    Normalize a US phone number to E.164 (+1XXXXXXXXXX).
    Returns None if the number fails NANP format rules.
    """
    digits = re.sub(r'\D', '', raw)

    # Strip leading country code if present
    if len(digits) == 11 and digits.startswith('1'):
        digits = digits[1:]

    if len(digits) != 10:
        return None

    npa, nxx = digits[:3], digits[3:6]

    # NPA and NXX must start with 2–9
    if npa[0] in ('0', '1') or nxx[0] in ('0', '1'):
        return None

    # Reject N11 service codes
    if re.match(r'^[2-9]11$', nxx):
        return None

    return f'+1{digits}'

# Usage
print(normalize_us_phone('203.214.5454'))   # +12032145454
print(normalize_us_phone('0123456789'))      # None
⚠ What format validation cannot tell you
A number that passes NANP format rules may still be unassigned, disconnected, a VoIP burner, or ported to a different carrier. Format validation is necessary but never sufficient on its own. You need Layer 2.

4. Layer 2 — Carrier and Line-Type Lookup via API

Once a number passes format validation, the second layer queries live carrier databases to answer three questions your app actually cares about:

  1. Is the number currently active? (assigned to a live subscriber)
  2. What carrier owns it right now? (accounting for number portability)
  3. What type of line is it? (mobile, landline, VoIP, toll-free)

This data comes from the Number Portability Administration Center (NPAC) — the authoritative database that tracks which carrier holds each US number after porting. A good phone validation API like PilotLookup queries NPAC daily to stay current, then returns the enriched data in a single REST call.

The PilotLookup API request

GET https://www.pilotlookup.net/api/validate?phone={number}
Authorization: Bearer YOUR_API_KEY

Example response

 JSON response — 200 OK — ~100ms
{
  "phone":     "+12032145454",
  "valid":     true,
  "carrier":   "T-Mobile",
  "line_type": "mobile",
  "city":      "Bridgeport",
  "state":     "CT"
}

Response field reference

FieldTypeValues / Notes
phonestringNormalized E.164 format
validbooleantrue = number is active and assigned to a carrier
carrierstringCurrent carrier (post-porting). E.g. AT&T, Verizon, T-Mobile, Google Voice
line_typestringmobile | landline | voip | toll-free | unknown
citystringCity associated with the NPA-NXX assignment (original registration location)
statestringTwo-letter state code

5. Code Examples — Full Integration

Node.js (fetch)

Node.js — async/await
const API_KEY = process.env.PILOTLOOKUP_API_KEY;

async function validatePhone(phone) {
  const normalized = normalizeUSPhone(phone);  // from Layer 1
  if (!normalized) {
    return { valid: false, error: 'Invalid format' };
  }

  const res = await fetch(
    `https://www.pilotlookup.net/api/validate?phone=${normalized.slice(2)}`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } }
  );

  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

// Express middleware example
app.post('/signup', async (req, res) => {
  const result = await validatePhone(req.body.phone);

  if (!result.valid) {
    return res.status(422).json({ error: 'Please enter a valid US phone number' });
  }

  if (result.line_type === 'voip') {
    // Flag for review, don't hard-block (see fraud section)
    req.body._riskFlag = 'voip';
  }

  if (result.line_type === 'landline') {
    // Warn user SMS won't work
    req.body._smsWarning = true;
  }

  // Proceed with enriched data...
  await db.users.create({
    phone:    result.phone,
    carrier:  result.carrier,
    lineType: result.line_type,
    state:    result.state,
  });

  res.json({ success: true });
});

Python (requests)

Python — Django / Flask view
import os, requests

PILOT_API_KEY = os.environ['PILOTLOOKUP_API_KEY']

def validate_phone(phone: str) -> dict:
    normalized = normalize_us_phone(phone)   # Layer 1
    if not normalized:
        return {"valid": False, "error": "Invalid format"}

    # API expects 10-digit number (strip +1)
    number = normalized[2:]

    resp = requests.get(
        f"https://www.pilotlookup.net/api/validate",
        params={"phone": number},
        headers={"Authorization": f"Bearer {PILOT_API_KEY}"},
        timeout=5,
    )
    resp.raise_for_status()
    return resp.json()

# Flask route example
@app.route('/api/register', methods=['POST'])
def register():
    data   = request.get_json()
    result = validate_phone(data.get('phone', ''))

    if not result.get('valid'):
        return jsonify(error="Invalid phone number"), 422

    line_type = result.get('line_type')
    risk_score = 0
    if line_type == 'voip':
        risk_score += 30
    elif line_type == 'landline':
        risk_score += 10

    user = User(
        phone=result['phone'],
        carrier=result.get('carrier'),
        line_type=line_type,
        risk_score=risk_score,
    )
    db.session.add(user)
    db.session.commit()
    return jsonify(success=True)

PHP (cURL)

PHP 8+
<?php

function validatePhone(string $phone): array {
    $digits = preg_replace('/\D/', '', $phone);
    if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
        $digits = substr($digits, 1);
    }
    if (strlen($digits) !== 10 ||
        preg_match('/^[01]/', $digits) ||
        preg_match('/^.[01]/', $digits)) {
        return ['valid' => false];
    }

    $url = "https://www.pilotlookup.net/api/validate?phone={$digits}";
    $ch  = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["Authorization: Bearer " . $_ENV['PILOTLOOKUP_API_KEY']],
        CURLOPT_TIMEOUT        => 5,
    ]);
    $body = curl_exec($ch);
    curl_close($ch);

    return json_decode($body, true);
}

6. When in the Request Lifecycle to Validate

Timing matters. Here's a breakdown of where to run each validation layer and the trade-offs:

Validation layerWhere to runLatencyCostBest for
Format regex Client-side (onBlur / onChange) <1ms Free Immediate UX feedback
Format + NANP rules Server-side (request handler) <1ms Free Guard before any API call
Carrier / line-type API Server-side (request handler) ~100ms $0.001–$0.005/call Fraud gating, SMS pre-checks
Carrier / line-type API Background job (post-save) Async $0.001–$0.005/call CRM enrichment, bulk hygiene

For high-trust flows like account signup or checkout, validate synchronously before completing the transaction. The ~100ms API overhead is invisible to users and prevents bad data from ever landing in your database. For lower-stakes flows like newsletter subscriptions, you can accept the submission, save the number, then enrich asynchronously in a queue worker.

ℹ Design pattern: cache by normalized number
Cache carrier lookup results keyed by the normalized E.164 number. Results are stable for days or weeks — numbers don't port every hour. A 24-hour TTL cache on frequently-submitted numbers can cut your API spend significantly in high-volume flows.

7. Recognizing Fraud Patterns with Line-Type Data

The line_type field is your highest-signal fraud indicator. Here's how to interpret it:

VoIP numbers

VoIP numbers (Google Voice, Skype, TextNow, Twilio, Bandwidth, etc.) are the preferred vehicle for fraud. They can be created programmatically in bulk, used once for a verification SMS, then abandoned. In datasets from e-commerce and fintech apps, VoIP numbers correlate significantly higher with chargebacks, trial abuse, and account takeovers than mobile numbers from major carriers.

However, not every VoIP number belongs to a fraudster. Remote workers, developers, and privacy-conscious users legitimately use Google Voice or similar services. A blanket block on VoIP is too aggressive for most consumer apps. The better approach:

  • Flag VoIP numbers for elevated risk scoring
  • Route VoIP signups to a manual review queue
  • Require additional verification (document ID, email confirmation) for VoIP accounts
  • Hard-block VoIP only for your highest-value / highest-risk actions (large purchases, withdrawals)

Landlines

A landline number is not inherently fraudulent, but it's usually wrong for SMS-dependent flows. If you rely on OTP delivery via SMS for account verification or two-factor authentication, a landline number means the user will never receive the code. Detect landlines at input time and offer an alternative (voice call OTP, email verification) rather than silently failing.

Toll-free numbers

A toll-free number (800, 844, 855, 866, 877, 888) entered as a personal number almost always indicates user error or form abuse. Block or flag these at the point of entry.

Risk scoring pattern

Node.js — risk scoring by line type and carrier
function computeRiskScore(validation) {
  let score = 0;

  if (!validation.valid)            score += 100;  // Invalid — hard block
  if (validation.line_type === 'voip')     score += 40;
  if (validation.line_type === 'toll-free') score += 80;
  if (validation.line_type === 'landline') score += 10;

  // Known VoIP carrier names add extra signal
  const voipCarriers = ['Google Voice', 'TextNow', 'Skype', 'MagicJack'];
  if (voipCarriers.includes(validation.carrier)) score += 20;

  return score;
}

// Decision logic
const risk = computeRiskScore(result);
if      (risk >= 100) rejectSubmission();
else if (risk >= 50)  flagForReview();
else if (risk >= 10)  requireAdditionalVerification();
else                   proceedNormally();

8. Bulk Validation for CRM Hygiene

If you have an existing database of phone numbers that were collected without validation, a bulk cleanup job pays for itself quickly — especially before an SMS campaign where you pay per message sent regardless of deliverability.

PilotLookup's API is stateless and parallelizable. A sensible bulk validation script uses a worker pool to run concurrent lookups while respecting rate limits. Here's a pattern using Python with asyncio:

Python — async bulk validation with rate limiting
import asyncio, aiohttp, os
from itertools import batched

API_KEY = os.environ['PILOTLOOKUP_API_KEY']
BASE_URL = 'https://www.pilotlookup.net/api/validate'
CONCURRENCY = 10   # concurrent requests — check your plan's rate limit

async def lookup_one(session, phone):
    async with session.get(
        BASE_URL,
        params={'phone': phone},
        headers={'Authorization': f'Bearer {API_KEY}'},
    ) as resp:
        data = await resp.json()
        return {'input': phone, **data}

async def bulk_validate(phone_numbers: list[str]) -> list[dict]:
    results = []
    sem = asyncio.Semaphore(CONCURRENCY)

    async def bounded_lookup(session, phone):
        async with sem:
            return await lookup_one(session, phone)

    async with aiohttp.ClientSession() as session:
        tasks = [bounded_lookup(session, p) for p in phone_numbers]
        results = await asyncio.gather(*tasks, return_exceptions=True)

    return [r for r in results if isinstance(r, dict)]

# Usage
phones = ['2032145454', '8005551234', '6462225555']
results = asyncio.run(bulk_validate(phones))

for r in results:
    status = '✓' if r.get('valid') else '✗'
    print(f"{status} {r['input']:15} {r.get('line_type','?'):10} {r.get('carrier','?')}")

For very large CRM datasets (100k+ records), run the job in batches during off-peak hours and write results back to a phone_validated_at + line_type column. This lets you segment campaigns by line_type = 'mobile' before sending, eliminating the cost of SMS delivery to landlines.

9. API Credits and Cost Planning

PilotLookup uses a credit-based model — no monthly subscriptions. You buy credits when you need them, and they count down per lookup. Here's the current pricing:

Starter
$25
5,000 credits
$0.005 / lookup
Professional
$75
25,000 credits
$0.003 / lookup
Enterprise
$300
500,000 credits
$0.0006 / lookup

Cost planning: At Business tier ($0.001/lookup), validating every signup at 10,000 signups/month costs $10/month. For context, a single fraudulent chargeback in e-commerce typically costs $15–$100 after fees. The math is straightforward — phone validation pays for itself by preventing even a handful of fraud events per month.

💡 Reduce costs with Layer 1 first
Run format validation (free) before every API call. In typical consumer-facing forms, 5–15% of phone number submissions fail basic format checks. Filtering these out before hitting the API reduces your credit spend by the same percentage at zero cost.

10. Implementation Checklist

Use this checklist before shipping phone validation to production:

ItemLayerDone?
Strip non-digit characters before any validation logicFormat
Handle optional leading 1 country codeFormat
Validate NPA and NXX first digits are 2–9Format
Reject N11 codes as NXX (211, 311, 411...)Format
Store numbers in E.164 format (+1XXXXXXXXXX)Format
API key stored in environment variable, never in codeAPI
Layer 1 format check runs before any API callAPI
API call has a timeout (5s max)API
Cache results by normalized E.164 number (24h TTL)API
Handle API errors gracefully (fail open or fail closed per risk level)API
Store carrier, line_type, state alongside the phone numberDB
VoIP numbers flagged, not necessarily hard-blockedRisk
Landlines warned about SMS non-deliverabilityUX
Toll-free numbers blocked or flagged at entryRisk
Format validation runs client-side for instant UX feedbackUX

Conclusion

Real US phone validation is a two-layer system: a lightweight format check that runs everywhere and costs nothing, followed by a carrier lookup API call that gives you the ground truth about whether a number is real, active, and what type of line it is. Together, they give your application exactly the intelligence it needs — whether you're preventing fraudulent signups, improving SMS deliverability, or cleaning a CRM before a campaign.

The implementation lift is low. A well-structured integration takes a backend developer a few hours, and the code examples in this guide cover the three most common stacks. The ongoing cost is fractions of a cent per lookup — dwarfed by the cost of fraud, failed SMS messages, and bad data at scale.

Start with PilotLookup's 10 free lookups — no credit card required — and you can have a working validation flow in production by the end of the day.

Start Validating US Phone Numbers Today

10 free lookups included. No credit card required. Live in production within an afternoon.

Get Your Free API Key →

Questions? Email support@pilotlookup.net or call 1-888-370-6801

Tags:
us phone validation · phone validation api · developer guide · carrier lookup · line type detection · fraud prevention · node.js · python · e.164 format · nanp · sms deliverability