ASN Lookup Function (Python + JavaScript)

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.

How to run this script

placeholder

  1. Download whichever file matches your stack: asn-lookup.py or asn-lookup.js (both shown below).
  2. Run: python3 asn-lookup.py 8.8.8.8 —or— node asn-lookup.js 8.8.8.8
  3. The CLI prints the ASN, AS name, prefix, registry, allocation date and country as JSON.
  4. Or import the function into your own script/module — both files export it (module.exports).

Full source code — both languages

asn-lookup.py Download asn-lookup.py
#!/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()
asn-lookup.js Download asn-lookup.js
// 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.

ASN Lookup Function (Python + JavaScript)

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.

A note on rate limits

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.

Glossary

ASN
Autonomous System Number — the unique number identifying a network operator's routing domain on the public internet, the core fact this function looks up.
rdap.country
The registrant country from the IP's RDAP (WHOIS successor) record — part of network.rdap in the API's response, surfaced by this function as "country".
classification.type
The API's coarse network-usage category for the IP (e.g. "hosting", "residential", "business") — baseline data, present on every response regardless of ?include=.
Named error types
Distinct exception/error classes (InvalidIPError, RateLimitedError) instead of one generic exception — lets a caller catch and handle each failure mode differently.
net.isIP()
Node's core `net` module's built-in address-family checker (returns 0/4/6) — used here instead of a hand-written regex or an npm validator package.
UMD-lite CLI/module dual use
Both files work as a standalone command-line script AND as an importable module (Python's `if __name__ == "__main__"`, Node's `require.main === module`) from the same file, with no separate build step.

API equivalent

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:

Read the full API docs

Frequently asked questions

Why show both a Python and a JavaScript version of the exact same function?
This section's audience is developers, and Python and Node are the two runtimes most likely to already be installed without any setup — showing both means you can copy the one that matches your project without translating the logic yourself.
Does asn_lookup() cache results or make repeat calls free?
The function itself does no client-side caching — but the API's OWN metering already makes a repeat lookup of the same IP the same UTC day free, so calling this function twice for the same address in one run costs nothing extra server-side.
What does the JS version do differently from the Python one for correctness?
Only in the strictness of the response ip-match guard, documented above — Python's ipaddress module allows an exact semantic comparison, Node's core net module doesn't, so the JS version checks address family instead and says so in its own comment.
Can I use this function for a private/non-routable IP?
You can call it, but expect mostly null fields back — a private-range address has no ASN, no RDAP record and no meaningful network classification in any public registry.
Is a Bearer token required to call this function?
No — both versions call the API anonymously by default; pass your own fetch/urllib headers if you want to add one for a higher rate-limit tier.

Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →