There are tools that claim to solve this problem, and many of them do, but they’ll charge you hundreds to thousands of dollars a month for it. For some teams that makes sense, but for most it’s overpriced for what you actually get.
You can build this yourself with no recurring cost, using a custom PHP endpoint and a free IP lookup API. The data isn’t perfect, but for B2B businesses targeting enterprise and government clients it’s genuinely useful intel that costs you nothing to run once it’s up.
How It Works
Every IP address on the internet is registered to an organization. That could be Comcast, a corporate network, or a government agency. By doing a reverse lookup on a visitor’s IP, you can determine what company or ISP owns it.
The implementation uses two lookup methods, fastest first:
ASN Lookup via ipapi.is checks who owns the IP block and usually returns a company name directly. It’s fast and the free tier is more than enough for most traffic volumes.
RDAP Lookup via ARIN is the fallback. It queries the official internet registry records directly. Slower, but no third-party dependency.
Results get cached for 90 days so repeat visitors skip the external lookups entirely. The company name gets pushed into your GTM data layer, flows into GA4, and lands in BigQuery for querying.
The Honest Limitation
This won’t identify small businesses on a standard Comcast or Verizon Business account. You’ll see the ISP name, not the company. If your target audience is small businesses using residential or basic business internet, this has real gaps.
If you’re selling to enterprise, healthcare systems, financial institutions, government agencies, or large organizations running their own network infrastructure, it works well. Those organizations register their IP ranges under their company name. That’s what gets surfaced.
The sample data at the bottom of this post is real tracked output from active client implementations. You’ll see ISP noise and some technical garbage mixed in, but you’ll also see JPMorgan Chase, McKinsey, U.S. House of Representatives, Blue Origin, and Davidson Kempner Capital Management. The signal is there.
The Implementation
This is a technical approach. It’s not a drop-in solution, and you’ll want a developer to integrate it properly into your stack.
Server-Side PHP/WordPress
Here’s the server-side PHP/WordPress code that makes it happen. It creates a POST endpoint at /api/company/ and handles IP detection, lookup, and caching.
/**
* Identify Visitor Company
*
* Reverse DNS lookup with rdap to determine the company associated
* with a website visitors network connection.
*/
class Company_Identify_API {
public function __construct() {
add_action('init', [$this, 'handle_company_request']);
}
/**
* Handle the company RDAP request
*/
public function handle_company_request() {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path !== '/api/company/') {
return;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Content-Type: application/json');
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$cf_ip = isset($_SERVER['HTTP_CF_CONNECTING_IP']) ? $_SERVER['HTTP_CF_CONNECTING_IP'] : null;
if ($cf_ip) {
$ips = array_unique(array_map('trim', explode(',', $cf_ip)));
$ip = $ips[0];
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_unique(array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])));
$ip = $ips[0];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['error' => "Invalid IP address: $ip"]);
exit;
}
$company = $this->get_company($ip);
http_response_code(200);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'company' => $company,
]);
exit;
}
private function get_company($ip) {
$cache_key = 'company_id_' . md5($ip);
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$company = $this->lookup_asn($ip)
?? $this->lookup_rdap($ip)
?? 'Unknown Organization';
set_transient($cache_key, $company, DAY_IN_SECONDS * 90);
return $company;
}
/**
* ASN lookup via ipapi.is
*/
private function lookup_asn($ip) {
$response = wp_remote_get("https://api.ipapi.is/?ip={$ip}", [
'timeout' => 3,
]);
if (is_wp_error($response)) {
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
return $data['asn']['org'] ?? $data['company']['name'] ?? null;
}
/**
* ARIN RDAP lookup
*/
private function lookup_rdap($ip) {
$response = wp_remote_get("https://rdap.arin.net/registry/ip/{$ip}", [
'headers' => ['Accept' => 'application/json'],
'timeout' => 5,
]);
if (is_wp_error($response)) {
return null;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($data['entities'])) {
return null;
}
$fn = null;
foreach ($data['entities'] as $entity) {
if (empty($entity['vcardArray'][1])) {
continue;
}
foreach ($entity['vcardArray'][1] as $vcard) {
if ($vcard[0] === 'org') {
return $vcard[3];
}
if ($vcard[0] === 'fn') {
$fn = $vcard[3];
}
}
}
return $fn ?? $data['name'] ?? null;
}
}
new Company_Identify_API();
The endpoint checks for Cloudflare’s HTTP_CF_CONNECTING_IP header first so you get the real visitor IP instead of a Cloudflare edge server IP. It falls back to X-Forwarded-For then REMOTE_ADDR. The exit at the end kills the WordPress process immediately after the response, no point loading the rest of the stack for an API call.
GTM / GA4 JavaScript
This runs on every page. It checks localStorage first so returning visitors don’t trigger another API call. On a cache hit it fires immediately, no network request needed.
/**
* Company Tracking in GA4 / GTM / BigQuery
*/
window.dataLayer = window.dataLayer || [];
(function ($) {
class NetworkCompanyIdentification {
constructor() {
this.identifyNetworkCompany();
}
identifyNetworkCompany() {
const storageKey = "network_company_set";
const cachedCompany = localStorage.getItem(storageKey);
const pushToDataLayer = (companyName) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "network_company_set",
network_company: companyName,
});
};
if (cachedCompany) {
pushToDataLayer(cachedCompany);
return;
}
fetch("/api/company/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
const companyName = data.company || "Unknown Organization";
localStorage.setItem(storageKey, companyName);
pushToDataLayer(companyName);
})
.catch((error) => {
console.error("Error: Could not fetch company data", error);
pushToDataLayer("Unknown Organization");
});
}
}
new NetworkCompanyIdentification();
})(jQuery);
In GTM, create a Data Layer Variable for network_company and attach it as a custom dimension on your GA4 page_view event. From there it flows into BigQuery automatically if you have the GA4 BigQuery export enabled.
Querying in BigQuery
SELECT
event_date AS tracked_on,
user_pseudo_id,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_path,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'network_company') AS network_company
FROM `your-project.analytics_XXXXXXXXX.events_2026*`
WHERE event_name = 'page_view'
ORDER BY tracked_on DESC
Swap in your BigQuery project ID and GA4 property dataset. The events_2026* wildcard pulls all tables for the year.
What You Actually Get
Here’s a real sample of tracked values from active client sites. Plenty of noise in there, ISP names, technical contact handles, timeout errors from slow RDAP responses. But look past that and the signal is obvious:
- JPMorgan Chase & Co.
- McKinsey & Company, Inc.
- UBS AG
- Raymond James Financial, Inc.
- The Bank of New York Mellon Corporation
- FMR LLC
- Microsoft Corporation
- Davidson Kempner Capital Management LP
- Viking Global Investors LP
- Barclays Capital
- Blue Origin Enterprises, L.P.
- Palo Alto Networks, Inc.
- HubSpot
- U.S. House of Representatives
- U.S. Department of Justice
- United States Senate
- Princeton University
- Cornell University
- Indiana University
These are real organizations with real procurement cycles. Knowing they’re on your site, and what they’re reading, is intel most of your competitors don’t have. Once it’s running there’s no ongoing cost, no third-party dependency beyond the free ipapi.is tier, and no ongoing maintenance. The 90-day server-side cache combined with localStorage on the client means the lookup only runs once per IP per quarter.