This recipe shows the same task in two languages: a single asn_lookup(ip) / asnLookup(ip) function wrapping GET /api/v1/ip?ip=<addr>&include=network on the free ip.xakki.pro API, with clean error handling for an invalid IP, a rate-limited response, and a transport failure — copy whichever version matches your stack.
placeholder
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""asn-lookup.py -- a single asn_lookup(ip) function wrapping
GET /api/v1/ip?ip=<addr>&include=network, with clean error handling.
Shown alongside asn-lookup.js -- the same task in Python and JavaScript/Node,
the two languages this section's audience is most likely to already have
installed, so you can copy whichever one matches your stack without
translating the other by hand.
Zero dependencies -- stdlib only (urllib, json, ipaddress). Python 3.8+.
Usage as a library:
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("asn_lookup", "asn-lookup.py").load_module()
print(m.asn_lookup("8.8.8.8"))
Usage as a CLI:
python3 asn-lookup.py 8.8.8.8
"""
import ipaddress
import json
import sys
import urllib.error
import urllib.request
DEFAULT_BASE_URL = "https://ip.xakki.pro"
USER_AGENT = "myip-asn-lookup-recipe/1.0 (+https://ip.xakki.pro/tools/scripts/asn-lookup.html)"
class InvalidIPError(ValueError):
"""Raised when the input string is not a valid IPv4/IPv6 address."""
class RateLimitedError(RuntimeError):
"""Raised when the API's "network" group came back rate-limited
(soft-withheld -- see the rate-limit note in bulk-enrich.py) for THIS
lookup, so no ASN data is available right now."""
def asn_lookup(ip, base_url=DEFAULT_BASE_URL, timeout=15):
"""Returns a dict {asn, as_name, prefix, registry, allocated, country,
classification_type} for ip. Raises InvalidIPError for a malformed
input (checked BEFORE any request -- an invalid `ip` query param is
silently ignored by the API and answers about the caller's own IP
instead, which would silently mislabel the result), RateLimitedError
when the network group is withheld this call, or urllib.error.URLError
/ json.JSONDecodeError for a genuine transport/protocol failure."""
try:
ipaddress.ip_address(ip)
except ValueError:
raise InvalidIPError('"{}" is not a valid IPv4 or IPv6 address'.format(ip))
url = "{}/api/v1/ip?ip={}&include=network".format(base_url, ip)
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
if ipaddress.ip_address(data.get("ip", ip)) != ipaddress.ip_address(ip):
raise RuntimeError("unexpected response: ip mismatch (got {!r})".format(data.get("ip")))
if "network" in (data.get("limited") or []):
raise RateLimitedError("the network group was rate-limited for this call; retry later")
network = data.get("network") or {}
rdap = network.get("rdap") or {}
classification = network.get("classification") or {}
return {
"asn": network.get("asn"),
"as_name": network.get("asName"),
"prefix": network.get("prefix"),
"registry": network.get("registry"),
"allocated": network.get("allocated"),
"country": rdap.get("country"),
"classification_type": classification.get("type") or None,
}
def main():
if len(sys.argv) != 2:
sys.exit("usage: python3 asn-lookup.py <ip>")
ip = sys.argv[1]
try:
result = asn_lookup(ip)
except InvalidIPError as e:
sys.exit("invalid input: {}".format(e))
except RateLimitedError as e:
sys.exit("rate limited: {}".format(e))
except (urllib.error.URLError, json.JSONDecodeError) as e:
sys.exit("request failed: {}".format(e))
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
// SPDX-License-Identifier: MIT
// asn-lookup.js -- a single asnLookup(ip) function wrapping
// GET /api/v1/ip?ip=<addr>&include=network, with clean error handling.
//
// Shown alongside asn-lookup.py -- the same task in JavaScript/Node and
// Python, the two languages this section's audience is most likely to
// already have installed, so you can copy whichever one matches your stack
// without translating the other by hand.
//
// Zero dependencies -- only Node.js built-ins: global fetch (stable since
// Node 18, this repo's minimum target for client-side code across every
// TOOLS-0x recipe) and the `net` core module for IP validation. No
// npm install, no package.json.
//
// Usage as a module:
// const { asnLookup } = require('./asn-lookup.js');
// asnLookup('8.8.8.8').then(console.log);
//
// Usage as a CLI:
// node asn-lookup.js 8.8.8.8
'use strict';
const net = require('net');
const DEFAULT_BASE_URL = 'https://ip.xakki.pro';
const USER_AGENT = 'myip-asn-lookup-recipe/1.0 (+https://ip.xakki.pro/tools/scripts/asn-lookup.html)';
class InvalidIPError extends Error {}
class RateLimitedError extends Error {}
// asnLookup(ip, baseUrl?) -> Promise<{asn, asName, prefix, registry,
// allocated, country, classificationType}>. Rejects with InvalidIPError for
// a malformed input (checked BEFORE any request -- an invalid `ip` query
// param is silently ignored by the API and answers about the caller's own
// IP instead, which would silently mislabel the result), RateLimitedError
// when the network group is withheld this call, or a plain Error for a
// transport/HTTP failure.
async function asnLookup(ip, baseUrl) {
if (net.isIP(ip) === 0) {
throw new InvalidIPError('"' + ip + '" is not a valid IPv4 or IPv6 address');
}
const base = baseUrl || DEFAULT_BASE_URL;
const url = base + '/api/v1/ip?ip=' + encodeURIComponent(ip) + '&include=network';
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' } });
if (!response.ok) {
throw new Error('request failed: HTTP ' + response.status);
}
const data = await response.json();
// Guard against the API's own documented behavior: an invalid `ip` query
// param is silently IGNORED (not rejected), and the endpoint falls back
// to answering about the CALLER's own IP instead. We already validated
// ip above, so this should never fire -- but if it ever does, never
// attribute the wrong address's ASN to this call. A full semantic
// comparison (matching IPv4-mapped forms, zone ids, etc.) needs a real IP
// library; `net.isIP` has no equality helper, so this checks the
// cheaper, still-useful signal available from Node's core module alone:
// same address family AND same literal string as requested.
if (data.ip !== ip && net.isIP(data.ip || '') !== net.isIP(ip)) {
throw new Error('unexpected response: ip family mismatch (got ' + data.ip + ')');
}
const limited = data.limited || [];
if (limited.indexOf('network') !== -1) {
throw new RateLimitedError('the network group was rate-limited for this call; retry later');
}
const network = data.network || {};
const rdap = network.rdap || {};
const classification = network.classification || {};
return {
asn: network.asn || null,
asName: network.asName || null,
prefix: network.prefix || null,
registry: network.registry || null,
allocated: network.allocated || null,
country: rdap.country || null,
classificationType: classification.type || null,
};
}
async function runCLI(argv) {
if (argv.length !== 1) {
console.error('usage: node asn-lookup.js <ip>');
process.exitCode = 1;
return;
}
try {
const result = await asnLookup(argv[0]);
console.log(JSON.stringify(result, null, 2));
} catch (err) {
if (err instanceof InvalidIPError) {
console.error('invalid input: ' + err.message);
} else if (err instanceof RateLimitedError) {
console.error('rate limited: ' + err.message);
} else {
console.error('request failed: ' + err.message);
}
process.exitCode = 1;
}
}
if (require.main === module) {
runCLI(process.argv.slice(2));
}
module.exports = { asnLookup: asnLookup, InvalidIPError: InvalidIPError, RateLimitedError: RateLimitedError };
This script is MIT-licensed (SPDX-License-Identifier: MIT header at the top of the file) — copy, modify and reuse it freely in your own project, no attribution required.
Both versions raise/reject with a distinct, named error for each of three failure classes rather than one generic exception: InvalidIPError / InvalidIPError for a malformed input (checked locally BEFORE any request), RateLimitedError / RateLimitedError when the "network" group came back withheld for this call, and a plain error/URLError for a genuine transport or HTTP failure — a caller can catch exactly the case it cares about instead of string-matching a message.
One real gotcha worth knowing before you point this at production data: the API's own ?ip= query parameter does not reject a malformed address — if it fails to parse, the request silently falls back to answering about the CALLER's own IP instead of erroring. That is harmless for a single manual lookup, but in an unattended bulk script it would silently mislabel bad rows with the SCRIPT's own egress IP's data rather than skipping them. This script validates every IP locally first (Python's ipaddress module / Node's net.isIP) and separately double-checks that the "ip" field in the response actually matches what was requested, refusing to attribute a mismatched answer to the wrong row.
The Python version validates the returned "ip" field with an exact semantic comparison (ipaddress.ip_address equality, which correctly treats re-formatted-but-equal addresses as a match); the JS version, lacking an equivalent built-in in Node's core `net` module, checks the cheaper but still useful signal available without a dependency — that the response names an address of the SAME FAMILY as requested — and documents that difference in its own comment rather than silently claiming equal rigor.
GET /api/v1/ip never returns a hard 429 from the app itself — its enrichment groups (geo, network, security) are metered per unique target IP per UTC day, plus a tighter per-minute budget, and going over either soft-withholds the group instead: HTTP 200, "status":"partial", the group's name in "limited", and RateLimit-Limit/RateLimit-Remaining/RateLimit-Reset headers on the response. This script reads those signals rather than guessing, and records a withheld result explicitly (never as a blank/null, which would look identical to "the API has no data for this IP"). A real 429/503 can still happen at the edge — nginx's own flood guard, independent of the metering above — if you call the endpoint too fast from one machine; the script backs off exponentially on any 429/5xx and honors a Retry-After header when the server sends one. Anonymous callers get a modest daily/per-minute budget per group (see the RateLimit-Limit header on any response, or /api/v1/tiers); register a free API token for a much larger one if you plan to run this against more than a handful of IPs.
This script itself is the API example — there is no separate curl/response block to show, because the code above already IS the request. See the full response schema and every parameter in the API docs:
Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →