An API client to communicate with Ipdata.co.
composer require ipdata/api-client
The client is built upon PSR standards. We use PSR-18 for HTTP client and PSR-17 for RequestFactory. These needs to be passed to the client's constructor.
Example packages for PSR-17 and PSR-18:
composer require nyholm/psr7 symfony/http-client
use Ipdata\ApiClient\Ipdata;
use Symfony\Component\HttpClient\Psr18Client;
use Nyholm\Psr7\Factory\Psr17Factory;
$httpClient = new Psr18Client();
$psr17Factory = new Psr17Factory();
$ipdata = new Ipdata('my_api_key', $httpClient, $psr17Factory);To route requests through ipdata's EU servers (Paris, Ireland and Frankfurt) for GDPR compliance, pass the EU base URL:
$ipdata = new Ipdata('my_api_key', $httpClient, $psr17Factory, Ipdata::EU_BASE_URL);Call lookup() with no arguments to get the location of the calling IP address.
$data = $ipdata->lookup();To send a geocode request you simply need to provide the IP address you are interested in.
$data = $ipdata->lookup('69.78.70.144');
echo json_encode($data, JSON_PRETTY_PRINT);The output will be the response from the API server with one additional status field.
{
"ip": "69.78.70.144",
"is_eu": false,
"city": null,
"region": null,
"region_code": null,
"country_name": "United States",
"country_code": "US",
"continent_name": "North America",
"continent_code": "NA",
"latitude": 37.751,
"longitude": -97.822,
"postal": null,
"calling_code": "1",
"flag": "https:\/\/ipdata.co\/flags\/us.png",
"emoji_flag": "\ud83c\uddfa\ud83c\uddf8",
"emoji_unicode": "U+1F1FA U+1F1F8",
"company": {
"name": "Verizon Wireless",
"domain": "verizonwireless.com",
"network": "69.78.0.0\/16",
"type": "isp"
},
"asn": {
"asn": "AS6167",
"name": "Cellco Partnership DBA Verizon Wireless",
"domain": "verizonwireless.com",
"route": "69.78.0.0\/16",
"type": "business"
},
"carrier": {
"name": "Verizon",
"mcc": "310",
"mnc": "004"
},
"languages": [
{
"name": "English",
"native": "English"
}
],
"currency": {
"name": "US Dollar",
"code": "USD",
"symbol": "$",
"native": "$",
"plural": "US dollars"
},
"time_zone": {
"name": "America\/Chicago",
"abbr": "CST",
"offset": "-0600",
"is_dst": false,
"current_time": "2020-01-25T06:14:37.081772-06:00"
},
"threat": {
"is_tor": false,
"is_vpn": false,
"is_icloud_relay": false,
"is_proxy": false,
"is_datacenter": false,
"is_anonymous": false,
"is_known_attacker": false,
"is_known_abuser": false,
"is_threat": false,
"is_bogon": false,
"blocklists": [],
"scores": {
"vpn_score": 0,
"proxy_score": 0,
"threat_score": 0,
"trust_score": 0
}
},
"count": "6",
"status": 200
}If you are not interested in all the fields in the response, you may query only the fields you want.
$data = $ipdata->lookup('69.78.70.144', ['longitude', 'latitude', 'country_name']);
echo json_encode($data, JSON_PRETTY_PRINT);{
"longitude": -97.822,
"latitude": 37.751,
"country_name": "United States",
"status": 200
}If you want to look up multiple IPs at the same time you may use the bulkLookup() function.
$data = $ipdata->bulkLookup(['1.1.1.1', '69.78.70.144'], ['longitude', 'latitude', 'country_name']);
echo json_encode($data, JSON_PRETTY_PRINT);{
"0": {
"longitude": 143.2104,
"latitude": -33.494,
"country_name": "Australia"
},
"1": {
"longitude": -97.822,
"latitude": 37.751,
"country_name": "United States"
},
"status": 200
}An Ipdata instance keeps the PSR-18 client you give it for as long as it lives and sends every request through
it, so the persistent HTTPS connections of that HTTP client are reused across lookups. Create one Ipdata per
process (or one per API key or endpoint), register it as a service in your container, and reuse it for every
lookup: each new client (and each new HTTP client) pays a fresh TLS handshake, which costs more than the lookup
itself. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to
HTTP/1.1 keep-alive otherwise.
The library is transport-agnostic, so "the platform" is the PSR-18 client you inject:
-
Symfony HttpClient with ext-curl (
Symfony\Component\HttpClient\Psr18Client, used in the examples above): nothing to configure. OnePsr18Clientkeeps its connections alive and negotiates HTTP/2 through ALPN on its own. By default it also ignores the protocol version of the PSR-7 request, so leave$httpVersionatnull. -
Guzzle and php-http/curl-client honour the protocol version of the PSR-7 request. Opt in to HTTP/2 by passing
'2.0'as the trailing$httpVersionconstructor argument; curl then negotiates HTTP/2 through ALPN and falls back to HTTP/1.1 when the server does not offer it:use GuzzleHttp\Client; use Ipdata\ApiClient\Ipdata; use Nyholm\Psr7\Factory\Psr17Factory; $ipdata = new Ipdata('my_api_key', new Client(), new Psr17Factory(), Ipdata::DEFAULT_BASE_URL, '2.0');
On PHP 8 you can skip the base URL with a named argument:
new Ipdata('my_api_key', new Client(), new Psr17Factory(), httpVersion: '2.0'). Without the argument the request keeps the version set by your PSR-17 factory (HTTP/1.1 withnyholm/psr7), and the connection is still kept alive between calls on the same instance. -
Stream-based clients (Guzzle without ext-curl, php-http/socket-client, anything built on PHP's
http://stream wrapper) must keep$httpVersionatnull: they write the version literally into the request line, and a request line readingHTTP/2.0breaks the request.
A few things to keep in mind:
- Under PHP-FPM or mod_php, PHP destroys all objects, and with them the curl handles, at the end of each PHP
request, so a connection is only reused by the lookups made within one PHP request. Long-running workers
(Symfony Messenger consumers, queue workers, RoadRunner, FrankenPHP worker mode, Swoole, CLI daemons) that keep
the same
Ipdatainstance benefit fully. - The PSR-18 API is synchronous, one request at a time, so HTTP/2 multiplexing brings nothing here: the saving is the TLS handshake on every call after the first.
- PHP runs one request per process, so there is no thread-safety concern. After
pcntl_fork(), build a newIpdataand HTTP client in the child instead of sharing the parent's sockets. Ipdatahas noclose()method because it does not own the connections: they belong to the PSR-18 client you injected and are closed when that client is released, at the latest when the process ends. Timeouts, proxies and other transport settings are configured on that client as well.- Every request carries a
User-Agent: ipdata-php/<version>header, where the version is the installed version of this package as recorded by Composer.
A list of all the fields returned by the API is maintained at Response Fields
The client returns the decoded JSON unchanged, so new fields reach you as soon as the API sends them. Recent additions:
is_satellite(boolean): the IP address belongs to a satellite internet provider. Present only whentrue; the key is omitted otherwise, so read it as$data['is_satellite'] ?? false.is_anycast(boolean): the IP address is an anycast address announced from several locations. Present only whentrue; read it as$data['is_anycast'] ?? false.threat.blocklists[].first_seen(string,YYYY-MM-DD, UTC): the first day the address appeared on that blocklist.threat.blocklists[].last_seen(string,YYYY-MM-DD, UTC): the most recent day the address appeared on that blocklist.threat.blocklists[].days_seen(integer): the number of days the address has been seen on that blocklist.
Some feeds, for example ipdata's own vpn feed, carry none of the three blocklist date fields, so read them as $entry['first_seen'] ?? null. The dates are passed through as strings; the client does not parse them. is_satellite and is_anycast can also be requested through the $fields argument of lookup() and bulkLookup().
A list of possible errors is available at Status Codes
Releases are cut by CI, and the version source is the first ## x.y.z heading in CHANGELOG.md: composer.json
carries no version field, and Packagist reads the versions of ipdata/api-client from the Git tags.
To release, add a ## x.y.z heading with the release notes at the top of CHANGELOG.md in a pull request and merge
it. On the push to master, once the tests have passed, the release job of the CI workflow tags the merge commit
x.y.z, creates the GitHub release with generated notes, and Packagist picks the new tag up through its GitHub hook.
A merge whose version is already tagged publishes nothing, so there is no manual tagging.