Official PHP client for the IPScanner API.
Full API reference: https://ipscanner.io/api-documentation
composer require ipscanner.io/sdkRequires PHP 8.1 or newer with the curl and json extensions.
use IPScanner\Client;
$client = new Client(apiKey: 'pk_live_...');
$result = $client->ip->lookup('8.8.8.8');
echo $result['verdict']['classification'];
echo $result['purity']['grade'];Every method returns the decoded JSON response as an associative array, using the same key names as the API.
vpnProvider is the VPN brand (for example Mullvad) when known, otherwise null; provider is the network owner. On the Free plan, premium fields are null and listed in locked, with planRequired naming the plan that includes them.
$client->ip->lookup('example.com'); // IPv4, IPv6, CIDR or hostname
$client->ip->vpn('1.2.3.4');
$client->ip->proxy('1.2.3.4');
$client->ip->geo('2001:db8::1');
$client->ip->asn('1.2.3.4');
$client->ip->whois('example.com');
$client->ip->history(limit: 50, verdict: 'vpn');
$client->ip->demo('1.2.3.4'); // no key needed
$client->ip->myip(); // no key neededPage through history by passing the previous nextBefore as before. A nextBefore of 0 means there are no more pages.
vpn and proxy carry purity (the score breakdown) on paid plans; a locked answer has deductionCount instead.
$result = $client->email->check('jane@outlok.com');
// $result['status'] 'risky', $result['reason'] 'typo', $result['suggestion'] 'jane@outlook.com'Checks syntax, throwaway domains, whether the domain receives mail, typos of big webmail domains and role addresses. The mailbox itself is not probed. One request per address.
$report = $client->bulk->check(ips: ['1.2.3.4', '5.6.7.8']);
$report = $client->bulk->check(input: file_get_contents('ips.txt'));stream() returns a generator of events as the server produces them: one meta, then result and error events, then one done. Missing and null fields are filled with empty defaults.
foreach ($client->bulk->stream(ips: $ips) as $event) {
if ($event['type'] === 'result') {
printf("%s %s %d\n", $event['ip'], $event['verdict'], $event['score']);
} elseif ($event['type'] === 'done' && !$event['complete']) {
printf("Stopped early: %s\n", $event['reason']);
}
}complete on the done event is true only when the reason is complete and every address was processed. The request is sent when iteration starts.
$client->agentscan->check(
ip: '1.2.3.4',
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
headers: ['accept-language' => 'en-GB'],
);
$client->agentscan->verify(ip: '66.249.66.1', bot: 'googlebot');
$client->agentscan->batch([
['ip' => '1.2.3.4', 'userAgent' => 'curl/8.0'],
]);
$client->agentscan->allowlist();
$client->agentscan->selfCheck();$attestation = $client->provenance->check(ip: '1.2.3.4', claimedJurisdiction: 'GB');
echo $attestation['policy_action'];
$client->provenance->verify();
$client->provenance->verifyAnchored();
$client->provenance->chain(limit: 100);
$client->provenance->jurisdictions();
$csv = $client->provenance->export(from: '2026-01-01', to: '2026-01-31');The check response uses snake_case keys, as the API returns them. export returns the CSV as a string.
$result = $client->edge->check(
ip: '1.2.3.4',
site: 'site_...',
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
);
echo $result['class'];
$client->sites->policy('site_...');edge->check is metered as 2 requests. site is null when no site was given or the site is unknown.
$result = $client->gate->verify(
secret: 'gs_...',
token: $token, // posted by gate.js
remoteIp: $_SERVER['REMOTE_ADDR'],
email: $_POST['email'] ?? null, // optional: adds the email check
);
if ($result['action'] === 'block') {
// reject the form
}gate->verify sends the site secret instead of the API key and is never retried. The response uses snake_case keys. With email, $result['email'] holds the check and its action, and $result['action'] is the stricter of the network and email actions. Token errors (invalid_token, expired_token, already_used, ...) throw ApiException; a wrong secret throws AuthenticationException.
$client->account->limits();
$client->account->usage();$client->asnDirectory->top(top: 20, by: 'addresses');
$client->asnDirectory->search('cloudflare', limit: 5);
$client->asnDirectory->get('AS15169');$client->crawlers->list();All exceptions extend IPScanner\Exception\IPScannerException.
| Exception | When |
|---|---|
ApiException |
Any non-2xx response. Has status, errorCode, details, body and headers. |
AuthenticationException |
401 or 403 |
NotFoundException |
404 |
RateLimitException |
429. Adds reason, retryAfter, resetAt, limit, usage, remaining, needed, plan, upgradeUrl and rateLimit (parsed rate limit headers). |
ConnectionException |
The request could not be completed. |
TimeoutException |
The request timed out. Extends ConnectionException. |
use IPScanner\Exception\RateLimitException;
try {
$client->ip->lookup('1.2.3.4');
} catch (RateLimitException $e) {
sleep($e->retryAfter ?? 60);
}GET requests are retried up to maxRetries times on connection errors and 502, 503 and 504 responses. POST requests and 429 responses are never retried.
$client = new Client(
apiKey: 'pk_live_...', // default: IPSCANNER_API_KEY
baseUrl: 'https://ipscanner.io', // default: IPSCANNER_API_URL, then https://ipscanner.io
timeout: 30, // seconds; bulk streams use 300
maxRetries: 2,
transport: null, // any IPScanner\Http\Transport
);Keyless endpoints (ip->demo, ip->myip, asnDirectory, crawlers, gate->verify) work without a key.
MIT