Overview
The IPFly PHP SDK wraps the IPFly API in a single dependency-free file. It uses the curl extension when available for real parallel batch lookups, and falls back to PHP's built-in streams otherwise — so your integration code can stay focused on what to do with the response instead of the transport layer.
TTL + LRU caching
In-process memory, a shared JSON file, or APCu — shared across PHP-FPM workers with no disk I/O.
Automatic retries
Exponential backoff with jitter on network errors and 5xx responses. 4xx fails fast, no wasted retries.
Real parallel batching
batchLookup() uses curl_multi for genuine concurrency — not a sequential loop dressed up as a batch.
Client-side rate limiting
A token-bucket limiter smooths bursts in long-running scripts and batch jobs.
Typed exceptions
A single IPFlyException with ->getStatus() and ->getErrorCode() instead of parsing messages.
Zero dependencies
One file, PHP 7.4+. No Composer package required. MIT licensed.
Where This SDK Belongs
This client is meant to run server-side — in a web backend, a CLI script, a queue worker, a cron job. It is not meant to be shipped to a browser or a distributed client app where your API token could be extracted.
If you need geolocation results in a frontend app, put this SDK behind your own endpoint and call that endpoint from the client (see the Laravel proxy example below), or use the IPFly JavaScript SDK with a domain-restricted token for prototyping.
Installation
No Composer package is published — copy ipfly_sdk.php into your project.
# Drop ipfly_sdk.php directly into your source tree, e.g.: cp ipfly_sdk.php src/IPFly/ipfly_sdk.php
curl -O https://ipfly.world/libs/php-sdk/ipfly_sdk # or wget https://ipfly.world/libs/php-sdk/ipfly_sdk
<?php
require_once __DIR__ . '/ipfly_sdk.php';
use IPFly\IPFlyClient;
use IPFly\IPFlyException;
$client = new IPFlyClient('YOUR_TOKEN');Requirements
PHP 7.4 or later. The curl extension is optional but recommended — it enables real parallel batch lookups and more precise timeouts; without it the SDK falls back to PHP's stream wrappers automatically. The apcu extension is only needed if you choose 'storage' => 'apcu' for the cache.
Quick Start
Create a client once, then call lookup() wherever you need geolocation data.
<?php
require_once 'ipfly_sdk.php';
use IPFly\IPFlyClient;
use IPFly\IPFlyException;
$client = new IPFlyClient('YOUR_TOKEN', [
'include' => 'security', // optional: request the security/ASN field set on every call
]);
// Look up a specific IP
$data = $client->lookup('8.8.8.8');
echo $data['city'], ' ', $data['country_name'], "\n";
// Look up the caller's own IP (omit the ip argument)
$me = $client->lookupSelf();
echo "You appear to be in {$me['city']}\n";
// Handle errors explicitly
try {
$client->lookup('not-an-ip');
} catch (IPFlyException $err) {
echo $err->getErrorCode(), ': ', $err->getMessage(), "\n";
}Client Options
The second constructor argument is an options array: new IPFlyClient($token, [...]).
| Option | Type | Default | Description |
|---|---|---|---|
base_url | string | ipfly.world/api | Override to point at your own backend proxy. |
include | string | null | Default include value sent with every request (e.g. "security"). Overridable per call. |
timeout | float | 8.0 | Per-request timeout in seconds. |
retries | int | 2 | Retry attempts for network errors and 5xx. 4xx is never retried. |
retry_delay | float | 0.3 | Base delay (seconds) for exponential backoff; jitter is added automatically. |
rate_limit | float | 0 | Max requests/second this client will issue. 0 = unlimited. |
concurrency | int | 6 | Default max parallel cURL handles for batchLookup(). |
cache | array | see below | Response cache settings — see Cache Options. |
debug | bool | false | Write internal lifecycle events to STDERR (tokens are redacted). |
on_request | callable | null | function(?string $ip, string $url): void — fired right before a request is sent. |
on_response | callable | null | function(?string $ip, array $data, bool $fromCache): void — fired after a successful lookup. |
on_error | callable | null | function(?string $ip, IPFlyException $error): void — fired when a lookup ultimately fails. |
on_cache_hit | callable | null | function(?string $ip, array $data): void — fired when a cached response is served instead of a network call. |
Cache Options
Passed as the cache array, e.g. new IPFlyClient($token, ['cache' => ['storage' => 'apcu', 'ttl' => 3600]]).
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Set false to disable caching entirely. |
ttl | float | 300 | How long a cached response stays valid, in seconds. |
max_size | int | 500 | Max cached entries before least-recently-used entries are evicted. |
storage | string | 'memory' | 'memory', 'file', or 'apcu' — see the comparison below. |
path | string | system temp dir | File path used when storage => 'file'. |
prefix | string | 'ipfly:' | Key prefix used when storage => 'apcu'. |
Which storage should I pick?
'memory' lives only for the current request/process — fine for CLI scripts, mostly useless for a typical PHP-FPM app where each request is a fresh process. 'file' persists a shared JSON file across requests; simple and portable, best at low/medium traffic. 'apcu' is shared in-memory across PHP-FPM workers on the same machine with no disk I/O — the best choice for a typical web app, if the extension is installed.
lookup() & lookupSelf()
lookup(?string $ip = null, array $options = []): array looks up a single IP. Pass null (or omit) to geolocate the caller — or use the lookupSelf(array $options = []) shorthand.
$client->lookup('1.1.1.1', [
'include' => 'security', // overrides the client-level `include` for this call only
'skip_cache' => true, // force a fresh network request, bypassing the cache
'timeout' => 3.0, // override the client's default timeout for this call
]);Returns the parsed JSON response as an associative array (see Response Shape). Throws IPFlyException on failure (see Error Handling).
batchLookup()
batchLookup(array $ips, ?int $concurrency = null, array $options = []): BatchResult[] looks up an array of IPs. When the curl extension is loaded this runs genuinely in parallel via curl_multi, bounded by $concurrency; otherwise it falls back to a sequential loop automatically. This call never throws for an individual IP failure — it returns a list of BatchResult in the same order as $ips.
use IPFly\BatchResult;
$results = $client->batchLookup(['8.8.8.8', '1.1.1.1', 'not-an-ip']);
foreach ($results as $r) {
if ($r->ok) {
echo "{$r->ip} -> {$r->data['city']}\n";
} else {
echo "{$r->ip} failed: {$r->error->getErrorCode()}\n";
}
}BatchResult is a small value object: public string $ip, public bool $ok, public ?array $data, public ?IPFlyException $error.
Cache Controls
| Method | Returns | Description |
|---|---|---|
clearCache() | void | Empties the response cache immediately. |
cacheStats() | array | Returns ['enabled', 'size', 'ttl', 'max_size'] — useful for logging or a health-check endpoint. |
is_valid_ip()
A namespaced function that validates an IPv4 or IPv6 string without making a request.
use function IPFly\is_valid_ip;
is_valid_ip('8.8.8.8'); // true
is_valid_ip('2001:4860:4860::8888'); // true
is_valid_ip('not-an-ip'); // falseResponse Shape
Successful lookups return the same JSON structure the IPFly API returns, unchanged, as a PHP associative array — see the full field reference in the API docs. Shape depends on your plan and the include value requested.
[
'ip' => '8.8.8.8',
'hostname' => 'dns.google',
'country_name' => 'United States',
'city' => 'Mountain View',
'latitude' => '37.4056',
'longitude' => '-122.0775',
'time_zone' => ['name' => 'America/New_York', /* ... */],
'asn' => ['asn' => 'AS15169', 'name' => 'Google LLC', /* ... */],
'security' => ['is_vpn' => false, 'is_tor' => false, /* ... */],
]Error Handling
All failures throw IPFly\IPFlyException, a RuntimeException subclass with getMessage(), getStatus(), getErrorCode(), and getCause().
| Code | Meaning |
|---|---|
| MISSING_TOKEN | No token was supplied when creating the client. |
| INVALID_IP | The IP string failed local validation before any request was sent. |
| TIMEOUT | The request exceeded timeout. |
| NETWORK_ERROR | The request failed before a response was received (offline, DNS, connection refused, etc). |
| HTTP_ERROR | The API responded with a non-2xx status — check getStatus() (401 = bad token, 403 = plan/permission issue, 404 = private/bogon IP, 429/5xx = retry-worthy). |
| BAD_RESPONSE | The server responded but the body wasn't valid JSON, or the response was empty. |
use IPFly\IPFlyClient;
use IPFly\IPFlyException;
$client = new IPFlyClient('YOUR_TOKEN');
try {
$client->lookup('8.8.8.8');
} catch (IPFlyException $err) {
if ($err->getErrorCode() === 'TIMEOUT') {
// retry later, log, fall back to a default
} elseif ($err->getStatus() === 401) {
// token is invalid — surface a config error, don't retry
}
error_log($err->getMessage());
}Performance Notes
Caching
Use 'apcu' storage for a typical PHP-FPM web app so the cache is actually shared across requests, rather than reset every time like 'memory' would be.
Parallel batching
batchLookup() uses curl_multi so N IPs don't cost N × round-trip-time — they're bounded by concurrency concurrent connections instead.
Rate limiting
A courtesy limiter on the client side that smooths bursts (e.g. a bulk-enrichment script) so you don't blow through your plan's requests/second in a tight loop.
Retries
Only apply to transient failures (network errors, timeouts, 5xx), including inside a batch. A 401/403/404 fails fast since retrying won't fix a bad token or a private IP.
Examples
Two common integration patterns: enriching a CSV of visitor IPs in bulk, and proxying instead of embedding your token in a client-facing app.
<?php
require_once 'ipfly_sdk.php';
use IPFly\IPFlyClient;
$client = new IPFlyClient(getenv('IPFLY_TOKEN'), [
'cache' => ['storage' => 'file', 'path' => __DIR__ . '/geo_cache.json'],
]);
$ips = [];
if (($handle = fopen('visitors.csv', 'r')) !== false) {
$header = fgetcsv($handle);
$ipCol = array_search('ip', $header, true);
while (($row = fgetcsv($handle)) !== false) {
$ips[] = $row[$ipCol];
}
fclose($handle);
}
foreach ($client->batchLookup($ips, 8) as $result) {
if ($result->ok) {
echo "{$result->ip} -> {$result->data['country_name']}\n";
} else {
echo "{$result->ip} -> error: {$result->error->getErrorCode()}\n";
}
}<?php
require_once __DIR__ . '/../../ipfly_sdk.php';
use IPFly\IPFlyClient;
use IPFly\IPFlyException;
$client = new IPFlyClient(getenv('IPFLY_TOKEN'));
$ip = $_GET['ip'] ?? $_SERVER['REMOTE_ADDR'];
header('Content-Type: application/json');
try {
echo json_encode($client->lookup($ip, ['include' => 'security']));
} catch (IPFlyException $err) {
http_response_code($err->getStatus() ?: 502);
echo json_encode(['error' => $err->getErrorCode(), 'message' => $err->getMessage()]);
}<?php
namespace App\Http\Controllers;
use IPFly\IPFlyClient;
use IPFly\IPFlyException;
use Illuminate\Http\Request;
class GeoController extends Controller
{
public function __invoke(Request $request)
{
static $client = null;
$client ??= new IPFlyClient(config('services.ipfly.token'), [
'cache' => ['storage' => 'apcu'],
]);
try {
return response()->json(
$client->lookup($request->query('ip') ?? $request->ip(), ['include' => 'security'])
);
} catch (IPFlyException $err) {
return response()->json(
['error' => $err->getErrorCode(), 'message' => $err->getMessage()],
$err->getStatus() ?: 502
);
}
}
}In both proxy examples, your frontend calls your own endpoint — the IPFly token stays in this process and is never sent to the client.
Requirements
PHP 7.4 or later (uses typed properties and arrow functions; no PHP 8-only syntax required). The curl extension is optional but recommended — without it, the SDK automatically falls back to PHP's built-in stream wrappers for single lookups and runs batchLookup() sequentially. The apcu extension is only needed for 'storage' => 'apcu' caching.
License
MIT — use it, fork it, ship it.