A CFML client for the ipLogs.com IP reputation and VPN-detection API. It runs the same way on Adobe ColdFusion 2016 through 2025, Lucee, and BoxLang. No signup and no API key are required. The library adds optional response caching and can fall back to locally downloaded data sets when the API is unreachable.
ipLogs.com scores an IP address and tells you whether it looks like a residential connection, a datacenter, or a VPN/proxy exit. This library wraps the four public endpoints:
check- score a single IPbulk-check- score up to 100 IPs per request (this client auto-chunks larger lists)health- service probevpn-provider- the aggregated VPN-provider snapshot
It also downloads the free CC-BY data sets ipLogs publishes (Tor exits, datacenter ASNs, Spamhaus DROP, and others) so you can match an IP offline.
With CommandBox:
box install iplogsOr drop IPLogs.cfc into your project and instantiate it directly.
ipLogs = new IPLogs();
result = ipLogs.check("8.8.8.8");
if (result.success) {
writeDump(result.data.verdict); // clean | suspicious | vpn_likely | vpn_detected
} else {
writeDump(result.error);
}Every method returns the same normalized envelope so you always know where the answer came from:
[
"success": true, // did the call succeed
"statusCode": 200,
"source": "api", // api | cache | offline
"cached": false,
"fetchedAt": "2026-05-30T18:00:00Z", // ISO-8601 UTC
"data": { ... }, // the API payload, or the offline verdict
"error": ""
]All init() arguments are optional and have sensible defaults.
| Argument | Default | Purpose |
|---|---|---|
apiBaseUrl |
https://iplogs.com/v1 |
API root |
dataBaseUrl |
https://iplogs.com/data |
data set download root |
storage |
cache |
cache, server, or none |
cacheRegion |
engine default (ipaCache on Lucee) |
object-cache region for cacheGet/cachePut |
cacheTTL |
86400 |
seconds to keep a successful result (24 hours) |
offlineFallback |
true |
fall back to local data sets when the API fails |
datasetDir |
<cfc dir>/datasets |
where downloaded data sets live |
datasetTTL |
86400 |
seconds before a data set is treated as stale |
fallbackDatasets |
all 10 | which data sets the offline check consults |
fallbackMap |
see below | per-data-set verdict, flag, score, and confidence |
httpTimeout |
{check:30, bulk:90, health:10, dataset:60} |
per-endpoint timeouts in seconds |
userAgent |
cf-iplogs/<version> (+github) |
request User-Agent |
throwOnError |
false |
throw on failure instead of returning a success=false envelope |
Example:
ipLogs = new IPLogs(
storage = "cache",
cacheTTL = 86400,
offlineFallback = true,
datasetDir = expandPath("/data/iplogs")
);Scores a single IP. Validates the address, checks the cache, calls the API, and falls back to a local match if the API is down and offlineFallback is on.
env = ipLogs.check("23.234.89.127");
writeOutput(env.data.verdict & " (score " & env.data.score & ")");Scores an array of IPs. Lists longer than 100 are split into multiple API calls and merged into a single data.results array. This method is not cached.
env = ipLogs.bulkCheck(["8.8.8.8", "1.1.1.1", "23.234.89.127"]);
for (row in env.data.results) {
writeOutput(row.ip_info.ip & ": " & row.verdict & "<br>");
}Probes the service. Not cached.
env = ipLogs.health();
writeOutput(env.success ? "up" : "down");Returns the aggregated VPN-provider snapshot. Cached. Falls back to the local vpn-providers data set when the API is unreachable.
env = ipLogs.vpnProvider();ipLogs.downloadDataset("tor-exits"); // fetch one data set to datasetDir
ipLogs.refreshDatasets(); // refresh every stale data set
ipLogs.listDatasets(); // [{name, present, sizeBytes, ageSeconds, stale}, ...]
ipLogs.localCheck("185.220.101.1"); // offline match, returns an envelope (source="offline")Set storage at construction:
cacheuses the engine object cache throughcachePut/cacheGetwith a named region. On Lucee there is no default object cache, so register one (see below). Adobe CF ships a default; BoxLang core may not, in which case a cache write is a quiet no-op and the next read is a miss.serverkeeps results in a struct underserver.cf_iplogs, guarded by a named lock. This needs no cache configuration, but it is per-JVM and clears on restart.nonedisables caching.
Only check and vpnProvider results are cached. health and bulkCheck always go to the network. A cache hit comes back with source="cache" and cached=true.
On Lucee, register a RAM cache in Application.cfc so storage="cache" has somewhere to write:
if (structKeyExists(server, "lucee")) {
this.cache.connections["ipaCache"] = [
"class": "lucee.runtime.cache.ram.RamCache",
"storage": false,
"custom": ["timeToLiveSeconds": 0, "timeToIdleSeconds": 0],
"default": ""
];
this.cache.object = "ipaCache";
}The library defaults cacheRegion to ipaCache on Lucee to match.
When a check fails and offlineFallback is on, the library matches the IP against the data sets in datasetDir and returns a degraded verdict marked source="offline". Stale data sets refresh on lookup; if a refresh fails, the existing copy on disk is used.
Downloads are lazy. A data set is only fetched the first time the offline path needs it (or when you ask for it explicitly), so datasetDir starts empty and stays empty for as long as the API keeps answering. That is expected, not a failure.
Two consequences worth planning for:
- The first offline check is the slow one.
localCheckconsults every set infallbackDatasets(all 10 by default), so that first call downloads each missing set, includingaws-ranges.jsonat roughly 2.4 MB (about 2.9 MB for the full set). After that, each set is cached on disk and reused until it passesdatasetTTL(24 hours). - To avoid paying that cost on a live request, pre-warm the cache once at startup or on a schedule:
// fetch every stale or missing set now (e.g. in Application.cfc onApplicationStart)
ipLogs.refreshDatasets();
// or fetch a single set
ipLogs.downloadDataset("tor-exits");
// or narrow the offline footprint so only a few sets are ever downloaded
ipLogs = new IPLogs(fallbackDatasets = ["tor-exits", "spamhaus-drop", "datacenter-asns"]);refreshDatasets() skips sets that are still fresh, so it is cheap to call on every startup. Pass force=true to re-download regardless of age.
The offline result keeps the API verdict values (clean, suspicious, vpn_likely, vpn_detected) and adds a flags array so you can tell categories apart. The default mapping:
| Category | Verdict | Flag | Score |
|---|---|---|---|
| Tor exit, Mullvad relay, residential-proxy backbone | vpn_detected |
anonymizer |
0.95 |
| Named VPN provider | vpn_detected |
vpn_provider |
0.90 |
| Spamhaus DROP, FireHOL Level 1 | suspicious |
threat |
0.85 |
| Datacenter ASN, AWS, GCP, Cloudflare | suspicious |
datacenter |
0.50 |
| No match | clean |
(none) | 0.10 |
The threat flag is kept separate from VPN and datacenter signals so a known abuser reads differently from plain hosting, even though both map to suspicious.
Tune it without touching the CFC:
// only consult two data sets, and re-score Tor as vpn_likely
ipLogs = new IPLogs(
fallbackDatasets = ["tor-exits", "spamhaus-drop"],
fallbackMap = [
"tor-exits": ["verdict":"vpn_likely","flag":"tor","score":0.42,"confidence":0.5,"match":"ip"],
"spamhaus-drop": ["verdict":"suspicious","flag":"threat","score":0.85,"confidence":0.85,"match":"cidr"]
]
);
// or override per call
ipLogs.localCheck("185.220.101.1", ["spamhaus-drop"]);CIDR matching uses java.math.BigInteger, so IPv4 and IPv6 ranges work the same way on every engine.
The data sets are downloaded from ipLogs.com and licensed CC-BY 4.0. Keep the attribution if you redistribute them.
Verified on Adobe ColdFusion 2016 and 2023, Lucee 5, and BoxLang 1.x, with the same source running on CF 2018/2021/2025 and Lucee 6. On BoxLang, install the bx-compat-cfml and bx-esapi modules. A few choices keep behavior consistent across engines:
- Timestamps are built from component date functions, not
dateTimeFormatmasks (BoxLang readsmmas minutes). - Cache reads use
cacheGetreturning null, notcacheKeyExists. - File mtimes come from
java.io.File.lastModified(), notgetFileInfo(which returns a date object on Lucee). - HTTP status is matched against
^2\d\drather thanval(statusCode), becauseval("Connection Failure")is 0 on ACF/Lucee but 4 on BoxLang.
There is no TestBox dependency. tests/tests.cfm is a self-contained harness that instantiates the library directly and runs every assertion, so it compiles and passes on all engines from Adobe CF 2016 through BoxLang.
Pick a CommandBox server file for the engine you want and start it:
box install
box server start server-lucee5.jsonEach engine has its own file on its own port:
| File | Engine | Port |
|---|---|---|
server-cf2016.json |
Adobe ColdFusion 2016 | 8810 |
server-cf2018.json |
Adobe ColdFusion 2018 | 8811 |
server-cf2021.json |
Adobe ColdFusion 2021 | 8812 |
server-cf2023.json |
Adobe ColdFusion 2023 | 8813 |
server-cf2025.json |
Adobe ColdFusion 2025 | 8814 |
server-lucee5.json |
Lucee 5 | 8815 |
server-lucee6.json |
Lucee 6 | 8816 |
server-lucee7.json |
Lucee 7 | 8817 |
server-boxlang.json |
BoxLang 1.x | 8818 |
Then open the test page in a browser for the HTML report:
http://localhost:<port>/tests/tests.cfm
Add ?format=json (or ?json=1) for the raw JSON envelope, handy for scripted runs:
curl "http://localhost:<port>/tests/tests.cfm?format=json"tests/demo.cfm exercises every method against the bundled fixtures and renders the envelopes.
MIT for the code (see LICENSE). The downloadable data sets are CC-BY 4.0 from ipLogs.com.
- ipLogs.com API: https://github.com/DigitalDTech/iplogs-api
- ipLogs.com tools and data: https://iplogs.com/tools