Skip to content
Finowl

PHP · Email breach checks

Check email breaches in PHP with cURL

For PHP applications the email breach check usually runs after a registration form is submitted or from a queued Laravel job that rechecks accounts. json_decode with the associative flag turns the response into arrays, so a foreach over $result['breaches'] gives you each record's Title, BreachDate and DataClasses. implode makes the data classes readable for a notification email or an admin screen.

The script below is a self-contained CLI program you can run with php check-email.php. It retries only when the 429 code is rate_limited, using the Retry-After header captured by a cURL header callback. A 400 throws InvalidArgumentException, since the address itself needs fixing, and a 401 points at the API key. Any other status, including quota_exceeded, surfaces as a RuntimeException with the error code.

Steps

  1. 1

    Confirm the cURL extension is enabled with php -m and look for curl in the list.

  2. 2

    Export FINOWL_API_KEY, or set it in your web server or PHP-FPM pool environment.

  3. 3

    Save the example as check-email.php and run php check-email.php alex@example.com.

  4. 4

    Use the Title, BreachDate and DataClasses values in your notification or admin view.

Check email breaches in PHP with cURL
<?php

declare(strict_types=1);

function checkEmail(string $email, int $attempts = 3): array
{
    for ($i = 0; $i < $attempts; $i++) {
        $retryAfter = 1;
        $ch = curl_init('https://api.finowl.co/v1/breaches/email');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . getenv('FINOWL_API_KEY'),
                'Content-Type: application/json',
            ],
            CURLOPT_POSTFIELDS => json_encode(['email' => $email], JSON_THROW_ON_ERROR),
            CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$retryAfter): int {
                if (stripos($header, 'Retry-After:') === 0) {
                    $retryAfter = max(1, (int) trim(substr($header, 12)));
                }
                return strlen($header);
            },
        ]);
        $body = curl_exec($ch);
        if ($body === false) {
            throw new RuntimeException('Network error: ' . curl_error($ch));
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $data = json_decode($body, true) ?? [];
        $code = $data['error']['code'] ?? 'unknown_error';

        if ($status === 200) {
            return $data;
        } elseif ($status === 400) {
            throw new InvalidArgumentException("Invalid email address: {$code}");
        } elseif ($status === 401) {
            throw new RuntimeException('Unauthorized: check FINOWL_API_KEY');
        } elseif ($status === 429 && $code === 'rate_limited') {
            sleep($retryAfter);
            continue;
        }
        throw new RuntimeException("Finowl error {$status}: {$code}");
    }
    throw new RuntimeException('Still rate limited after several attempts');
}

$result = checkEmail($argv[1] ?? 'alex@example.com');

if (!$result['breached']) {
    echo $result['email'] . ' does not appear in any known breach' . PHP_EOL;
}
foreach ($result['breaches'] as $breach) {
    $flag = $breach['IsVerified'] ? '' : ' [unverified]';
    echo "{$breach['Title']} ({$breach['BreachDate']}){$flag}" . PHP_EOL;
    echo '  exposed: ' . implode(', ', $breach['DataClasses']) . PHP_EOL;
}

IN PRODUCTION

Production tips

  • Run the lookup from a queue worker, such as a Laravel job, instead of inside the request that renders the signup page. That keeps form submission fast even when the API is slow.

  • PHP-FPM workers each make their own requests, so a burst of signups can add up quickly. Throttle the queue to keep the whole application under 60 requests per minute.

  • Escape breach fields with htmlspecialchars before echoing them into HTML. Treat Description in particular as untrusted text and sanitize it if you choose to display it.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Check email breaches in PHP with cURL

Grab an API key, paste the example and run it. 100 free credits every month.