Tutorial · PHP

Validate phone numbers in PHP

Validate and normalise phone numbers in plain PHP or Laravel, enrich them with carrier and line type, and check whole lists in batches, with code we ran on PHP 8.5.

Updated October 29, 2026 · 5 min read

Phone number validation in PHP takes two tools: libphonenumber-for-php checks numbers against each country’s numbering plan and formats them, offline; the numlookupapi API adds carrier, line type and location for any number when you need them. This guide shows both in plain PHP and in Laravel, plus batch validation for lists. We ran every snippet with PHP 8.5, libphonenumber-for-php 9.0.42 and the Laravel 13 components; the API calls ran against a local stub that returns the documented responses.

Validate and format with libphonenumber for PHP

composer require giggsey/libphonenumber-for-php
<?php
require __DIR__ . '/vendor/autoload.php';

use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberToCarrierMapper;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;

$util = PhoneNumberUtil::getInstance();

try {
    $number = $util->parse('07400 123456', 'GB');
    var_dump($util->isValidNumber($number));                         // bool(true)
    echo $util->format($number, PhoneNumberFormat::E164), "\n";          // +447400123456
    echo $util->format($number, PhoneNumberFormat::INTERNATIONAL), "\n"; // +44 7400 123456
    echo $util->format($number, PhoneNumberFormat::NATIONAL), "\n";      // 07400 123456
    var_dump($util->getNumberType($number) === PhoneNumberType::MOBILE); // bool(true)
    echo $util->getRegionCodeForNumber($number), "\n";                 // GB
    echo PhoneNumberToCarrierMapper::getInstance()->getNameForNumber($number, 'en'), "\n"; // Three
} catch (NumberParseException $e) {
    echo 'Not a phone number: ', $e->getMessage(), "\n";
}

The version 9 releases support PHP 8.1 to 8.5.

Carrier and line type with the numlookupapi API

Get a free API key on the pricing page (250 requests a month for testing and personal projects, no credit card; commercial use from the Basic plan) and set it as NUMLOOKUPAPI_KEY. The function handles the three kinds of answers: a result, a 422 for malformed input, and errors such as a wrong key or a used-up quota:

<?php
// phone_api.php – PHP 8.1 or later with the curl extension

function lookupPhone(string $number, ?string $countryCode = null): array
{
    $url = 'https://api.numlookupapi.com/v1/validate/' . rawurlencode($number);
    if ($countryCode !== null) {
        $url .= '?' . http_build_query(['country_code' => $countryCode]);
    }
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['apikey: ' . getenv('NUMLOOKUPAPI_KEY')],
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 10,
    ]);
    $body = curl_exec($ch);
    if ($body === false) {
        throw new RuntimeException('numlookupapi: ' . curl_error($ch));
    }
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
    if ($status === 422) {
        return ['valid' => false, 'error' => $json['message']]; // invalid_number, invalid_country_code
    }
    if ($status !== 200) {
        throw new RuntimeException("numlookupapi $status: " . ($json['error']['code'] ?? $json['message']));
    }
    return $json; // an unknown number is 200 with "valid" => false
}

$result = lookupPhone('+14158586273');
echo $result['line_type'], ' ', $result['carrier']; // mobile AT&T Mobility LLC

There is no curl_close(): the handle is freed automatically since PHP 8.0, and PHP 8.5 deprecates the function. The answer contains valid, country_code, location, carrier, line_type, number_type (with voip for internet-based numbers), timezones and the number in E.164, national, international and RFC 3966 formats; the validate endpoint documentation lists every field. Check valid rather than the HTTP status: a number that is not in use returns 200 with "valid": false. A 429 means the monthly quota is used up or, on the free plan, more than 10 requests a minute were sent.

In Laravel: the HTTP client

use Illuminate\Support\Facades\Http;

$result = Http::withHeaders(['apikey' => config('services.numlookupapi.key')])
    ->timeout(10)
    ->get('https://api.numlookupapi.com/v1/validate/' . rawurlencode($phone))
    ->throw()
    ->json();

Put the key in .env and read it in config/services.php ('numlookupapi' => ['key' => env('NUMLOOKUPAPI_KEY')]), so config:cache keeps working. throw() turns 401, 422 and 429 answers into a RequestException; validate the format with the rule below first, so user typos never reach the API.

A Laravel validation rule

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;

class ValidPhone implements ValidationRule
{
    public function __construct(private string $defaultRegion = 'US') {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $util = PhoneNumberUtil::getInstance();
        try {
            $number = $util->parse((string) $value, $this->defaultRegion);
        } catch (NumberParseException) {
            $fail('The :attribute is not a valid phone number.');
            return;
        }
        if (! $util->isValidNumber($number)) {
            $fail('The :attribute is not a valid phone number.');
        }
    }
}
$request->validate(['phone' => ['required', new ValidPhone('GB')]]);

In our run through Laravel’s validator, 020 7946 0958 and +1 415 858 6273 passed with the default region GB, while 07400 12 and hello failed with “The phone is not a valid phone number.” After validation, store the E.164 form ($util->format($number, PhoneNumberFormat::E164)). If you need the carrier or VoIP detection, call the API in a queued job and save line_type and number_type next to the number. The propaganistas/laravel-phone package offers a ready-made rule and cast built on the same library.

Validate a list in batches

POST /v1/validate takes up to 100 numbers per request. Each number counts as one request, identical entries in the same batch are validated and billed once, and the answer is keyed by the number exactly as you sent it:

$results = [];
foreach (array_chunk(array_values(array_unique($numbers)), 100) as $chunk) {
    $response = Http::withHeaders(['apikey' => config('services.numlookupapi.key')])
        ->timeout(30)
        ->post('https://api.numlookupapi.com/v1/validate', ['numbers' => $chunk])
        ->throw();
    $results += $response->json('data'); // keyed by the number as sent
}

Add 'country_code' => 'GB' to the body for numbers without a country prefix. A number that cannot be read gets "error": "invalid_number" in its own entry instead of failing the batch. The same number in two spellings counts twice, so normalise to E.164 first. Without code, the bulk phone validator checks a list in your browser.

What validation cannot tell you

A valid number exists in the numbering plan, but may be switched off or ported to another network. The carrier field (in the library and in the API) is the network the number range was assigned to. For the live status and the network after porting, an HLR lookup asks the mobile network: add hlr=1 on a paid plan (see the HLR lookup API).

More languages: phone number validation in JavaScript and in Python.

Frequently asked questions

Which PHP library validates phone numbers?

giggsey/libphonenumber-for-php, the PHP port of Google's libphonenumber. Install it with composer require giggsey/libphonenumber-for-php; version 9 requires PHP 8.1 or later.

How do I validate phone numbers in Laravel?

Write a validation rule that parses the number with libphonenumber and calls isValidNumber(), as shown in this guide, or use the propaganistas/laravel-phone package, which wraps the same library.

How do I format a phone number to E.164 in PHP?

Parse it with PhoneNumberUtil::getInstance()->parse($input, 'GB') and call format($number, PhoneNumberFormat::E164), which returns a string such as +447400123456. Store that form and format nationally only for display.

How do I get the carrier of a phone number in PHP?

PhoneNumberToCarrierMapper returns the network a number block was originally assigned to, and an empty string for US numbers. The numlookupapi API returns carrier and line type for any number, the US included; an HLR lookup (hlr=1, paid plans) shows the network after porting.

Phone number API

Validate numbers like this in your app

numlookupapi checks any phone number worldwide in one request and returns validity, line type, carrier, location and every standard format as JSON.

Read the documentation

250 free requests every month. No credit card required.

GET https://api.numlookupapi.com/v1/validate/+12069220880

{
  "valid": true,
  "number": "12069220880",
  "local_format": "2069220880",
  "international_format": "+12069220880",
  "country_prefix": "+1",
  "country_code": "US",
  "country_name": "United States of America",
  "location": "Seattle",
  "line_type": "landline",
  "e164_format": "+12069220880",
  "national_format": "(206) 922-0880"
}

Keep reading

Start using our phone number API for free today!

Get 250 requests / month for free