PHP: Get the Location from an IP Address with the Official SDK
Short answer
To get the location of an IP address in PHP, install the official everapi/iplookupapi-php package with Composer, create an Iplookupapi\Iplookupapi\IplookupapiClient with your API key and call $client->info([‘ip’ => $ip]). The method returns the decoded JSON response as an array: country, region, city, coordinates, time zone and ISP are under the data key.
On this page
- Install the PHP package with Composer
- Authenticate and look up one IP address
- Get the visitor’s IP address and location in PHP
- Fields, language, hostname and status
- Look up up to 1,000 IPs in one batch request
- Handle errors, timeouts and retries
- Cache lookups to save requests
- Laravel: bind the client and use $request->ip()
- Symfony: trusted_proxies and getClientIp()
- WordPress: wp_remote_get() with transients
- Without Composer: plain cURL
The official PHP package everapi/iplookupapi-php wraps the API in one class, IplookupapiClient, with two methods: info() for a lookup and status() for your quota. Below: installation, single and visitor lookups, fields and languages, batches of 1,000 addresses, error handling, and recipes for Laravel, Symfony and WordPress. Other languages are on the SDK overview.
Install the PHP package with Composer
The package needs PHP 8.0 or later and installs Guzzle 7 (guzzlehttp/guzzle ^7.4) as its HTTP client. The package on Packagist currently publishes only a dev-master version, so a plain composer require stops with a minimum-stability error. Require the branch explicitly:
composer require everapi/iplookupapi-php:dev-masterThe package’s composer.json does not register its own namespace for autoloading. Add the mapping to your project’s composer.json, then regenerate the autoloader:
{
"require": {
"everapi/iplookupapi-php": "dev-master"
},
"autoload": {
"psr-4": {
"Iplookupapi\\Iplookupapi\\": "vendor/everapi/iplookupapi-php/src/"
}
}
}composer dump-autoloadTip
Write the class name with this exact capitalization, Iplookupapi\Iplookupapi\IplookupapiClient: Composer’s autoloader matches namespaces case-sensitively.
Authenticate and look up one IP address
Create a free key at app.iplookupapi.com/register and store it in the IPLOOKUPAPI_KEY environment variable, not in your code. The client sends it in the apikey HTTP header, so it does not end up in URLs or access logs.
<?php
require __DIR__ . '/vendor/autoload.php';
use Iplookupapi\Iplookupapi\IplookupapiClient;
$apiKey = getenv('IPLOOKUPAPI_KEY') ?: throw new RuntimeException('Set IPLOOKUPAPI_KEY');
$client = new IplookupapiClient($apiKey);
$result = $client->info(['ip' => '8.8.8.8']);
$data = $result['data'] ?? null;
if ($data === null) {
exit('Lookup failed: ' . ($result['message'] ?? 'no JSON response') . PHP_EOL);
}
echo $data['location']['country']['name'], PHP_EOL;
echo $data['location']['city']['name'] ?? 'unknown city', PHP_EOL;
echo $data['location']['latitude'], ', ', $data['location']['longitude'], PHP_EOL;
echo $data['timezone']['id'], PHP_EOL;
echo $data['connection']['isp'], ' (AS', $data['connection']['asn'], ')', PHP_EOL;info() and status() have no declared return type. Both return the JSON body decoded into an associative array, or null if the body is not JSON. The lookup sits under data: location, connection, timezone, range_type and security. Network, company and abuse data start at the Basic plan; security flags such as is_vpn start at Professional. The info endpoint reference lists every field.
Get the visitor’s IP address and location in PHP
In plain PHP, the address that connected to your server is in $_SERVER['REMOTE_ADDR']. Skip private and reserved addresses, such as 127.0.0.1 on your development machine, because they have no location.
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$public = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($public !== false) {
$result = $client->info([
'ip' => $public,
'fields' => 'location.country.alpha2,location.city.name,timezone.id',
]);
$country = $result['data']['location']['country']['alpha2'] ?? null;
}Always pass ip. Without it, the API answers for the address that sent the request, which in server-side code is your server.
Behind a load balancer, CDN or reverse proxy, REMOTE_ADDR holds the proxy’s address. The client address then arrives in a header such as X-Forwarded-For, which anyone can forge, so read it only from proxies you trust. The real client IP guide covers common setups, and get the IP address in PHP shows Laravel, Symfony and validation.
Fields, language, hostname and status
info() passes its array straight through as query parameters, so every parameter in the API documentation works:
| Parameter | Example | Effect |
|---|---|---|
ip | '2001:4860:4860::8888' | IPv4 or IPv6 address to look up |
fields | 'ip,location.country.alpha2,timezone.id' | Returns only these dot paths |
language | 'de' | Localized names: de, en, es, fr, ja, pt, ru, zh (default en) |
hostname | 1 | Adds a reverse DNS lookup to hostname; slower |
$result = $client->info(['ip' => '80.110.0.1', 'language' => 'de']);
echo $result['data']['location']['country']['name_translated'] ?? '', PHP_EOL;
$status = $client->status(); // free, does not count against your quota
echo 'Requests left this month: ', $status['quotas']['month']['remaining'] ?? 'n/a', PHP_EOL;Look up up to 1,000 IPs in one batch request
The SDK has no batch method yet. Use Guzzle, which the package already installed, to send up to 1,000 addresses per POST to /v1/batch. Each valid address counts as one request; invalid items come back in errors unbilled, and duplicates are billed once. If your quota cannot cover the whole batch, the API refuses it with 429 before any lookup. Billing details are on the batch IP lookup API page, parameters in the batch reference.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
$http = new Client([
'base_uri' => 'https://api.iplookupapi.com/v1/',
'headers' => ['apikey' => getenv('IPLOOKUPAPI_KEY')],
'timeout' => 30,
'http_errors' => false,
]);
$ips = ['8.8.8.8', '80.110.0.1', '2001:4860:4860::8888', 'not-an-ip'];
$results = [];
foreach (array_chunk(array_values(array_unique($ips)), 1000) as $chunk) {
try {
$response = $http->post('batch', [
'query' => ['fields' => 'ip,location.country.alpha2,location.city.name'],
'json' => ['ips' => $chunk],
]);
} catch (GuzzleException $e) {
exit('Batch failed: ' . $e->getMessage() . PHP_EOL); // network error or timeout
}
$body = json_decode((string) $response->getBody(), true);
if ($response->getStatusCode() !== 200) {
exit('Batch failed (HTTP ' . $response->getStatusCode() . '): ' . ($body['message'] ?? '') . PHP_EOL);
}
$results += $body['data'] ?? [];
foreach ($body['errors'] ?? [] as $item => $reason) {
echo "Skipped $item: $reason", PHP_EOL;
}
}
foreach ($results as $ip => $info) {
echo $ip, ' ', $info['location']['country']['alpha2'] ?? '-', PHP_EOL;
}Handle errors, timeouts and retries
The client configures Guzzle with http_errors off, so an HTTP error does not throw. You get the decoded error body instead: always a message, and on 401, 403, 404 and 429 an error object with a machine-readable code. The SDK does not expose the HTTP status code, so check for data:
| HTTP status | Typical cause | What to do |
|---|---|---|
| 401 | Missing or invalid key | Check that IPLOOKUPAPI_KEY is set for the PHP process |
| 403 | Feature not in your plan, or key for another product | Check the key and your plan |
| 404 | No data for this IP | Treat the location as unknown |
| 422 | Validation error, for example an invalid IP | Validate input with filter_var() first |
| 429 | rate_limit_exceeded (Free: 10/minute) or quota_exceeded | Fall back to “unknown”; upgrade or wait for the reset |
| 500 | Server error | Retry once |
Network failures and timeouts throw Iplookupapi\Iplookupapi\IplookupapiException. The default timeout is 15 seconds; lower it with the timeout setting. Anything in guzzle_opts is merged into the Guzzle options, including a connect timeout and Guzzle’s retry middleware:
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Iplookupapi\Iplookupapi\IplookupapiClient;
use Iplookupapi\Iplookupapi\IplookupapiException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
$stack = HandlerStack::create();
$stack->push(Middleware::retry(
function (int $retries, RequestInterface $request, ?ResponseInterface $response = null, ?Throwable $error = null): bool {
if ($retries >= 1) {
return false; // one retry, after a 1-second pause
}
return $error instanceof ConnectException // connection failure or timeout
|| ($response !== null && $response->getStatusCode() >= 500);
}
));
$client = new IplookupapiClient(getenv('IPLOOKUPAPI_KEY') ?: '', [
'timeout' => 3,
'guzzle_opts' => ['connect_timeout' => 2, 'handler' => $stack],
]);
try {
$result = $client->info(['ip' => '8.8.8.8']);
} catch (IplookupapiException $e) {
$result = null; // still failing after the retry
}
if (!isset($result['data'])) {
error_log('IP lookup failed: ' . ($result['error']['code'] ?? $result['message'] ?? 'no response'));
}Do not retry 429 inside a web request: the per-minute limit takes up to a minute to reset, the monthly quota a new period. Only successful calls count against your quota.
Cache lookups to save requests
Repeat visitors send the same IP many times, and its location rarely changes within a day. Cache each result by IP, for example with APCu:
function cachedLookup(IplookupapiClient $client, string $ip, int $ttl = 86400): ?array
{
$key = 'iplookup:' . $ip;
$cached = apcu_fetch($key, $found);
if ($found) {
return $cached;
}
$data = $client->info(['ip' => $ip])['data'] ?? null;
if ($data !== null) {
apcu_store($key, $data, $ttl);
}
return $data;
}APCu lives in the memory of one server. With several servers, use Redis, for example $redis->set($key, json_encode($data), ['ex' => 86400]) with the phpredis extension.
Tip
Do not cache failed lookups. A 429 or a timeout cached for a day turns a short outage into a day of missing locations.
Laravel: bind the client and use $request->ip()
Register the client as a singleton in app/Providers/AppServiceProvider.php:
use Iplookupapi\Iplookupapi\IplookupapiClient;
public function register(): void
{
$this->app->singleton(IplookupapiClient::class, fn () => new IplookupapiClient(
getenv('IPLOOKUPAPI_KEY') ?: '',
['timeout' => 5],
));
}Laravel loads .env into getenv() by default. After php artisan config:cache it no longer reads .env, so set IPLOOKUPAPI_KEY as a real environment variable on production servers.
Type-hint the client in a controller and cache with Cache::remember(). It treats a cached null as a miss, so failed lookups run again next time:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Iplookupapi\Iplookupapi\IplookupapiClient;
public function show(Request $request, IplookupapiClient $iplookup)
{
$ip = $request->ip();
$location = Cache::remember("iplookup:$ip", now()->addDay(), fn () => $iplookup->info([
'ip' => $ip,
'fields' => 'location.country.alpha2,location.city.name,timezone.id',
])['data'] ?? null);
return view('welcome', ['location' => $location]);
}$request->ip() returns the proxy’s address unless you trust it. In Laravel 11 and later, call $middleware->trustProxies(at: [...]) in bootstrap/app.php, as shown in the Laravel trusted proxies documentation. In Laravel 10 and earlier, set $proxies in app/Http/Middleware/TrustProxies.php.
Symfony: trusted_proxies and getClientIp()
Register the client as a service and pass the key from the environment:
# config/services.yaml
services:
Iplookupapi\Iplookupapi\IplookupapiClient:
arguments:
$apiKey: '%env(IPLOOKUPAPI_KEY)%'Tell Symfony which proxies may set forwarded headers, as described in Symfony’s proxy documentation:
# config/packages/framework.yaml
framework:
trusted_proxies: '127.0.0.1,10.0.0.0/8'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto']Then $request->getClientIp() returns the visitor’s address:
use Iplookupapi\Iplookupapi\IplookupapiClient;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
public function location(Request $request, IplookupapiClient $iplookup): JsonResponse
{
$result = $iplookup->info(['ip' => $request->getClientIp(), 'fields' => 'location.country.alpha2']);
return new JsonResponse($result['data'] ?? null);
}WordPress: wp_remote_get() with transients
WordPress does not need Composer. Its HTTP API calls the REST endpoint directly, and a transient caches the answer:
function myplugin_visitor_country(): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return '';
}
$cache_key = 'iplookup_' . md5($ip);
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$url = 'https://api.iplookupapi.com/v1/info?' . http_build_query([
'ip' => $ip,
'fields' => 'location.country.alpha2',
]);
$response = wp_remote_get($url, [
'headers' => ['apikey' => getenv('IPLOOKUPAPI_KEY')],
'timeout' => 5,
]);
if (is_wp_error($response) || (int) wp_remote_retrieve_response_code($response) !== 200) {
return '';
}
$body = json_decode(wp_remote_retrieve_body($response), true);
$country = $body['data']['location']['country']['alpha2'] ?? '';
set_transient($cache_key, $country, DAY_IN_SECONDS);
return $country;
}With full-page caching, PHP runs once per cached page, not per visitor, so one visitor’s country ends up in the cache. Exclude those pages from the page cache or load the location with a separate uncached request.
Without Composer: plain cURL
Any PHP install with the cURL extension can call the API directly:
<?php
$ch = curl_init('https://api.iplookupapi.com/v1/info?' . http_build_query(['ip' => '8.8.8.8']));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['apikey: ' . getenv('IPLOOKUPAPI_KEY')],
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$result = $body === false ? null : json_decode($body, true);
if ($status !== 200 || !isset($result['data'])) {
exit("Lookup failed (HTTP $status): " . ($result['message'] ?? curl_error($ch)) . PHP_EOL);
}
echo $result['data']['location']['country']['name'], PHP_EOL;Note
An IP location is an estimate of where the network is registered and routed, not a street address. Country is usually reliable; city can be off, especially on mobile networks. See how accurate IP geolocation is. In the EU, IP addresses can count as personal data, so keep cache lifetimes short and store only what you need.
To choose a plan by monthly lookups, see pricing.
Frequently asked questions
How do I get the location of an IP address in PHP?
Send the IP address to an IP geolocation API and read the location from the response. With the official iplookupapi PHP package, call $client->info([‘ip’ => ‘8.8.8.8’]) and read $result[‘data’][’location’][‘country’][’name’] and $result[‘data’][’location’][‘city’][’name’]. Without Composer, send a GET request to https://api.iplookupapi.com/v1/info?ip=8.8.8.8 with your key in the apikey header and decode the JSON.
How do I get the visitor's IP address in PHP?
Read $_SERVER[‘REMOTE_ADDR’]. It holds the address of whatever opened the TCP connection to your server. Behind a load balancer, CDN or reverse proxy, that is the proxy’s address, and the client’s address is in a header such as X-Forwarded-For. Only trust that header when the request comes from a proxy you control; the real client IP guide explains how.
Why does my PHP IP lookup return my server's location?
There are two common causes. If you call the info endpoint without the ip parameter, the API answers for the address that sent the request, which is your server. If you pass $_SERVER[‘REMOTE_ADDR’] behind a proxy or CDN, you look up the proxy. Pass the visitor’s address explicitly and configure trusted proxies in your framework.
Can I get a location from an IP address in PHP for free?
Yes. The iplookupapi Free plan includes 150 requests a month at up to 10 requests a minute, with location, connection, time zone and currency data. Cache each result for up to a day so repeat visitors do not use extra requests. Paid plans start at $9.99 a month for 65,000 requests; see pricing.
Does the iplookupapi PHP SDK support batch lookups?
The current version of everapi/iplookupapi-php has two methods, info() and status(), and no batch method. For lists, send a POST request to https://api.iplookupapi.com/v1/batch with up to 1,000 addresses in {“ips”: […]}. Guzzle is already installed as a dependency of the SDK, so you can use it for that call.