Drive it from anything
Everything the interface does is available over HTTP with Basic credentials — from a browser address bar, a shell, a script or a scheduler. No session, no tokens to juggle.
Authentication
Every endpoint except the index requires HTTP Basic credentials — the same username and password used to sign in. The probe engine makes outbound connections on your behalf, so it is never exposed anonymously.
curl -u 'USERNAME:PASSWORD' 'https://webtool.nosiaht.com/api/v1/scan?sub=example.com'
Credentials also work inline in a URL, which makes a result viewable straight
from the address bar. Percent-encode reserved characters in the password
(@ becomes %40).
https://USERNAME:PASSWORD@webtool.nosiaht.com/api/v1/scan?sub=example.com
Endpoints
Base URL https://webtool.nosiaht.com/api/v1
Machine-readable index: endpoints, port profiles and the batch limit.
curl 'https://webtool.nosiaht.com/api/v1/'
Enumerate subdomains across six sources and merge them. Add
&probe=1 to probe the results in the same call.
Each entry in sources carries ok and
count. A source that contributed nothing reports
error: "unavailable" and nothing further — the
reason describes upstream state and this deployment's configuration,
so it is written to the server log instead.
| Parameter | Meaning |
|---|---|
| sub | Apex domain to enumerate. Required. |
| probe | 1 also probes each host found. |
| profile | Which ports to scan when probing. Default quick. |
curl -u 'USER:PASS' 'https://webtool.nosiaht.com/api/v1/scan?sub=example.com&probe=1'
Probe specific hosts. Up to 100 per call, comma-separated.
curl -u 'USER:PASS' \
'https://webtool.nosiaht.com/api/v1/probe?target=example.com,www.example.com&profile=full'
Deep inspection of one host: all 17 ports with banner confirmation, full TLS certificate, security headers, redirect chain and MX / NS / TXT records.
curl -u 'USER:PASS' 'https://webtool.nosiaht.com/api/v1/inspect/example.com'
Same as /probe but emits newline-delimited JSON, one object per
host, written the moment each probe finishes. Use this for anything large.
curl -sN -u 'USER:PASS' -X POST \
-H 'Content-Type: application/json' \
-d '{"domains":["example.com","www.example.com"],"profile":"quick"}' \
'https://webtool.nosiaht.com/api/v1/probe/stream'
Port profiles
The profile parameter selects how much of each host is scanned.
| Profile | Ports | Use |
|---|---|---|
| quick default | 80, 443 | Bulk scanning. Web ports only. |
| standard | 21, 22, 25, 80, 443, 8080, 8443 | Common services alongside the web. |
| full | 21, 22, 25, 53, 80, 110, 143, 443, 465, 587, 993, 995, 3306, 5432, 6379, 8080, 8443 | Single-host inspection, with DNS records. |
Response fields
Each probed host returns the following. Fields are null when they do not apply.
| Field | Meaning |
|---|---|
| state |
online · offline (refused — host is up, nothing listening) ·
filtered (no response at all) · unresolved ·
blocked · invalid
|
| unreachable_reason | Why, when the state is not online. |
| protocol | Scheme of the final URL after redirects, not the one dialled. |
| status_code | HTTP status of the final response. |
| addresses, ipv6 | Resolved A and AAAA records. |
| http.final_url | Where the request actually ended up. |
| http.redirect_chain | Every hop, with its status code. |
| http.http_version | HTTP/1.1 or HTTP/2. |
| http.security_headers | Six headers as {name: true|false}. |
| http.server, http.title | Server banner and page title. |
| tls.grade | A–F. F means the certificate failed validation. |
| tls.valid, tls.error | Whether it verified, and why not. |
| tls.days_remaining | Negative once expired. |
| tls.self_signed, tls.covers_host | Self-signed, and whether the name matches. |
| open_ports[].banner | Service banner, where the protocol announces itself. |
| open_ports[].confirmed | Whether the banner matched the expected service. |
| cname | CNAME chain and whether its target still resolves. |
| takeover | Dangling-CNAME assessment, or null. |
| latency_ms, elapsed_ms | Response time, and total probe time. |
Large scans
A single call probes at most 100 hosts and has to finish inside the platform's request timeout. For a domain with hundreds of subdomains, enumerate first and then stream the probing in batches — results arrive as each host finishes rather than at the end.
# 1. enumerate only
curl -s -u 'USER:PASS' 'https://webtool.nosiaht.com/api/v1/scan?sub=example.com' \
| jq -r '.subdomains[]' > hosts.txt
# 2. probe in batches of 100, streamed
split -l 100 hosts.txt batch_
for f in batch_*; do
jq -Rsc '{domains: (split("\n") | map(select(length > 0))), profile: "quick"}' "$f" \
| curl -sN -u 'USER:PASS' -X POST -H 'Content-Type: application/json' \
-d @- 'https://webtool.nosiaht.com/api/v1/probe/stream'
done
Examples
curl -s -u 'USER:PASS' 'https://webtool.nosiaht.com/api/v1/scan?sub=example.com&probe=1' \
| jq '.results[]
| select(.tls.valid == false or .status_code >= 400)
| {domain, status_code, tls: .tls.grade, reason: .tls.error}'
curl -s -u 'USER:PASS' 'https://webtool.nosiaht.com/api/v1/scan?sub=example.com&probe=1' \
| jq -r '.results[]
| select(.tls.days_remaining != null and .tls.days_remaining < 30)
| "\(.domain)\t\(.tls.days_remaining) days"'
import httpx
AUTH = ("USERNAME", "PASSWORD")
BASE = "https://webtool.nosiaht.com"
scan = httpx.get(
f"{BASE}/api/v1/scan",
params={"sub": "example.com", "probe": 1},
auth=AUTH, timeout=90,
).json()
for host in scan["results"]:
if host["state"] == "online":
tls = host.get("tls") or {}
print(host["domain"], host["protocol"],
host["status_code"], tls.get("grade"))
import httpx, json
with httpx.stream(
"POST", f"{BASE}/api/v1/probe/stream", auth=AUTH, timeout=None,
json={"domains": ["example.com", "www.example.com"], "profile": "quick"},
) as response:
for line in response.iter_lines():
if not line:
continue
event = json.loads(line)
if event["event"] == "result":
print(event["data"]["domain"], event["data"]["state"])
const auth = "Basic " + btoa("USERNAME:PASSWORD");
const response = await fetch(
"https://webtool.nosiaht.com/api/v1/scan?sub=example.com&probe=1",
{ headers: { Authorization: auth } }
);
const scan = await response.json();
for (const host of scan.results) {
if (host.state === "online") {
console.log(host.domain, host.protocol, host.status_code, host.tls?.grade);
}
}
Errors
Failures return JSON with a stable code you can branch on.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_domain | Not a hostname, or an IP address was supplied. |
| 400 | missing_target | No target or sub given. |
| 400 | invalid_profile | Unknown profile name. |
| 401 | unauthorized | Missing or wrong credentials. |
| 403 | blocked | Target resolves into private address space. |
| 413 | batch_too_large | More than 100 hosts in one call. |
| 429 | rate_limited | Too many requests. Slow down. |
| 503 | misconfigured | The server is missing required configuration. |
Reachability is measured from this server's network. A host that firewalls or
geo-restricts it reports filtered, which means no answer — not
that the host is down.