PHP
The official PHP client library for the InternetData database API.
Getting Started
composer require internetdata/internetdataRequires PHP 8.2 or newer.
Usage
Every database call needs a key carrying the db.download scope, which you create in the console. The Open databases are licensed to every organization from the start and are free under CC BY-SA 4.0, and list() marks them open. The rest are licensed by contract; if your organization doesn't have a license yet, request an evaluation. Then:
use InternetData\Client;
use InternetData\Options;
$client = new Client(new Options(apiKey: getenv('INTERNETDATA_API_KEY') ?: null));
foreach ($client->database->list() as $database) {
echo "{$database->base}: {$database->standing}\n"; // bogon_ip: licensed
}Every database call hangs off $client->database, which is where the sibling VPNDetection library keeps the same seven calls.
A license is held against a database FAMILY, while a download names one version, so the IDs you pass everywhere else come from versions:
$database->base; // 'bogon_ip'
$database->versions[0]->id; // 'bogon_ip_v1', the ID a download takes
$database->versions[0]->formats; // ['csvgz', 'mmdb']
$database->licenseType; // 'standard', or null when there is no licenseDownloading
download streams a file straight to disk, so nothing bigger than a chunk is ever held in memory whatever the database weighs. It writes through a neighboring .part file and renames on success, so a transfer that dies half way leaves no truncated file that reads as a whole database:
$written = $client->database->download('bogon_ip_v1', 'mmdb', '/srv/data/bogon_ip_v1.mmdb');
echo "{$written} bytes";Pass a stream you opened instead of a path, and it stays yours to close:
$handle = fopen('php://temp', 'w+b');
$client->database->download('bogon_ip_v1', 'csvgz', $handle);downloadUrl hands back the time-limited link the API redirects to, without following it. The link authorizes itself, so it carries none of your credentials and you can pass it to whatever should run the transfer:
$url = $client->database->downloadUrl('bogon_ip_v1', 'mmdb');downloadBytes returns the file as a string. It holds the whole thing in memory, and the catalog spans seven orders of magnitude, so check metadata first for anything you have not measured: past your memory_limit this is a fatal error, not merely a slow one.
$bytes = $client->database->downloadBytes('bogon_asn_v1', 'csvgz');Each of the three downloads the build that is live unless you name one. A database.updated webhook names the build it announces and carries its sha256, so a receiver that downloads after the next publish asks for that build to get the file the hash is of:
$client->database->download('vpn_ip_v1', 'mmdb', './vpn_ip_v1.mmdb', $event['data']['build']);A build is kept 7 days. Past that the call throws bad_request with the message BUILD_GONE, and downloading without a build fetches the live one. A name that is not a build's shape (20261105.0t53s5) throws InvalidArgumentException before anything is sent.
What is inside, and whether it changed
metadata answers the schema, a few real rows, the row count and the size of every published format, without moving the file. Poll it to decide whether today's build is worth fetching, and to budget a transfer before you start it:
$meta = $client->database->metadata('bogon_ip_v1');
$meta->updated->format('Y-m-d'); // '2026-09-04'
$meta->entries; // 44
$meta->size['csvgz']; // 760
$meta->schema['csvgz'][0]->name; // 'start_ip'checksums publishes four digests per file, so a download can be verified against what the API says it served:
$sums = $client->database->checksums('bogon_ip_v1', 'csvgz');
hash_file('sha256', '/srv/data/bogon_ip_v1.csv.gz') === $sums->sha256; // trueDownload history
downloads lists your organization's recent attempts, newest first. Refusals are listed too, because a denial is what answers "it stopped working" and its absence answers nothing:
foreach ($client->database->downloads(20) as $attempt) {
echo "{$attempt->created->format('c')} {$attempt->databaseId} {$attempt->outcome}\n";
}Errors
Failures throw an InternetDataException carrying a kind and an isRetryable() flag:
use InternetData\InternetDataException;
try {
$client->database->download('bogon_ip_v1', 'mmdb', '/srv/data/bogon_ip_v1.mmdb');
} catch (InternetDataException $err) {
echo $err->kind->value, ' ', $err->isRetryable() ? 'retryable' : 'final', "\n";
}kind is one of bad_request, unauthorized, forbidden, rate_limited, quota_exceeded, server_error or network. The message is the API's own result code where it sent one, so a refusal says which refusal it was: NOT_LICENSED and LICENSE_EXPIRED are both a forbidden, and only one of them is fixed by renewing.
rate_limited is a 429 that carries Retry-After, and the library retries it for you. quota_exceeded is a 429 without one, which it never retries. InternetData's database API has no quota, so you won't see quota_exceeded from it.
Retries are configurable, and only ever repeat a request that could still succeed:
$client = new Client(new Options(apiKey: $key, retries: 4));What your key can see
list is the catalog as the API served it for your key, and the library keeps no copy of its own.
Sign in with OAuth (device flow)
A program running on the person's own machine can let them sign in with a browser and pick one of their API keys, instead of asking them to paste it:
$client = new Client();
$device = $client->oauth->deviceAuthorization('your-client-id', [
'scope' => 'account.read apikeys.read apikeys.reveal',
]);
echo "Open {$device->verificationUri} and enter {$device->userCode}\n";
$token = $client->oauth->pollDeviceToken('your-client-id', $device);
if ($token->apikey === null) {
throw new RuntimeException("no API key came back: none was picked, or it can't be shown again");
}
$keyed = new Client(new Options(apiKey: $token->apikey));A denied sign-in throws OauthAccessDeniedException and a code that ran out OauthExpiredTokenException. Client IDs are issued on request from support@internetdata.io, and $client->oauth->revoke('your-client-id', $token->refreshToken) signs the machine out again.
Sign in with OAuth (authorization code)
An app that can take a browser redirect signs the person in there instead, with a PKCE pair made for that one sign-in:
$client = new Client();
$redirectUri = 'http://127.0.0.1:8765/callback';
$pkce = $client->oauth->createPkce();
$url = $client->oauth->authorizationUrl('your-client-id', $redirectUri, $pkce->challenge, [
'scope' => 'apikeys.use', 'state' => 'your-state',
]);
// Open $url in the browser. Its redirect to $redirectUri carries code and state.
$token = $client->oauth->exchangeAuthorizationCode('your-client-id', $code, $pkce->verifier, $redirectUri);Check that state came back as you sent it before you exchange code, which works once. The client ID can also be the https URL of a client metadata document your app serves, and such an app is never handed a key, so $token->apikey stays null.
Register a client
An app with no client ID, and no client metadata document to use as one, registers itself, then signs in with the clientId it gets back as the client ID for authorizationUrl and exchangeAuthorizationCode:
$client = new Client();
$registration = $client->oauth->register(['http://127.0.0.1:8765/callback'], ['clientName' => 'Your App']);Each redirect URI must be https on a public host name, or http on a loopback address, which then matches any port. Nothing is kept until a person approves the client, so an app with no client ID saved can simply register again.