Prerequisites
- PHP 8.1 or higher with the
curlandjsonextensions - An ip-api.io API key — get one free
- Composer for installation — no other packages required
Install the package
Add the official ip-api-io/ipapi-php package with Composer. It has
zero Composer dependencies — only the ext-curl and
ext-json extensions, which ship with most PHP installs.
composer require ip-api-io/ipapi-php Construct the client
Import IpApiIo\Client and create one instance with your API key. Read the
key from an environment variable — never hardcode it. The API rejects keyless requests
with 401.
<?php
require 'vendor/autoload.php';
use IpApiIo\Client;
$client = new Client(apiKey: getenv('IP_API_IO_KEY')); Reuse a single client across your request — register it as a singleton in your framework's service container (Laravel, Symfony) so the cURL handle is shared.
Look up an IP address
Call lookup($ip) for any IPv4 or IPv6 address, or lookup()
with no argument to resolve the caller's own IP. Every response is an associative array
with a location array and a suspicious_factors array.
$info = $client->lookup('8.8.8.8');
echo $info['ip']; // "8.8.8.8"
echo $info['isp']; // "Google LLC"
echo $info['location']['country']; // "United States"
echo $info['location']['city']; // "Mountain View"
echo $info['location']['timezone']; // "America/Los_Angeles"
// Resolve the caller's own IP (no argument):
$me = $client->lookup();
echo $me['ip'], ' ', $me['location']['country']; $info['suspicious_factors']['is_vpn']. Fields
are null for private ranges or unrecognized addresses.
Detect VPN, proxy, Tor & batch lookups
The suspicious_factors array carries seven boolean flags on every
lookup — this is what sets ip-api.io apart from plain geolocation APIs. You get
security intelligence in the same call, at no extra cost.
$info = $client->lookup('185.220.101.45'); // example Tor exit node
$sf = $info['suspicious_factors'];
if ($sf['is_vpn'] || $sf['is_proxy'] || $sf['is_tor_node']) {
echo 'Anonymized traffic — consider a CAPTCHA or block';
}
if ($sf['is_datacenter']) {
echo 'Cloud/datacenter IP — likely automated';
}
if ($sf['is_threat']) {
echo 'Active threat signal — block this request';
}
$isRisky = $sf['is_vpn'] || $sf['is_proxy'] || $sf['is_tor_node'] || $sf['is_threat'];
Need to check many IPs at once? lookupBatch takes up to 100 addresses in a
single request (it throws an InvalidArgumentException if the array is empty
or longer than 100):
$batch = $client->lookupBatch(['8.8.8.8', '1.1.1.1', '9.9.9.9']);
echo $batch['total_processed'], ' ', $batch['successful_lookups'];
foreach ($batch['results'] as $ip => $result) {
echo $ip, ' ', $result['location']['country'], ' ', $result['suspicious_factors']['is_vpn'];
} $client->riskScore($ip) for a combined
fraud risk score from
0 to 100.
Production-ready error handling
The client throws typed exceptions so you can react to each failure precisely. It
never retries on its own — on a rate limit, RateLimitError
tells you exactly when to try again.
use IpApiIo\Client;
use IpApiIo\IpApiError;
use IpApiIo\RateLimitError;
use IpApiIo\AuthenticationError;
use IpApiIo\InvalidRequestError;
use IpApiIo\ServerError;
$client = new Client(apiKey: getenv('IP_API_IO_KEY'));
try {
$info = $client->lookup('8.8.8.8');
// ... use $info
} catch (RateLimitError $e) {
// HTTP 429 — quota exhausted
echo "Rate limited. Remaining: {$e->remaining}. Resets at {$e->reset}";
} catch (AuthenticationError $e) {
echo 'Invalid or missing API key — get one free at https://ip-api.io';
} catch (InvalidRequestError $e) {
echo 'Bad request: ', $e->getMessage();
} catch (ServerError $e) {
echo 'ip-api.io is having issues — try again later';
} catch (IpApiError $e) {
echo "error {$e->statusCode}: ", $e->getMessage();
} RateLimitError::$reset (a Unix timestamp) to schedule the next attempt
instead of hammering the API.
Behind a load balancer, run the lookup server-side and pass the real client IP from the
X-Forwarded-For header:
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
$clientIp = trim(explode(',', $xff)[0]) ?: ($_SERVER['REMOTE_ADDR'] ?? '');
try {
$info = $client->lookup($clientIp);
header('Content-Type: application/json');
echo json_encode([
'ip' => $info['ip'],
'country' => $info['location']['country'],
'is_vpn' => $info['suspicious_factors']['is_vpn'],
'is_threat' => $info['suspicious_factors']['is_threat'],
]);
} catch (IpApiError $e) {
// Treat ip-api.io as a non-critical dependency
echo json_encode(['ip' => $clientIp, 'country' => null]);
}