Articles

A cross-engine CFML client for the ipLogs.com IP reputation API

Is this IP a VPN, a datacenter, or a real person?

iplogs - IP reputation & VPN detection for CFML
June 4, 2026

I keep needing the same thing in different projects: a quick way to ask "is this IP a VPN, a datacenter, or a real person" without signing up for yet another paid service. ipLogs.com does that, the API needs no key, and it publishes its data sets under CC-BY. So I wrote a CFML wrapper for it and put it on ForgeBox as iplogs.

It runs on Adobe ColdFusion 2016 through 2025, Lucee, and BoxLang from the same IPLogs.cfc.

What the API gives you

ipLogs scores an address and returns a verdict: clean, suspicious, vpn_likely, or vpn_detected, plus the ASN, the organization, the country, and the signals behind the score. There are four endpoints: a single check, a bulk-check for up to 100 IPs at a time, a health probe, and a VPN-provider snapshot.

The basics

ipLogs = new IPLogs();

result = ipLogs.check("23.234.89.127");
writeOutput(result.data.verdict);   // vpn_detected

Every method hands back the same envelope, so you never have to guess where the answer came from:

[
    "success":    true,
    "statusCode": 200,
    "source":     "api",        // api, cache, or offline
    "cached":     false,
    "fetchedAt":  "2026-05-30T18:00:00Z",
    "data":       { ... },
    "error":      ""
]

That source field turned out to be the most useful part. When you are debugging why a verdict looks off, knowing whether it came from the live API, a cached copy, or an offline data set saves a lot of time.

Caching

IP verdicts do not change minute to minute, so caching the answers is worth it. You pick the backend at construction:

ipLogs = new IPLogs(storage="cache", cacheTTL=86400);

cache uses the engine object cache through cachePut/cacheGet with a named region. server stashes results in the server scope behind a lock, which is handy when you have not configured an object cache. none turns it off. Only check and vpnProvider get cached; health and bulkCheck always hit the network.

One Lucee wrinkle: Lucee has no default object cache, so cacheGet throws unless you register one. The README shows the Application.cfc block for a RAM cache, and the library defaults its region to ipaCache on Lucee to match.

Working offline

This is the part I am happiest with. ipLogs publishes ten free data sets: Tor exit lists, Mullvad relays, datacenter ASNs, AWS and GCP ranges, Spamhaus DROP, FireHOL, and more. The library downloads them and matches an IP locally. If the API is unreachable or you are rate-limited, check quietly falls back to a local match and marks the result source="offline".

ipLogs.refreshDatasets();
env = ipLogs.localCheck("185.220.101.1");
writeOutput(env.data.verdict);   // vpn_detected, flagged "anonymizer"

The offline result keeps the same verdict values as the API and adds a flags array, so a Spamhaus hit (threat) reads differently from a plain datacenter range (datacenter), even though both come back as suspicious. You can retune the scoring per data set through fallbackMap without editing the component, and you can limit which data sets get consulted.

CIDR matching uses java.math.BigInteger, so IPv4 and IPv6 ranges work the same way on every engine. That sidesteps the usual mess of engine-specific IP helpers.

Cross-engine details

A few things kept it portable, and most of them I only found by running the tests on all four engines. Timestamps are built from year(), month(), and the other component functions instead of a dateTimeFormat mask, because BoxLang reads those masks by Java rules where mm means minutes. Cache reads rely on cacheGet returning null rather than cacheKeyExists. File timestamps come from java.io.File.lastModified(), since getFileInfo returns a date object on Lucee and an epoch elsewhere.

My favorite trap: val("Connection Failure") returns 0 on Adobe CF and Lucee, but 4 on BoxLang. My dataset downloader checked the HTTP status with val(), so on BoxLang a refused connection looked like a 2xx and the error body got written to disk as if it were real data. The fix was to match the status against ^2\d\d instead of trusting val(). I would never have caught that without a BoxLang run.

The library is verified on Lucee 5, Adobe CF2016, Adobe CF2023, and BoxLang. The TestBox suite runs on Lucee; there is also a small TestBox-free harness that exercises the library directly and passes on all four.

Get it

box install iplogs

Source is at https://github.com/JamoCA/cf-iplogs, and the ipLogs API itself is at https://github.com/DigitalDTech/iplogs-api. No key, no signup. The data sets are CC-BY 4.0, so keep the attribution if you pass them along.