
How to Integrate a Phone Validation API in Under 10 Minutes
PilotLookup Team
Author
In this article
1. Before You Start: 2-Minute Setup
You need two things before writing any code: an API key and a test phone number. Both take under a minute each.
Get your API key: Register at pilotlookup.net/register — no credit card required. Your account includes 10 free lookups immediately. Once registered, navigate to the API Keys section of your dashboard and copy your key.
Set your environment variable: Store the key as an environment variable rather than hardcoding it anywhere in your source.
export PILOTLOOKUP_API_KEY="your_api_key_here"
For production deployments, add this to your environment configuration — Heroku config vars, AWS Secrets Manager, Vercel environment variables, Railway variables, or your equivalent. Never commit the raw key value to a repository.
Test phone number: Use your own mobile number for initial testing. You'll be able to verify the carrier and line type in the response against what you know to be true about your own number.
2. Node.js Integration
The following example works with Node.js 18+ using native fetch. It integrates cleanly into any Express, Fastify, or Next.js application with no additional dependencies.
Node.js — complete integration with format validationconst API_KEY = process.env.PILOTLOOKUP_API_KEY;
const BASE_URL = 'https://www.pilotlookup.net/api/validate';
// Layer 1: free format validation — runs before every API call
function normalizeUSPhone(input) {
const digits = input.replace(/\D/g, '');
const local = digits.startsWith('1') && digits.length === 11
? digits.slice(1)
: digits;
if (local.length !== 10) return null;
if (/^[01]/.test(local) || /^[01]/.test(local.slice(3))) return null;
return local;
}
// Layer 2: carrier lookup via API
async function validatePhone(rawPhone) {
const normalized = normalizeUSPhone(rawPhone);
if (!normalized) {
return { valid: false, error: 'Invalid phone number format' };
}
const res = await fetch(`${BASE_URL}?phone=${normalized}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` },
signal: AbortSignal.timeout(5000), // 5 second timeout
});
if (!res.ok) {
throw new Error(`API error ${res.status}: ${res.statusText}`);
}
return res.json();
}
// Express route example
import express from 'express';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
try {
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 === 'landline') {
return res.status(422).json({
error: 'This number cannot receive SMS. Please enter a mobile number.'
});
}
// Store user with enriched phone data
await db.users.create({
phone: result.phone, // normalized E.164
carrier: result.carrier,
line_type: result.line_type,
state: result.state,
risk_flag: result.line_type === 'voip' ? 'voip' : null,
});
res.json({ success: true });
} catch (err) {
console.error('Phone validation error:', err.message);
// Fail open — log for review, don't block the user
res.json({ success: true, _validation_skipped: true });
}
});
3. Python Integration
Works with Python 3.9+ using the requests library. Drop the route handler into any Django, Flask, or FastAPI application.
pip install requests
Python — complete integration with format validation
import os
import re
import requests
from requests.exceptions import Timeout, RequestException
API_KEY = os.environ['PILOTLOOKUP_API_KEY']
BASE_URL = 'https://www.pilotlookup.net/api/validate'
def normalize_us_phone(raw: str) -> str | None:
"""Strip formatting, validate NANP rules.
Returns 10-digit string or None if invalid."""
digits = re.sub(r'\D', '', raw)
if len(digits) == 11 and digits.startswith('1'):
digits = digits[1:]
if len(digits) != 10:
return None
if digits[0] in ('0', '1') or digits[3] in ('0', '1'):
return None
return digits
def validate_phone(raw_phone: str) -> dict:
"""Validate a US phone number.
Returns the API response dict, or an error dict."""
normalized = normalize_us_phone(raw_phone)
if not normalized:
return {'valid': False, 'error': 'Invalid phone number format'}
try:
resp = requests.get(
BASE_URL,
params={'phone': normalized},
headers={'Authorization': f'Bearer {API_KEY}'},
timeout=5,
)
resp.raise_for_status()
return resp.json()
except Timeout:
return {'valid': None, 'error': 'Validation service timeout'}
except RequestException as e:
return {'valid': None, 'error': str(e)}
# Flask route example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/signup', methods=['POST'])
def signup():
data = request.get_json()
result = validate_phone(data.get('phone', ''))
if result.get('valid') is False:
return jsonify(error=result.get('error', 'Invalid phone number')), 422
if result.get('line_type') == 'landline':
return jsonify(error='This number cannot receive SMS.'), 422
# Save user with enriched phone data
user = {
'phone': result.get('phone'),
'carrier': result.get('carrier'),
'line_type': result.get('line_type'),
'state': result.get('state'),
'risk_flag': 'voip' if result.get('line_type') == 'voip' else None,
}
# db.save(user)
return jsonify(success=True)
4. PHP Integration
Works with PHP 8.0+ using built-in cURL. No Composer dependencies required — paste this directly into any Laravel controller, Symfony service, or vanilla PHP handler.
PHP 8+ — complete integration with format validation<?php
$API_KEY = $_ENV['PILOTLOOKUP_API_KEY'];
$BASE_URL = 'https://www.pilotlookup.net/api/validate';
function normalize_us_phone(string $raw): ?string
{
$digits = preg_replace('/\D/', '', $raw);
if (strlen($digits) === 11 && str_starts_with($digits, '1')) {
$digits = substr($digits, 1);
}
if (strlen($digits) !== 10) return null;
if (in_array($digits[0], ['0', '1'])) return null;
if (in_array($digits[3], ['0', '1'])) return null;
return $digits;
}
function validate_phone(string $raw): array
{
global $API_KEY, $BASE_URL;
$normalized = normalize_us_phone($raw);
if (!$normalized) {
return ['valid' => false, 'error' => 'Invalid phone number format'];
}
$url = $BASE_URL . '?phone=' . urlencode($normalized);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $API_KEY"],
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['valid' => null, 'error' => $error];
}
return json_decode($body, true) ?? ['valid' => null, 'error' => 'Invalid JSON response'];
}
// Usage in a POST handler
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = validate_phone($_POST['phone'] ?? '');
if ($result['valid'] === false) {
http_response_code(422);
echo json_encode(['error' => $result['error'] ?? 'Invalid phone number']);
exit;
}
if (($result['line_type'] ?? '') === 'landline') {
http_response_code(422);
echo json_encode(['error' => 'This number cannot receive SMS.']);
exit;
}
// Persist enriched data
$phone = $result['phone'] ?? null; // E.164 normalized
$carrier = $result['carrier'] ?? null;
$line_type = $result['line_type'] ?? null;
$state = $result['state'] ?? null;
$risk_flag = $line_type === 'voip' ? 'voip' : null;
// $db->save(compact('phone', 'carrier', 'line_type', 'state', 'risk_flag'));
echo json_encode(['success' => true]);
}
5. Testing Your Integration
Once your integration code is in place, verify it end-to-end with these test cases before deploying to production:
| Input | Expected result | What it tests |
|---|---|---|
| Your own mobile number | valid: true, line_type: "mobile", correct carrier name |
Happy path — real active mobile number |
1234567890 |
Caught by format validation, no API call made | Format validation (NPA starts with 1) |
abc-not-a-number |
Caught by format validation, no API call made | Non-digit input handling |
8005551234 |
line_type: "toll-free" |
Toll-free number detection |
| A known Google Voice number | line_type: "voip", carrier: "Google Voice" |
VoIP detection accuracy |
| Empty string | Caught by format validation; user-friendly error returned | Empty input handling |
Also test your error handling paths: temporarily use an invalid API key and verify your code returns a graceful error rather than crashing. Test with the API key environment variable unset. Test what happens when you pass a number that is correctly formatted but not currently assigned — your application should handle valid: false cleanly. These edge cases are far easier to discover in staging than in production under real user load.
6. What to Build Next
A basic integration returning valid or invalid is a solid foundation. Here's what to layer on once the core is working and deployed:
- Caching. Cache results keyed by normalized E.164 number with a 24-hour TTL. This prevents duplicate API calls when the same number is submitted multiple times — common in signup retry flows and in shared-device environments where multiple users submit the same office number.
- Risk scoring. Add a
risk_scorecolumn to your users table. Seed it with 40 points for VoIP, 80 points for toll-free, and 0 for mobile. Use this score to route high-risk signups to a manual review queue rather than hard-blocking them — some VoIP users are legitimate. - Event emission. Emit a
phone.validatedevent after validation completes. This decouples downstream logic — fraud scoring, welcome SMS, CRM sync — from the signup handler and makes each step independently testable and observable. - Monitoring. Log every API call result to your observability stack: the phone hash (never the raw number), line type, carrier, response latency, and remaining credits. A sudden spike in VoIP numbers is an early signal of a fraud bot campaign. A drop in API response rate or a credit exhaustion event should page your on-call engineer before users are affected.
Start Integrating in the Next 10 Minutes
Get your free API key — no credit card, no waiting. 10 lookups included immediately.
Get Your API Key →Full API Docs · support@pilotlookup.net · 1-888-370-6801