# PHP SDK

> Official IPScope server-side client for PHP 8.2+ (PHP-FPM, FrankenPHP, RoadRunner & Swoole) (ipscope/ipscope-php).

> **Status: stable.** Published as `ipscope/ipscope-php` for PHP 8.2+ (PHP-FPM, FrankenPHP, RoadRunner & Swoole). Strictly scoped to IP intelligence operations with built-in bounded LRU caching and automatic retry backoff.

## Install

```bash Install
composer require ipscope/ipscope-php
```

## Quickstart: single, batch & prefix lookups

Pass your API key explicitly or set `IPSCOPE_KEY` in your environment. Every lookup returns the canonical [response schema](https://ipscope.io/docs/getting-started/response-format) with `geo`, `network`, `connection`, `security` and `security_extensions`.

```php PHP
<?php

declare(strict_types=1);

use IpScope\Sdk\IpScopeClient;
use IpScope\Sdk\IpScopeSecurity;

// Optional: inspect runtime patch status for CVE-2026-109618 / CVE-2026-109619
$advisory = IpScopeSecurity::getRuntimeAdvisoryStatus();

$client = new IpScopeClient(apiKey: getenv('IPSCOPE_KEY') ?: '');

// 1. Single IPv4 or IPv6 lookup (cached automatically in bounded LRU cache)
$ipInfo = $client->lookupIp('198.51.100.7');
echo $ipInfo->ip . ' ' . $ipInfo->geo->countryCode . ' AS' . $ipInfo->network->asn . PHP_EOL;
echo 'VPN: ' . ($ipInfo->security->isVpn ? 'true' : 'false') . PHP_EOL;

// 2. Batch lookup (up to 100 IPs; deduplicates & reuses cached entries)
$batch = $client->lookupBatch(['198.51.100.7', '1.1.1.1', '2606:4700:4700::1111']);
foreach ($batch->results as $record) {
    echo $record->ip . ' -> ' . $record->network->org . PHP_EOL;
}

// 3. Memory-bounded generator streaming for 10,000+ IPs (constant O(chunk) RAM)
foreach ($client->lookupBatchStream($largeIpList) as $chunk) {
    foreach ($chunk->results as $record) {
        // Process each record with < 400 KB steady-state memory overhead
    }
}

// 4. Network CIDR prefix lookup
$prefix = $client->lookupNetwork('198.51.100.0/24');
echo $prefix->queriedCidr . ' -> AS' . $prefix->record?->asn . ' (' . $prefix->record?->organization . ')' . PHP_EOL;
```

### 200 `IpLookupResponse`: 1. Single IPv4/IPv6 lookup response (GET /v1/ip/{ip})

- `ip` (string, required): Canonical normalized representation of the queried IPv4 or IPv6 address.
- `ip_version` (integer, required): Protocol family (`4` for IPv4, `6` for IPv6).
- `matched_network` (string, optional, nullable): Longest-matching routed CIDR prefix in the active Radix/LPM snapshot.
- `geo` (GeoLocation, optional): Country and city geolocation record (`geo_country` and `geo_city` families).
  - `country_code` (string, optional, nullable): Effective ISO 3166-1 alpha-2 country code (`"US"`, `"DE"`, `"TR"`).
  - `declared_country_code` (string, optional, nullable): Country code registered in RIR WHOIS/RDAP allocation records or RFC 8805 geofeed.
  - `physical_country_code` (string, optional, nullable): Country code where active BGP routing and latency triangulation place the infrastructure.
  - `is_virtual_location` (boolean, required): `true` when `declared_country_code` differs from `physical_country_code` (cross-border routed or geofeed-relocated prefix).
  - `location_type` (string, required): How the effective location was resolved by the lookup engine.
  - `confidence` (number, optional, nullable): Calibrated geolocation confidence score.
  - `subdivision` (string, optional, nullable): First-level administrative region or state. Included when `geo_city` is licensed.
  - `city` (string, optional, nullable): Resolved city name. Included when `geo_city` is licensed.
  - `accuracy_radius_km` (integer, optional, nullable): Estimated accuracy radius in kilometres around the city centroid. Included when `geo_city` is licensed.
  - `timezone` (string, optional, nullable): IANA time zone identifier. Included when `geo_city` is licensed.
- `network` (NetworkInfo, optional): BGP origin autonomous system metadata (`network` family).
  - `asn` (integer, optional, nullable): Origin Autonomous System Number without the `AS` prefix.
  - `organization` (string, optional, nullable): Canonical autonomous system or operator organization name.
- `connection` (ConnectionInfo, optional): Access network classification (`connection` and `hosting` families).
  - `connection_type` (string, required): Primary access network classification.
  - `is_residential` (boolean, required): `true` for consumer FTTH, cable, and DSL broadband ISPs.
  - `is_mobile` (boolean, required): `true` for cellular carrier gateways (4G/5G CGNAT pools).
  - `is_business` (boolean, required): `true` for enterprise leased lines and corporate autonomous systems.
  - `is_hosting` (boolean, required): `true` for cloud providers, colocation facilities, and VPS networks.
- `security` (SecurityInfo, optional): VPN, proxy, Tor, and hosting anonymizer classification (`anonymity` and `hosting` families).
  - `is_vpn` (boolean, required): `true` when the IP is an active commercial or self-hosted VPN exit node.
  - `is_hosting` (boolean, required): `true` when routed by a datacenter, cloud, or hosting ASN.
  - `is_tor_exit` (boolean, required): `true` when listed in the active Tor Project exit consensus.
  - `is_proxy` (boolean, required): `true` for open HTTP/SOCKS proxies and residential proxy relays.
  - `anonymizer_type` (string, required): Primary anonymizer classification.
  - `confidence` (number, optional, nullable): Calibrated anonymity confidence score.
  - `reason_codes` (string[], required): Deterministic evidence codes explaining why the IP was flagged.
- `security_extensions` (SecurityExtensions, optional): Extended threat telemetry and scanner recency (`security_extensions` family).
  - `abuse_velocity_score` (number, optional, nullable): Normalized abuse velocity score from active honeypot and darknet observations.
  - `is_known_scanner` (boolean, optional, nullable): `true` when the IP has been observed performing internet-wide port or vulnerability scanning.
  - `observation_recency_hours` (integer, optional, nullable): Hours elapsed since the most recent active security or anonymizer observation.
- `dataset_version` (string, required): Version identifier of the compiled IPScope dataset snapshot serving the response.
- `updated_at` (string, optional, nullable): ISO 8601 UTC timestamp when the active dataset snapshot was published.

```json 200 IpLookupResponse
{
  "ip": "198.51.100.7",
  "ip_version": 4,
  "matched_network": "198.51.100.0/24",
  "geo": {
    "country_code": "US",
    "declared_country_code": "US",
    "physical_country_code": "US",
    "is_virtual_location": false,
    "location_type": "physical",
    "confidence": 0.94,
    "subdivision": "Virginia",
    "city": "Ashburn",
    "postal_code": "20147",
    "latitude": 39.0438,
    "longitude": -77.4874,
    "accuracy_radius_km": 10,
    "timezone": "America/New_York"
  },
  "network": {
    "asn": 64501,
    "organization": "Example Cloud LLC"
  },
  "connection": {
    "connection_type": "hosting",
    "is_hosting": true,
    "is_residential": false,
    "is_mobile": false,
    "is_business": false
  },
  "security": {
    "is_vpn": true,
    "is_hosting": true,
    "is_tor_exit": false,
    "is_proxy": false,
    "anonymizer_type": "vpn",
    "confidence": 0.96,
    "reason_codes": [
      "vpn_handshake_confirmed",
      "datacenter_asn_range"
    ]
  },
  "security_extensions": {
    "abuse_velocity_score": 18,
    "is_known_scanner": false,
    "observation_recency_hours": 12,
    "anonymizer_reason": "vpn_handshake_confirmed",
    "threat_level": "medium",
    "abuse_velocity": "low",
    "confidence": 0.96
  },
  "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
  "updated_at": "2026-10-27T00:00:00Z"
}
```

### 200 `BatchLookupResponse`: 2 & 3. Batch & streaming lookup response (POST /v1/ip/batch)

- `dataset_version` (string, required): Snapshot version that served all lookups in this batch.
- `count` (integer, required): Total number of resolved IP records returned in `results`.
- `results` (IpLookupResponse[], required): Ordered array of `IpLookupResponse` objects projected to your API key’s `licensedFamilies`.
  - `ip` (string, required): Canonical normalized representation of the queried IPv4 or IPv6 address.
  - `ip_version` (integer, required): Protocol family (`4` for IPv4, `6` for IPv6).
  - `matched_network` (string, optional, nullable): Longest-matching routed CIDR prefix in the active Radix/LPM snapshot.
  - `geo` (GeoLocation, optional): Country and city geolocation record (`geo_country` and `geo_city` families).
    - `country_code` (string, optional, nullable): Effective ISO 3166-1 alpha-2 country code (`"US"`, `"DE"`, `"TR"`).
    - `declared_country_code` (string, optional, nullable): Country code registered in RIR WHOIS/RDAP allocation records or RFC 8805 geofeed.
    - `physical_country_code` (string, optional, nullable): Country code where active BGP routing and latency triangulation place the infrastructure.
    - `is_virtual_location` (boolean, required): `true` when `declared_country_code` differs from `physical_country_code` (cross-border routed or geofeed-relocated prefix).
    - `location_type` (string, required): How the effective location was resolved by the lookup engine.
    - `confidence` (number, optional, nullable): Calibrated geolocation confidence score.
    - `subdivision` (string, optional, nullable): First-level administrative region or state. Included when `geo_city` is licensed.
    - `city` (string, optional, nullable): Resolved city name. Included when `geo_city` is licensed.
    - `accuracy_radius_km` (integer, optional, nullable): Estimated accuracy radius in kilometres around the city centroid. Included when `geo_city` is licensed.
    - `timezone` (string, optional, nullable): IANA time zone identifier. Included when `geo_city` is licensed.
  - `network` (NetworkInfo, optional): BGP origin autonomous system metadata (`network` family).
    - `asn` (integer, optional, nullable): Origin Autonomous System Number without the `AS` prefix.
    - `organization` (string, optional, nullable): Canonical autonomous system or operator organization name.
  - `connection` (ConnectionInfo, optional): Access network classification (`connection` and `hosting` families).
    - `connection_type` (string, required): Primary access network classification.
    - `is_residential` (boolean, required): `true` for consumer FTTH, cable, and DSL broadband ISPs.
    - `is_mobile` (boolean, required): `true` for cellular carrier gateways (4G/5G CGNAT pools).
    - `is_business` (boolean, required): `true` for enterprise leased lines and corporate autonomous systems.
    - `is_hosting` (boolean, required): `true` for cloud providers, colocation facilities, and VPS networks.
  - `security` (SecurityInfo, optional): VPN, proxy, Tor, and hosting anonymizer classification (`anonymity` and `hosting` families).
    - `is_vpn` (boolean, required): `true` when the IP is an active commercial or self-hosted VPN exit node.
    - `is_hosting` (boolean, required): `true` when routed by a datacenter, cloud, or hosting ASN.
    - `is_tor_exit` (boolean, required): `true` when listed in the active Tor Project exit consensus.
    - `is_proxy` (boolean, required): `true` for open HTTP/SOCKS proxies and residential proxy relays.
    - `anonymizer_type` (string, required): Primary anonymizer classification.
    - `confidence` (number, optional, nullable): Calibrated anonymity confidence score.
    - `reason_codes` (string[], required): Deterministic evidence codes explaining why the IP was flagged.
  - `security_extensions` (SecurityExtensions, optional): Extended threat telemetry and scanner recency (`security_extensions` family).
    - `abuse_velocity_score` (number, optional, nullable): Normalized abuse velocity score from active honeypot and darknet observations.
    - `is_known_scanner` (boolean, optional, nullable): `true` when the IP has been observed performing internet-wide port or vulnerability scanning.
    - `observation_recency_hours` (integer, optional, nullable): Hours elapsed since the most recent active security or anonymizer observation.
  - `dataset_version` (string, required): Version identifier of the compiled IPScope dataset snapshot serving the response.
  - `updated_at` (string, optional, nullable): ISO 8601 UTC timestamp when the active dataset snapshot was published.

```json 200 BatchLookupResponse
{
  "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
  "count": 3,
  "results": [
    {
      "ip": "198.51.100.7",
      "ip_version": 4,
      "matched_network": "198.51.100.0/24",
      "geo": {
        "country_code": "US",
        "declared_country_code": "US",
        "physical_country_code": "US",
        "is_virtual_location": false,
        "location_type": "physical",
        "confidence": 0.94,
        "subdivision": "Virginia",
        "city": "Ashburn",
        "postal_code": "20147",
        "latitude": 39.0438,
        "longitude": -77.4874,
        "accuracy_radius_km": 10,
        "timezone": "America/New_York"
      },
      "network": {
        "asn": 64501,
        "organization": "Example Cloud LLC"
      },
      "connection": {
        "connection_type": "hosting",
        "is_hosting": true,
        "is_residential": false,
        "is_mobile": false,
        "is_business": false
      },
      "security": {
        "is_vpn": true,
        "is_hosting": true,
        "is_tor_exit": false,
        "is_proxy": false,
        "anonymizer_type": "vpn",
        "confidence": 0.96,
        "reason_codes": [
          "vpn_handshake_confirmed",
          "datacenter_asn_range"
        ]
      },
      "security_extensions": {
        "abuse_velocity_score": 18,
        "is_known_scanner": false,
        "observation_recency_hours": 12,
        "anonymizer_reason": "vpn_handshake_confirmed",
        "threat_level": "medium",
        "abuse_velocity": "low",
        "confidence": 0.96
      },
      "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
      "updated_at": "2026-10-27T00:00:00Z"
    },
    {
      "ip": "203.0.113.42",
      "ip_version": 4,
      "matched_network": "203.0.113.0/24",
      "geo": {
        "country_code": "DE",
        "declared_country_code": "DE",
        "physical_country_code": "DE",
        "is_virtual_location": false,
        "location_type": "physical",
        "confidence": 0.92,
        "subdivision": "Hesse",
        "city": "Frankfurt am Main",
        "accuracy_radius_km": 10,
        "timezone": "Europe/Berlin"
      },
      "network": {
        "asn": 24940,
        "organization": "Hetzner Online GmbH"
      },
      "connection": {
        "connection_type": "hosting",
        "is_residential": false,
        "is_mobile": false,
        "is_business": false,
        "is_hosting": true
      },
      "security": {
        "is_vpn": false,
        "is_hosting": true,
        "is_tor_exit": false,
        "is_proxy": false,
        "anonymizer_type": "none",
        "confidence": 0.91,
        "reason_codes": [
          "datacenter_asn_range"
        ]
      },
      "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
      "updated_at": "2026-10-27T00:00:00Z"
    }
  ]
}
```

### 200 `NetworkPrefixLookupResponse`: 4. Network CIDR prefix lookup response (GET /v1/networks/{prefix}/{length})

- `queried_cidr` (string, required): Normalized canonical CIDR block.
- `family` (integer, required): IP address family (`4` or `6`).
- `dataset_version` (string, required): Active dataset snapshot version.
- `record` (NetworkPrefixRecord, required): Matched prefix record in the routing snapshot.
  - `prefix` (string, required): Matched CIDR prefix in the routing table.
  - `family` (integer, required): Address family (`4` or `6`).
  - `asn` (integer, optional, nullable): Origin Autonomous System Number.
  - `organization` (string, optional, nullable): Registered autonomous system organization name.
  - `countryCode` (string, optional, nullable): ISO 3166-1 alpha-2 country code.
  - `city` (string, optional, nullable): Resolved city name.
  - `locationType` (string, required): Location resolution classification.
  - `isVpn` (boolean, required): Whether the prefix is classified as a VPN exit range.
  - `isHosting` (boolean, required): Whether the prefix belongs to a hosting/cloud ASN.
  - `isTorExit` (boolean, required): Whether the prefix contains active Tor exit nodes.
  - `isProxy` (boolean, required): Whether the prefix contains active proxy relays.
  - `anonymizerType` (string, required): Primary anonymizer classification (`"vpn"`, `"none"`, etc.).

```json 200 NetworkPrefixLookupResponse
{
  "queried_cidr": "198.51.100.0/24",
  "family": 4,
  "dataset_version": "2026-10-27T00:00:00Z.crawl-1793088000000",
  "record": {
    "prefix": "198.51.100.0/24",
    "family": 4,
    "asn": 64501,
    "organization": "Example Cloud LLC",
    "countryCode": "US",
    "city": "Ashburn",
    "locationType": "physical",
    "isVpn": true,
    "isHosting": true,
    "isTorExit": false,
    "isProxy": false,
    "anonymizerType": "vpn"
  }
}
```

## Architecture & runtime guarantees

- Hardened against PHP Zend Engine OOM & persistent-worker vulnerabilities (`CVE-2026-109618` & `CVE-2026-109619`) via pre-OOM heap headroom guards (`IpScopeSecurity::assertMemoryHeadroom`).
- Streaming `CURLOPT_WRITEFUNCTION` inspection aborts oversized responses before buffering, with bounded JSON recursion (`max_depth = 16`).
- Zero static or global state across persistent worker requests (FrankenPHP, RoadRunner, Swoole, PHP-FPM).

## Reading local MMDB files

If you subscribe to a [downloadable database](https://ipscope.io/docs/databases), our `.mmdb` snapshots open directly in standard MaxMind DB readers with sub-microsecond in-memory lookups:

```php PHP (MMDB)
<?php
use MaxMind\Db\Reader;

$reader = new Reader('/var/lib/ipscope/ipscope-complete.mmdb');
$record = $reader->get('198.51.100.7');
$reader->close();
```

## Plain HTTP fallback

Need to call the [REST API](https://ipscope.io/docs/api-reference) without installing a package? Use PHP’s standard HTTP client directly:

```php PHP (HTTP)
<?php
$ch = curl_init('https://api.ipscope.io/v1/ip/198.51.100.7');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('IPSCOPE_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
]);
$info = json_decode((string) curl_exec($ch), true, 16, JSON_THROW_ON_ERROR);
```
