# Validate an IP address in PHP (filter_var, and when you need more)

_2026-09-21 · Boundstone (https://boundstone.io/blog/validate-ip-php)_


If you searched how to validate an IP address in PHP, the honest answer is that you almost certainly do not need a library, a package, or an API. PHP ships a built-in filter that checks IP format, and with a flag or two will also pin the version to IPv4 or IPv6 and reject private and reserved ranges — all in the standard library, no dependency, no network call. Most of the time that filter is the whole job. This post shows exactly how to use it, then draws a clear line at the point where a filter stops answering your question and a service starts.

## When "validate IP address in PHP" just means filter_var

The core tool is `filter_var` with the `FILTER_VALIDATE_IP` filter. It returns the IP string on success and `false` on failure, so a strict comparison against `false` is the correct check.

```php
<?php

function isValidIp(string $ip): bool
{
    return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}

var_dump(isValidIp('8.8.8.8'));                    // bool(true)
var_dump(isValidIp('2606:4700:4700::1111'));       // bool(true)
var_dump(isValidIp('999.1.1.1'));                  // bool(false)
var_dump(isValidIp('not-an-ip'));                  // bool(false)
```

That is the answer for "is this a syntactically valid IP address." No Composer install, no wrapper. If format is all you care about, you can stop reading here — the standard library has you covered.

## Pin the version: IPv4 or IPv6

By default `FILTER_VALIDATE_IP` accepts both families. When your schema expects one — an IPv4 column, an IPv6-only feature flag — pass a flag to pin the version. `FILTER_FLAG_IPV4` accepts only v4, `FILTER_FLAG_IPV6` only v6.

```php
<?php

// Accept IPv4 only
filter_var('10.0.0.5', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);   // "10.0.0.5"
filter_var('::1',      FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);   // false

// Accept IPv6 only
filter_var('::1',      FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);   // "::1"
filter_var('10.0.0.5', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);   // false
```

Pinning the version at the boundary means a v6 string never sneaks into a v4-shaped field, and you fail fast instead of storing something your downstream code can't parse.

## Reject private and reserved ranges natively

Here is the part people reach for a library to do, and shouldn't. `filter_var` will reject private and reserved ranges for you with two more flags: `FILTER_FLAG_NO_PRIV_RANGE` fails on private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and `fc00::/7`), and `FILTER_FLAG_NO_RES_RANGE` fails on reserved ranges (loopback, link-local, and similar). Combine them with a bitwise OR.

```php
<?php

function isPublicIp(string $ip): bool
{
    return filter_var(
        $ip,
        FILTER_VALIDATE_IP,
        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
    ) !== false;
}

var_dump(isPublicIp('8.8.8.8'));        // bool(true)  — public
var_dump(isPublicIp('192.168.1.1'));    // bool(false) — private
var_dump(isPublicIp('127.0.0.1'));      // bool(false) — reserved (loopback)
var_dump(isPublicIp('169.254.0.1'));    // bool(false) — reserved (link-local)
```

If you're accepting a client-supplied IP — a header like `X-Forwarded-For`, a webhook source, a form field — this one line filters out the addresses that should never appear as a public source. If the difference between those buckets is fuzzy to you, [what is a private IP address](/blog/what-is-a-private-ip) walks through the ranges and why they exist. For a value you just want to paste and check without writing any PHP, the keyless [IP validator tool](/tools/ip-validator) runs the same classification in the browser.

## What filter_var does not tell you

`filter_var` answers a boolean: valid or not, public or not. It does not tell you *where* the address is, *who* runs it, or *whether* it's a VPN, proxy, or Tor exit. That's the honest limit of format-and-range validation in any language — and it's worth stating plainly, because a "valid public IP" is not the same as a trustworthy one.

At Boundstone we don't paper over that gap. IP intelligence — geolocation, ASN, hosting/datacenter detection, proxy/VPN/Tor detection, reputation — needs licensed data, and it is **not shipped**. Every response says so in a machine-readable list rather than leaving you to guess.

## Classification over HTTP, as one system

So where does an API earn its place past the standard library? Not by re-doing `filter_var`'s job over the network — that would be slower for no gain. It earns it when you want the *classification itself* as structured data, returned by the same contract that validates your phone numbers and email addresses, so one integration covers all three. `POST /v1/verify/ip` returns `valid`, `version`, `normalized`, `classification`, `is_public`, and `is_bogon`.

```php
<?php

$ch = curl_init('https://api.boundstone.io/v1/verify/ip');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer bs_live_YOUR_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode(['ip' => '8.8.8.8']),
]);

$res = json_decode(curl_exec($ch), true);
curl_close($ch);

echo $res['classification'];              // "public"
var_dump($res['is_public']);              // bool(true)
var_dump($res['is_bogon']);              // bool(false)

// The honesty contract — read it, don't assume:
print_r($res['checks']['performed']);
// ["format", "version", "range_classification"]
print_r($res['checks']['not_performed']);
// ["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]
```

The `checks.not_performed` array is the point. It tells your code, every call, that a `classification` of `"public"` was reached by range math and nothing else — no geo, no VPN detection was consulted. That's what makes the "public" you get back trustworthy: you know precisely what it does and does not mean.

## The short version

- **Just validating format?** `filter_var($ip, FILTER_VALIDATE_IP)` — done, no dependency.
- **Need a specific family?** Add `FILTER_FLAG_IPV4` or `FILTER_FLAG_IPV6`.
- **Rejecting private/reserved source IPs?** Add `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE`. PHP does this natively — reach for it before any package.
- **Want public/private/bogon classification as structured data across phone, email, and IP in one contract?** `POST /v1/verify/ip`, and read `checks.not_performed`.
- **Geolocation, VPN, or proxy detection?** Not something `filter_var` can do — and at Boundstone it's honestly `not_performed` until licensed data ships.

Same story in another runtime? Here's [how to validate an IP in Node.js](/blog/validate-ip-nodejs). The [docs](/docs) cover the full response shape and the free tier: 250 credits a month, no card, credits never expire.
