A command-line client for the Internet.nl batch API, plus a documented recipe for running your own batch instance.
Internet.nl tests a domain for IPv6, DNSSEC, RPKI, TLS and a set of informational checks. The public site tests one domain at a time in a browser; this tool submits a whole fleet to a batch API v2 endpoint, polls until the run finishes, and renders the result as a diffable table or as JSON for pipelines.
Status: implemented. The user-visible surface stays pinned in
openspec/changes/archive/2026-09-08-add-internetnl-cli/design.md; the commands below match the current behaviour.
New here? See the illustrated how-it-works page (the whole flow, the commands, the deploy topology), or the runnable internetnl-cli-demo (quickstart + example hosts + CI-gate example).
Try it in your browser, no account needed: https://mwest2020.github.io/internetnl-cli-demo/ — type a domain, get a real result from the same v1.0.0 service every tenant uses, one run at a time (strictly rate-limited; see docs/reference/demo-api.md for the contract that page relies on and docs/how-to/demo-run.md for how it is run).
Measure your own hosts freely. Measure someone else's only where the measurement is one their operator already invites.
Internet.nl is a public service whose own website invites anyone to test any domain, and the Dutch government publishes a twice-yearly measurement of some 12,000 government domains, per domain, by name. A single run against a domain that already sits in that category stays inside the norm the service itself sets. A run against a domain that does not, without asking, does not.
The rest is on you, and the tool enforces none of it:
- Once, not repeatedly. A batch run costs the measured host real work — DNS lookups, TLS handshakes, connection attempts. One run answers your question. A loop is abuse, whoever owns the domain.
- A verdict is not an audit. Batch results differ from the website's in documented ways. Never present a score about someone else's domain as a finding about their security.
- Aggregate, don't shame. If you measure a whole sector, publish what most often goes wrong and how to fix it — not a ranking of who scored worst. The first is useful to everyone; the second only burns the relationship you presumably wanted.
export INTERNETNL_ENDPOINT=https://batch.internet.nl/api/batch/v2
export INTERNETNL_CREDENTIAL=you:… # hosted API needs an account; user:pass, split on the first ':'
internetnl submit --file hosts.txt # prints request-id, then polls
internetnl poll <request-id> # resume after a closed laptop
internetnl results <request-id> --json > results.json # machine-readableThere is no default endpoint: you configure where you measure — the
hosted instance (account required) or your own.
Switching between them is a change of INTERNETNL_ENDPOINT, nothing else.
Every endpoint needs one, and there are three routes:
- A supporter key for
api.westerweel.work— the netnl facade in front of an instance run by this project's author. A one-off donation via Buy Me a Coffee of $2 or more mints a lifetime credential and mails it to you automatically, usually within a minute. It is a beta service on a best-effort, no-SLA basis, with the same fair-use rate limit for everyone — read docs/how-to/supporter-key.md for exactly what that promises and what it does not. - An account on the hosted batch API at Internet.nl, if your organisation qualifies for one.
- Your own instance — see Self-hosting below.
No credential at all is needed to try the tool: the browser page runs one domain at a time against the same facade, anonymously — the same service, not a scaled-down imitation of it.
- A client. The tests, the scoring and the verdicts belong to Internet.nl and run on the endpoint you point at. Nothing is measured, approximated or filled in locally: if the API is unreachable, the answer is an error, not a guess.
- A pipeline gate.
--fail-on-scoredexits non-zero when a subtest that counts toward the score fails, with an allowlist file for accepted exceptions. Informational findings are shown but never gate. - Honest. Every result carries the endpoint host, the run timestamp and the API version. A subtest missing from a response renders as unknown, never as passing.
- Not a dashboard (upstream has Internet.nl-dashboard), not a scraper of the website UI, and not a reimplementation of any test.
internetnl results <request-id> --format findings [--findings-out FILE]
writes netnl-findings/v1: every subtest of a finished batch, verbatim —
the API's own status and verdict, category names derived from the
API's own results.categories, and the domain's own score and report
URL. No aggregation, no re-scoring, no HTML scraping. It exists so a
consumer (Wanderer is the first) can adopt internet.nl results as its
own dimension without reimplementing internet.nl's category rules or the
batch API's edge cases. --json and the table are unchanged; see
docs/netnl-findings-v1.md for the schema.
Upstream documents the differences: the connection test is unavailable, DNSSEC tests skip the registrar lookup, and no prechecks run on whether a hostname resolves at all. Never quote a batch verdict as "the internet.nl score" without naming the endpoint — which is why every result is labelled.
Everything is environment-tunable; see
openspec/changes/archive/2026-09-08-add-internetnl-cli/design.md
for the full table (INTERNETNL_ENDPOINT, INTERNETNL_USERNAME,
INTERNETNL_PASSWORD, timeouts, poll interval and maximum, batch size,
config path). INTERNETNL_CREDENTIAL is a single username:password
alternative to the username/password pair, split on the first :; set
either form, never both. Credentials come from the environment only —
never from command-line arguments, and they never appear in output or
logs.
internetnl is meant to run in someone else's pipeline: --json,
stable exit codes (0 ok, 1 config, 2 transport/API, 3
--fail-on-scored gate, 4 poll timeout) and a bundled GitHub Action
(action.yml):
- uses: MWest2020/internetnl-cli@v1
with:
hosts: example.org
endpoint: https://api.westerweel.work
credential: ${{ secrets.INTERNETNL_CREDENTIAL }}See docs/how-to/ci.md for the full input reference, a plain-CLI recipe for other CI systems (GitLab CI et al.), and the gate's exit-code semantics.
If you cannot get an account on the hosted batch instance, and a supporter key does not fit, you can run your own. Read docs/reference/self-hosted.md first: it needs a server with a fixed public IPv4 address and IPv6 — not something you run behind NAT — and it is a maintained service, not a script. docs/how-to/deploy-instance-vps.md walks the whole deployment, and docs/how-to/self-hosting-pitfalls.md collects the five traps that cost this project days — including a documented setting that silently kills all container egress, and a certbot in the webserver image that cannot start, so certificates never renew.
- Python via
uv, standard library only;uv run pytestruns the suite (no test touches the network or your real$HOME). - The repo is built through the habitat agent chain: role definitions live in
.claude/agents/, andscripts/verify.shis the non-negotiable gate a builder run must pass.
MIT. This licence covers the client only: the tests, the scoring and the Internet.nl name belong to the Internet.nl / Platform Internetstandaarden project and are theirs.