activedns
Python client for the ActiveDNS API: DNS search by domain, address, network and AS number. Python 3.10 or later.
pip install activedns
from activedns import Client
# name your program: it is sent in the User-Agent of every request
with Client("mytool/1.0") as client:
page = client.query("*.example.com")
for record in page.records:
print(record.domain, record.ip_address, record.observed)
A query is a domain (example.com), a wildcard (*.example.com), an address, a network (192.0.2.0/24) or
an AS number (AS13335). query_combined matches several at once.
query returns one page of the result and tells you whether there is more; see Paging.
It works without an account or a key. AS-number and combined searches, wider networks, complete answers and higher rates come with a token of your own.
Paging
A query returns one page: up to 100 records, or as many as page_size asks for. The page tells you whether
that was everything:
| attribute | meaning |
|---|---|
page.count |
how many records match in all (count_estimated: an estimate; count_capped: at least that many) |
page.has_more |
there are records after this page |
len(page.records) |
how many you got |
The client never fetches further pages by itself. Each one is a request against your rate limit, so how much of
a large result to retrieve is your decision, made with next_page:
page = client.query("*.example.com")
fetched = 0
while page is not None:
use(page.records)
fetched += len(page.records)
if fetched >= 500: # enough for this job
break
page = client.next_page(page)
next_page repeats the query from where the page ended, and returns None after the last page. Past the depth
your token may page to, it raises ForbiddenError.
Your own token
The SDK works the moment you install it, because it carries a token of its own. That token is shared by everyone who uses the SDK, so it is deliberately modest. A token issued to you opens up considerably more of the database, and using one is a single argument:
client = Client("mytool/1.0", token=os.environ["ACTIVEDNS_TOKEN"])
Nothing else in your code changes: the same query, query_combined and next_page, with more allowed.
What a token of your own gives you
| built-in token | your own token | |
|---|---|---|
| Search by AS number | no | yes: everything observed in a network operator's address space |
| Combined searches | no | yes: domain and network and AS number in one question |
| Widest network | /24 (256 addresses), IPv6 /48 | /16 (65,536 addresses) and wider |
| Records per request | 100 | 1,000 |
| How far into an answer | the first 200 records | 10,000 records and beyond |
| Request rate | 30 a minute, 60 at once | 120 a minute, 240 at once, and higher |
| What the rate counts | your network address, and the network around it | the token: the same allowance from a laptop, a CI job or a fleet |
| Place in the queue | after everyone else | ahead of all shared-token traffic |
The right-hand column is where tokens start, not where they end: every token has its own limits on the server, set for what its holder is building. The numbers are those of October 2026.
What that makes possible
Map an organisation by its AS number. Every name seen resolving into a network operator's address space, without knowing a single domain beforehand:
page = client.query("AS64496")
Ask precise questions. What does this company host at that provider? Which names under a domain sit in one particular network? A combined search answers in one request what would otherwise be thousands of records to download and filter yourself:
page = client.query_combined(domain="*.example.com", asn=64496)
page = client.query_combined(domain="*.example.com", ip="192.0.2.0/24")
Sweep whole networks. A /16 in one search instead of 256 separate /24s:
page = client.query("198.51.0.0/16")
Get the whole answer. The built-in token shows the first 200 records of a search, which is enough to look
around. Large zones, hosting ranges and CDNs have tens of thousands; with your own token next_page keeps
going, 1,000 records at a time if you ask for pages that size:
# every name under a large zone, not only the first 200 records
names = set()
page = client.query("*.example.com", page_size=1000)
while page is not None:
names.update(record.domain for record in page.records)
page = client.next_page(page)
print(len(names), "names")
With the built-in token the same loop ends after 200 records with a ForbiddenError:
403: cursor beyond your token's limit of 100.
Run it where your work runs. The built-in token's allowance belongs to a network address, so an office, a cloud region or a CI provider's address range shares it with whoever else is there. Your own token's allowance is yours wherever it is used: pipelines, scheduled jobs and several machines at once. Keep the token out of the code and hand it to the job as a secret:
# GitHub Actions
- run: python -m asset_monitor
env:
ACTIVEDNS_TOKEN: ${{ secrets.ACTIVEDNS_TOKEN }}
Stay fast when the service is busy. Searches are served by priority. The website comes first, issued tokens next, and the tokens shared by tools and SDKs last.
When you have outgrown the built-in token
The API tells you. A search the built-in token may not make raises a ForbiddenError that says what was
missing:
403: query type asn is not allowed for your token
403: combined queries (domain AND ip AND asn) are not allowed for your token
403: IPv4 network too wide for your token: prefix must be /24 or longer
403: cursor beyond your token's limit of 100; narrow the query instead
A RateLimitError whose exceeded is address or network on work you run regularly means the same thing.
Getting one
Write to us through activedns.net/contact and say what you are building and roughly how much you expect to query. Tokens are issued by hand, with limits to fit: a research project, an integration in your product and a security team's daily monitoring need different things.
About the built-in token
It identifies the SDK to the API, whatever User-Agent a request has, and it is not a secret. It allows domain,
wildcard (*.example.com), address and network searches within the limits in the table above. Its rate limit
counts per network address (an IPv4 address, an IPv6 /64), with a second, larger one for the network around it
(IPv4 /24, IPv6 /48), so that the users of the SDK do not spend each other's allowance.
Say who you are
Client takes the name and version of your program and refuses to work without one. Requests then carry
User-Agent: mytool/1.0 activedns-py/0.1.0 (python 3.12.3; linux)
Everyone using the SDK's token looks the same to the API otherwise. With a name, a tool that misbehaves can be told apart from the rest, and its author asked about it instead of everyone being limited.
Examples
Three small programs in examples/, each one file that runs as it is:
python examples/query.py example.com one page of a search, and the rate limit
python examples/query.py --limit 10 192.0.2.0/24
python examples/subdomains.py --pages 5 example.com names under a domain, paging with next_page
python examples/combined.py --domain '*.example.com' --asn 64496
They use the SDK's own token; set ACTIVEDNS_TOKEN to use yours. combined.py needs one, and shows the
refusal without it; query.py with your own token also takes AS numbers (AS64496) and wider networks.
Errors
Every error is an ActiveDNSError:
| error | meaning |
|---|---|
RateLimitError |
a rate limit is reached and the client did not wait it out; exceeded names it, retry_at says when to come back |
ForbiddenError (403) |
the token may not make that query; other queries go on working |
UnauthorizedError (401) |
the token is not accepted; the client sends nothing more |
APIError |
any other error response, with status_code and message |
TransportError |
no answer: a connection failure or a timeout |
from activedns import RateLimitError
try:
page = client.query("*.example.com")
except RateLimitError as err:
# err.exceeded: "address", "network" or "token"
# err.retry_at: when to try again
...
A fair client
The API is shared, so the client holds itself back without being asked:
| situation | what the client does |
|---|---|
| several threads query at once | one request at a time (max_concurrent to change) |
| 429, 502, 503, 504, connection failure | up to 4 retries (max_retries), waiting 1 s, 2 s, 4 s, 8 s … (at most 30 s) with jitter, and at least Retry-After |
| told to slow down | every thread using the client waits, not only the one that was told |
asked to wait longer than 30 s (max_wait) |
no sleeping: the call raises RateLimitError with retry_at, and later calls fail at once until then |
| a response says no requests are left | the next request waits for the limit to reset instead of being sent and refused |
| the token is refused (401) | nothing more is sent |
| bad query, forbidden query, server error, request timed out | raised as it is, never retried |
the server timed out on a search (Page.timed_out) |
the page is returned as it is, not retried |
| a result has more pages | nothing: further pages are fetched only when you call next_page |
Share one Client across a program: these limits are kept per client. client.rate_limits holds the limits
the server reported with its latest response.
Options
| argument | default |
|---|---|
token |
the SDK's own token |
max_concurrent |
1 |
max_retries |
4 |
max_wait |
30 seconds |
timeout |
60 seconds |
http_client |
a new httpx.Client |
base_url |
https://activedns.net |
Records, pages and rate limits are immutable attrs classes.
Development
pip install -e '.[dev]'
pytest unit tests, against a fake server
ruff check .
ruff format --check .
mypy
pytest e2e about ten requests to the live API, with the SDK's own token
The same checks run in GitHub Actions on every push and pull request (.github/workflows/ci.yml): lint and
types, the unit tests on Python 3.10 to 3.14, and then the end-to-end tests against the built wheel.
Metadata
Release files for activedns 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| activedns-0.1.0.tar.gz | 21.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| activedns-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.5 kB
Release files / activedns-0.1.0.tar.gz
| Download URL | activedns-0.1.0.tar.gz |
|---|---|
| Size | 21.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dbdf17890ec6e1acf9d1f5da8bb2013c391939c8f1b1129476c904381233d410
|
|
BLAKE2b-256 checksum How to use checksums |
9ae857bea6a010c83fdd4cc2ed3d2ee320a4ade7d08c9d2adf241ac2016ef9d0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / activedns-0.1.0-py3-none-any.whl
| Download URL | activedns-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
09d6b67d5495d73b44ba09b20d47c423c4bfcde47f125834734cbee688451bbe
|
|
BLAKE2b-256 checksum How to use checksums |
c0ed724ceee5186cd1dfff3657f2b42595f0ca745e8830bb4f84f1563ec85777
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log