Introduction
For a one-off query, dig and nslookup get the job done. But the moment DNS lookups need to happen inside an application — during provisioning, as part of a monitoring loop, or as a step in a security pipeline — command-line tools become a liability. They produce unstructured text that changes format across OS versions, require a shell subprocess, depend on the system resolver configuration, and offer no visibility into what actually happened when they fail.
The alternative is a DNS REST API: send an HTTPS request, get back structured JSON. No parsing, no environment dependency, no ambiguity. This guide covers how to query any DNS record type — standard records and email security records — using the WhoisJSON /nslookup endpoint, with concrete Node.js and Python examples for generic lookups, full email security audits, and parallel multi-type queries.
What Is a DNS Lookup API?
A DNS lookup API is an HTTPS service that accepts a domain name, queries authoritative DNS servers on your behalf, and returns the result as structured JSON. It is not the same as your operating system's resolver. The OS resolver uses a local cache, respects your network's DNS search domains, and may return stale results if the TTL has not expired locally. A DNS API bypasses the local stack entirely and queries the authoritative nameservers directly, returning fresh results every time.
For application developers, this distinction matters. System resolver behaviour varies across cloud providers, container runtimes, and local development environments. A DNS API provides a consistent, observable, environment-independent result: the same query returns the same response whether the code runs in a Docker container, a serverless function, or a developer's laptop. The response is always structured JSON — no text parsing, no format variations between dig versions, no silent failures when a record type is not supported locally.
Supported Record Types — Including Email Security Records
The WhoisJSON /nslookup endpoint returns all record types in a single JSON response. No separate calls per type — one request, one object.
Standard records
IPv4 and IPv6 addresses. The foundation of every DNS lookup — resolves a domain to its server IPs.
Mail exchange records. Each entry has an exchange hostname and a priority integer. Essential for verifying email routing.
Free-form text records used for SPF policies, domain ownership verification (Google, GitHub, etc.), and DKIM public keys.
Nameserver delegation. Identifies which nameservers are authoritative for the zone — useful for NS change detection.
Canonical name alias. Used for subdomain routing and CDN configurations. A dangling CNAME pointing to a decommissioned service is a subdomain takeover vector.
Start of Authority — zone metadata including primary nameserver, hostmaster email, serial number, and refresh/retry/expire timers.
Certification Authority Authorisation. Restricts which CAs may issue SSL certificates for the domain. Each record has flags, tag (issue, issuewild, iodef), and value.
Email security records
Most DNS APIs either ignore these record types or expose them through separate endpoints. WhoisJSON returns all four in the same response as the standard records above.
Domain-based Message Authentication. Defines the policy (none / quarantine / reject), reporting addresses (rua, ruf), and alignment mode. Queried from _dmarc.domain automatically.
Brand Indicators for Message Identification. Attaches a verified brand logo to authenticated emails. Requires DMARC p=quarantine or p=reject. Queried from default._bimi.domain.
Mail Transfer Agent Strict Transport Security. Signals that inbound SMTP connections must use TLS. Queried from _mta-sts.domain.
TLS Reporting. Specifies where TLS failure reports should be sent. Queried from _smtp._tls.domain.
Common Use Cases
- Email infrastructure verification: pull MX, TXT (SPF), and DMARC in a single round-trip to confirm that a domain's email routing and authentication are correctly configured before sending transactional email or activating a new tenant.
- Full email security audit: query MX + TXT + DMARC + BIMI + MTA-STS + TLSRPT in parallel for a complete picture of a domain's email security posture — useful for compliance checks, vendor due diligence, or security tooling.
- DNS change detection: poll NS and A records on a schedule, compare the serial number from SOA against a stored baseline, and trigger an alert when a change is detected. The SOA serial increments with every zone modification, making it a reliable change indicator.
- Domain validation before provisioning: verify that a TXT ownership token is present before activating a domain in your platform, or confirm MX records are live before enabling email features for a new customer.
- Subdomain takeover detection: enumerate CNAMEs for known subdomains and check whether the target hostname is still live. A CNAME pointing to a decommissioned service (Heroku, Fastly, GitHub Pages) is a takeover-ready dangling record.
- SSL issuance audit: verify CAA records to confirm that only approved certificate authorities are permitted to issue SSL certificates for a domain — a step in both pre-issuance validation and post-incident review.
How to Query DNS Records with WhoisJSON
The endpoint is GET https://whoisjson.com/api/v1/nslookup with a single required parameter: domain. Authentication is via the Authorization: TOKEN=YOUR_API_KEY header. There is no type parameter — the API always returns all available record types for the domain in one response.
/nslookup endpoint returns every record type simultaneously. To query a specific type only, filter the response client-side — do not make one request per type.cURL example:
curl -X GET "https://whoisjson.com/api/v1/nslookup?domain=whoisjson.com" \
-H "Authorization: TOKEN=YOUR_API_KEY"Abbreviated JSON response (fields present depend on what the domain has published):
{
"A": ["188.114.96.2", "188.114.97.2"],
"AAAA": ["2a06:98c1:3120::2", "2a06:98c1:3121::2"],
"MX": [
{ "exchange": "mailstream-central.mxrecord.mx", "priority": 20 },
{ "exchange": "mailstream-east.mxrecord.mx", "priority": 20 }
],
"NS": ["fish.ns.cloudflare.com", "anna.ns.cloudflare.com"],
"TXT": ["v=spf1 include:_spf.google.com ~all"],
"CAA": [
{ "flags": 0, "tag": "issue", "value": "letsencrypt.org" },
{ "flags": 0, "tag": "issuewild", "value": "letsencrypt.org" }
],
"SOA": {
"nsname": "fish.ns.cloudflare.com",
"hostmaster": "dns.cloudflare.com",
"serial": 2287007851,
"refresh": 10000,
"retry": 2400,
"expire": 604800,
"minttl": 3600
},
"DMARC": ["v=DMARC1; p=reject; rua=mailto:[email protected]"],
"BIMI": ["v=BIMI1; l=https://whoisjson.com/logo.svg; a=self"],
"MTASTS": ["v=STSv1; id=20250301000000Z"],
"TLSRPT": ["v=TLSRPTv1; rua=mailto:[email protected]"]
}
Code Examples
Example 1 — Generic DNS lookup (Node.js)
A reusable function that fetches all DNS records for a domain, then a parallel multi-type query that extracts A, MX, TXT, and NS from the single response.
const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://whoisjson.com/api/v1/nslookup';
/**
* Fetch all DNS records for a domain.
* Returns the full JSON object from the API.
*/
async function lookupDNS(domain) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const res = await fetch(
`${BASE_URL}?domain=${encodeURIComponent(domain)}`,
{
headers: { Authorization: `TOKEN=${API_KEY}` },
signal: controller.signal,
}
);
clearTimeout(timeout);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
clearTimeout(timeout);
throw err;
}
}
// --- Usage: extract A, MX, TXT, NS from a single response ---
const domain = 'whoisjson.com';
const records = await lookupDNS(domain);
console.log('A: ', records.A ?? []);
console.log('MX: ', records.MX ?? []);
console.log('TXT:', records.TXT ?? []);
console.log('NS: ', records.NS ?? []);
Example 2 — Full email security audit (Node.js)
A single lookupDNS() call retrieves all record types at once. The function below extracts every email security signal — MX, SPF, DMARC, BIMI, MTA-STS, TLSRPT — and formats them into a structured audit report.
// Reuses lookupDNS() from Example 1
/**
* Full email security audit for a domain.
* Returns a structured report with presence flags and raw values.
*/
async function emailSecurityAudit(domain) {
const rec = await lookupDNS(domain);
const mx = rec.MX ?? [];
const txt = rec.TXT ?? [];
const dmarc = rec.DMARC ?? [];
const bimi = rec.BIMI ?? [];
const mtaSts = rec.MTASTS ?? [];
const tlsRpt = rec.TLSRPT ?? [];
const spf = txt.find(t => t.startsWith('v=spf1')) ?? null;
return {
domain,
mx: {
present: mx.length > 0,
records: mx,
},
spf: {
present: spf !== null,
record: spf,
},
dmarc: {
present: dmarc.length > 0,
policy: dmarc[0]?.match(/p=(\w+)/)?.[1] ?? null,
record: dmarc[0] ?? null,
},
bimi: {
present: bimi.length > 0,
record: bimi[0] ?? null,
},
mtaSts: {
present: mtaSts.length > 0,
record: mtaSts[0] ?? null,
},
tlsRpt: {
present: tlsRpt.length > 0,
record: tlsRpt[0] ?? null,
},
};
}
// --- Usage ---
const audit = await emailSecurityAudit('whoisjson.com');
console.log(JSON.stringify(audit, null, 2));
/*
{
"domain": "whoisjson.com",
"mx": { "present": true, "records": [...] },
"spf": { "present": true, "record": "v=spf1 ..." },
"dmarc": { "present": true, "policy": "reject", "record": "v=DMARC1; ..." },
"bimi": { "present": true, "record": "v=BIMI1; ..." },
"mtaSts": { "present": true, "record": "v=STSv1; ..." },
"tlsRpt": { "present": true, "record": "v=TLSRPTv1; ..." }
}
*/
Example 3 — Email security audit (Python)
The same audit logic in Python using asyncio and aiohttp. A single asyncio.gather() call is used here to audit multiple domains in parallel.
# pip install aiohttp
import asyncio
import aiohttp
import json
import re
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://whoisjson.com/api/v1/nslookup"
HEADERS = {"Authorization": f"TOKEN={API_KEY}"}
async def lookup_dns(
session: aiohttp.ClientSession,
domain: str,
) -> dict:
"""Fetch all DNS records for a domain."""
async with session.get(
BASE_URL,
params={"domain": domain},
headers=HEADERS,
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
resp.raise_for_status()
return await resp.json()
def extract_spf(txt_records: list[str]) -> str | None:
return next((t for t in txt_records if t.startswith("v=spf1")), None)
def extract_dmarc_policy(dmarc_records: list[str]) -> str | None:
if not dmarc_records:
return None
m = re.search(r"p=(\w+)", dmarc_records[0])
return m.group(1) if m else None
async def email_security_audit(
session: aiohttp.ClientSession,
domain: str,
) -> dict:
"""Full email security audit for a single domain."""
try:
rec = await lookup_dns(session, domain)
except Exception as e:
return {"domain": domain, "error": str(e)}
txt = rec.get("TXT", [])
dmarc = rec.get("DMARC", [])
spf = extract_spf(txt)
return {
"domain": domain,
"mx": { "present": bool(rec.get("MX")), "records": rec.get("MX", []) },
"spf": { "present": spf is not None, "record": spf },
"dmarc": { "present": bool(dmarc), "policy": extract_dmarc_policy(dmarc), "record": dmarc[0] if dmarc else None },
"bimi": { "present": bool(rec.get("BIMI")), "record": (rec.get("BIMI") or [None])[0] },
"mtaSts": { "present": bool(rec.get("MTASTS")), "record": (rec.get("MTASTS") or [None])[0] },
"tlsRpt": { "present": bool(rec.get("TLSRPT")), "record": (rec.get("TLSRPT") or [None])[0] },
}
async def audit_many(domains: list[str]) -> list[dict]:
"""Audit multiple domains in parallel."""
async with aiohttp.ClientSession() as session:
return await asyncio.gather(
*[email_security_audit(session, d) for d in domains]
)
# --- Usage ---
if __name__ == "__main__":
domains = ["github.com", "cloudflare.com", "whoisjson.com"]
results = asyncio.run(audit_many(domains))
print(json.dumps(results, indent=2))
Querying Multiple Record Types at Once
Because the /nslookup endpoint returns all record types in a single response, "querying multiple types" means making one request and reading the fields you need — not making multiple requests. This keeps latency low and quota consumption minimal.
When you need DNS data for multiple domains simultaneously, the right approach is to send one request per domain in parallel — the same controlled concurrency pattern described in the bulk WHOIS lookup guide. A p-limit wrapper in Node.js or an asyncio.Semaphore in Python keeps the concurrent request count within your plan's RPM ceiling.
Error Handling
DNS lookup failures are per-domain, not global. Handle them independently so a single problematic domain does not abort a batch.
- NXDOMAIN (domain does not exist): the API returns an HTTP 400 or an empty response body. Log the domain and continue.
- SERVFAIL (resolver error): transient. Retry once after a short delay (2–5 seconds). If the second attempt also fails, log and skip.
- Record type not published: not an error — the field is simply absent from the JSON (e.g., no
DMARCkey if the domain has no DMARC record). Always use optional chaining or.get()with a default value when reading fields. - Timeout: set a per-request timeout of 10 seconds. DNS resolution for some ccTLDs can be slow; a hard timeout prevents the pipeline from stalling indefinitely.
- HTTP 429 (rate limit): back off for 60 seconds and retry. Tune concurrency to 80% of your plan's RPM to avoid hitting this in the first place.
Choosing the Right Plan
DNS lookups consume quota at the same rate as WHOIS requests — one API call per domain. The free Basic plan (20 RPM, 1,000 requests/month) is sufficient for ad-hoc validation and small monitoring loops. For continuous audits or bulk domain processing, the Pro plan (40 RPM, 30,000 requests/month) or higher is appropriate. High-volume pipelines — IOC enrichment, nightly portfolio sweeps, real-time provisioning validation — benefit from the unlimited-quota plans (Mega and above) with up to 900 RPM on Atlas.
See the full breakdown on the pricing page. A free account (1,000 requests/month, no credit card) is activated instantly on registration.
Conclusion
A DNS REST API replaces ad-hoc dig commands with a structured, reproducible, environment-independent lookup. One request returns every record type — A, MX, TXT, NS, CNAME, SOA, CAA, and the four email security records (DMARC, BIMI, MTA-STS, TLSRPT) — as a normalised JSON object ready for programmatic consumption.
The pattern is consistent regardless of use case: one request per domain, filter client-side for the types you need, handle missing fields gracefully with optional chaining or safe defaults, and scale to multiple domains with parallel requests bounded by your plan's rate limit.
Start for Free
DNS API with 1,000 free requests/month — no credit card. All record types, all endpoints.
Get Your Free API Key