How to Confirm Phone Number Ownership with Lookup Identity Match in PHP

August 31, 2026
Written by

Identity Match for phone numbers in the United States and Brazil doesn't require carrier registration and approval. Phone numbers from Canada, France, Germany, Italy, the Netherlands, Spain, and the United Kingdom require additional carrier registration and approval. Learn more about the carrier approval process here.

There's a lot you can learn from a phone number. Lookup Identity Match uses phone numbers as an identity to provide a zero-knowledge match against authoritative data sources like mobile carrier records. This allows you to build real-time confirmation that you're interacting with a real person linked to the phone number provided.

Diagram showing identity lookup using phone number, last name, and DOB with a missing first name.

Use Identity Match to prevent fake accounts and improve trust with legitimate, unique users. Customers can use Identity Match at sign up, login, and even during account changes or checkout. Best of all, Identity Match happens seamlessly in the background, with lower costs and friction than alternatives like credit bureau and document verification used to satisfy Anti-Money Laundering (AML) rules.

This blog post will show you how to use Twilio Lookup Identity Match to confirm phone number ownership and reduce sign up fraud.

Here's a sneak peak at what the Identity Match request looks like. Learn more about accepted parameters and coverage in the documentation.

curl -X GET "https://lookups.twilio.com/v2/PhoneNumbers/+14159929960\
?Fields=identity_match\
&FirstName=Jane\
&LastName=Doe\
&AddressLine1=321+Main+Street\
&City=Kingston\
&State=NY\
&PostalCode=12401\
&AddressCountryCode=US\
&DateOfBirth=19860414" \
-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN

Prerequisites

To code along with this post you'll need:

  1. A Twilio account. Sign up or sign in.
  2. Submit this form for Identity Match approval (Outside the US and Brazil only; takes 2-4 weeks)
  3. PHP 8.5
  4. Composer installed globally

Build the app

This post uses the international telephone input Code Exchange project as a starting point, but you could use your own sign up form instead. The base project relies on the excellent intl-tel-input plugin for collecting phone numbers in E.164 format.

Step 1: Clone the starter Code Exchange project

Start by creating a new PHP project from the Twilio Slim Base Project, add in the other required directories, and install the slim/twig-view package to add Twig template support:

composer create-project settermjd/twilio-slim-base-project identity-match-php
cd identity-match-php
mkdir -p data src/templates
composer require slim/twig-view

Open up the newly created identity-match-php directory in your preferred text editor and navigate to the .env file to set the value of your Account SID and Auth Token; you can find them on the front page of your Twilio Console.

Step 2: Extend your sign up form with Identity Match

Identity match needs information about the phone number owner to match against. Parameters can be any combination of name, address, national ID or date of birth.

This example is going to use first and last name, so you need to start by making sure there are fields to collect those details:

Create a new file named index.html.twig in src/templates file, and paste the code below into the file:

<!doctype html>
<html lang="en-AU">
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Identity Match Check</title>
    <link href="/css/styles.css" rel="stylesheet">
</head>
<body class="container">
    <main>
        <h1>Identity Match Check</h1>
        <form
            action="/"
            method="POST"
        >
            <div>
                <label for="firstName">First name:</label>
                <input id="firstName" type="text" name="firstName" required value="{{ firstName|default('') }}">
            </div>
            <div>
                <label for="lastName">Last name:</label>
                <input id="lastName" type="text" name="lastName" required value="{{ lastName|default('') }}">
            </div>
            <div>
                <label for="phone">Phone number:</label>
                <input id="phone" type="tel" name="phone" required inputmode="numeric" value="{{ phone|default('') }}">
            </div>
            <div>
                <input type="submit" class="primary" value="Verify" />
            </div>
        </form>
        {% if msg %}
        <div class="message-box">
            {{ msg }}.
        </div>
        {% endif %}
    </main>
</body>
</html>

This will render a small form allowing for your first name, last name, and phone number to be submitted.

Now, download the application's CSS file to public/css, naming it styles.css.

Then, replace the existing code in public/index.php with the following:

<?php

declare(strict_types=1);

use App\Application;
use DI\Container;
use Dotenv\Dotenv;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Twilio\Rest\Client;

require __DIR__ . '/../vendor/autoload.php';

$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();

$dotenv->required(
    [
        'TWILIO_ACCOUNT_SID',
        'TWILIO_AUTH_TOKEN',
    ],
)->notEmpty();

$container = new Container();

$client = new Client($_ENV['TWILIO_ACCOUNT_SID'], $_ENV['TWILIO_AUTH_TOKEN']);
$container->set(
    Client::class,
    fn(): Client => $client,
);

AppFactory::setContainer($container);
$app = AppFactory::createFromContainer($container);
$app->add(
    TwigMiddleware::create(
        $app,
        Twig::create(__DIR__ . '/../src/templates', ['cache' => false]),
    ),
);

$application = new Application($app, $client);
$application->setupRoutes();
$application->run();

The code initialises a new Slim Framework object. It passes that to a new Application object; which contains all of the functionality for handling requests to the application's various default route. The Application object is also initialised with a Twilio Rest Client object for making requests to Twilio's APIs before Twig middleware is instantiated, setting a Twig object in the application's request that will retrieve template files from the src/templates directory.

Now, update the code in src/Application.php with the following:

<?php

declare(strict_types=1);

namespace App;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App as SlimApp;
use Slim\Middleware\ContentLengthMiddleware;
use Slim\Views\Twig;
use Twilio\Rest\Client;

use function sprintf;

final class Application
{
    public function __construct(
        private readonly SlimApp $app,
        private readonly Client $client,
    ) {
        $app->add(new ContentLengthMiddleware());
        $app->addBodyParsingMiddleware();
        $app->addRoutingMiddleware();
        $app->addErrorMiddleware(true, true, true);
    }

    public function setupRoutes(): void
    {
        $this->app->map(['GET', 'POST'], '/', [$this, 'handleDefaultRoute']);
    }

    public function getRoutes(): array
    {
        return $this->app->getRouteCollector()->getRoutes();
    }

    public function run(): void
    {
        $this->app->run();
    }

    public function handleDefaultRoute(
        ServerRequestInterface $request,
        ResponseInterface $response,
    ): ResponseInterface {
        $templateData = [];

        return Twig::fromRequest($request)
            ->render(
                $response,
                'index.html.twig',
                $templateData,
            );
    }
}

The Application object's constructor:

  • Adds the ability to parse JSON request bodies, as that is how Twilio packages request and response information
  • Loads the application's routing table (defined in setupRoutes())
  • Adds error middleware for handling fatal errors and exceptions

The setupRoutes() function defines a single route (/), handled by the handleDefaultRoute() function, accessible with GET and POST requests. Currently, that function renders and returns src/templates/index.html.twig, which we saw earlier.

Step 3: Interpret the results of a Lookup Identity Match response

After calling the Lookup API, you need to decide what to do with the information in the response. Here's what the identity match portion of a sample response looks like:

{
  "first_name_match": "exact_match",
  "last_name_match": "high_partial_match",
  "address_lines_match": "no_match",
  "city_match": "no_match",
  "state_match": "high_partial_match",
  "postal_code_match": "no_data_available",
  "address_country_match": "exact_match",
  "national_id_match": null,
  "date_of_birth_match": "exact_match",
  "summary_score": 90,
  "error_code": null,
  "error_message": null
},

Possible responses include exact_match, no_match, and no_data_available. Name and address fields also support partial matches. Learn more about match scores in the documentation. The API will respond null if you don't provide an input to match on.

This application is only looking at name matches and will consider the match a success if it's an exact match or high partial match (e.g., Kelly instead of Kelley – happens all the time).

Back in src/Application.php, replace handleDefaultRoute() with the following implementation:

public function handleDefaultRoute(
    ServerRequestInterface $request,
    ResponseInterface $response,
): ResponseInterface {
    $templateData = [];

    if ($request->getMethod() === "POST") {
        $requestData = $request->getParsedBody();
        $phoneNumber = $requestData['phone'] ?? '';
        $firstName   = $requestData['firstName'] ?? '';
        $lastName    = $requestData['lastName'] ?? '';

        $templateData['firstName'] = $firstName;
        $templateData['lastName'] = $lastName;
        $templateData['phone'] = $phoneNumber;

        $numberDetails = $this->client
            ->lookups
            ->v2
            ->phoneNumbers($phoneNumber)
            ->fetch(
                [
                    "fields"    => "identity_match",
                    "firstName" => $firstName,
                    "lastName"  => $lastName,
                ],
            );

        $firstNameMatch = $numberDetails->identityMatch['first_name_match'] ?? '';
        $lastNameMatch  = $numberDetails->identityMatch['last_name_match'] ?? '';

        $success = $numberDetails->valid
            && in_array($firstNameMatch, ['exact_match', 'high_partial_match'])
            && in_array($lastNameMatch, ['exact_match', 'high_partial_match']);

        $templateData['responseData'] = $numberDetails;

        $templateData['msg'] = $success
        ? sprintf("First name is %s. Last name is %s", $firstNameMatch, $lastNameMatch)
        : "Cannot register with the information provided. Please ensure you're using a phone number you own.";
    }

    return Twig::fromRequest($request)
        ->render(
            $response,
            'index.html.twig',
            $templateData,
        );
}

This new version uses the Twilio Rest Client to call Twilio's Lookup API, to retrieve Lookup data on the provided phone number. If the phone number is valid, and the first and last names are an exact or high partial match, it sets a template variable named msg that shows the matches values of the first and last names. Otherwise, it sets the variable to "Cannot register with the information provided. Please ensure you're using a phone number you own.".

Run the application

Navigate back to your terminal, and start the application with composer serve. Now head back over to http://localhost:3000/index and test it out!!

Form showing user information input fields with the verification result.

Next steps for identity verification

You can extend your application with other Lookup packages like Line Type Intelligence, SIM swap detection and more. Check out our documentation to learn more about what's possible with the Lookup API.

Another way to prevent invalid phone numbers is to do phone verification - we also have an API for that! I can't wait to see what you build and secure.

Matthew Setter is a PHP, Go, and Rust Editor in the Twilio Voices team. He’s also the author of Mezzio Essentials and Deploy with Docker Compose. You can find him at msetter@twilio.com. He's also on LinkedIn and GitHub.