Bring DNSai DNS intelligence into your AI assistant, your code and your systems: an MCP server for Claude, ChatGPT, Copilot, Gemini and 19 more AI tools, a REST API with OpenAPI 3.1, and signed webhooks for DNS changes.
DNSai puts your DNS intelligence wherever you work: in your own code through the REST API, in your AI assistant through the MCP server, in your CRM and SIEM, and in your systems the moment something changes, through signed webhooks. Domains, MX and email gateways, SPF, DKIM and DMARC, nameservers, TXT records, WHOIS, website technologies, subdomains, Organization Deep Scans and every DNS change are all available.
The API and MCP server are included on Enterprise plans and above:
Limits belong to the subscription: everyone in your organization shares them, and every call counts from the first one.
All API endpoints are under https://app.dnsai.com/api/v1/ and return JSON responses in a consistent envelope format.
Prefer a traditional API? Jump to REST API and webhooks for curl, Python and JavaScript, Postman, signed webhooks and Excel, Power BI or Google Sheets, or straight to the endpoint reference.
Connect DNSai to the AI tools your team already uses, then ask in plain English: “Which of my target accounts moved to Microsoft 365 this month?”, “Summarize DMARC changes across my portfolio this week” or “Run an attack-surface review of example.com and its subdomains.” Each guide takes a few minutes.
Browse every guide: all AI integrations →
The DNSai MCP server is a remote Model Context Protocol server. Any MCP-capable assistant, including Claude, ChatGPT, Copilot, Cursor, Gemini, Perplexity and Grok, can read your domains, DNS and email-security posture, WHOIS, subdomains, website technologies, Deep Scan reports and change history, and act on them within your plan limits.
https://mcp2.dnsai.com/mcp
Claude, ChatGPT, Perplexity, Mistral Le Chat (OAuth, no key): add a custom connector with the URL above, sign in to DNSai and approve access. In Claude: Settings → Connectors → Add custom connector.
Claude Code, Codex, Cursor, VS Code / GitHub Copilot, Gemini and other desktop or command-line tools (API key): keep the key in an environment variable and send it as a header. For example, in Claude Code:
claude mcp add --transport http dnsai https://mcp2.dnsai.com/mcp \
--header "Authorization: Bearer $DNSAI_API_KEY"
Then ask: “Use DNSai whoami and tell me my plan.” Exact steps for each tool are in the AI integration guides.
| Method | Used by | How |
|---|---|---|
| OAuth 2.1 + PKCE (recommended) | Claude, ChatGPT, Perplexity, Mistral Le Chat | The client registers itself and you sign in and approve. Revoke it any time. Tokens carry the mcp scope. |
| API key header | Claude Code, Codex, Cursor, Copilot, Gemini, Grok, Windsurf, Hermes, LM Studio, n8n and more | Authorization: Bearer dnsai_live_… from an environment variable (see Authentication). Use one expiring key per tool. |
| Local bridge | Ollama, DeepSeek, Qwen, Llama and other local models | A small Python script turns DNSai’s read-only endpoints into function-calling tools for any OpenAI-compatible model server. |
| Area | Tools |
|---|---|
| Account | whoami — plan, limits and usage shared by your organization |
| Domains | list_domains, get_domain, get_domain_scan_results, lookup_domains (up to 100 at once), list_folders_and_tags |
| Email security | get_email_security — parsed DMARC tags, DKIM selectors, SPF analysis and gateway |
| WHOIS, website, subdomains | get_domain_whois, get_domain_website, get_domain_subdomains, get_technology_breakdown |
| DNS changes | list_changes (all filters below), get_change_stats, get_domain_changes |
| Organization Deep Scan | list_org_scans, get_org_scan, get_org_scan_report, list_org_scan_subdomains, estimate_org_scan, start_org_scan |
| Fresh data on demand | rescan_domain, scan_website, lookup_subdomains, refresh_whois |
| Organize and export | update_domain (star, tags, folder), add_domains, create_export (domains or changes, with a 15-minute download link), list_exports, get_job_status |
| Portfolio | get_dashboard_summary |
Guided prompts: audit_domain, triage_recent_changes, weekly_change_briefing, vendor_migration_watch, compare_spf_dmarc. Resources: dnsai://domains, dnsai://dashboard/summary, dnsai://changes/recent, dnsai://domain/{name}.
estimate_org_scan returns the cost and a 10-minute confirmation token, and start_org_scan only runs with that token after you approve.The API uses Bearer token authentication with API keys. Each API key is a long random string prefixed with dnsai_live_.
API access is available on Enterprise plans. Create an account or log in, then:
You can have up to 10 active API keys at a time — use one per tool or integration so you can revoke them separately. Every key draws from the same usage pool: your plan’s limits are shared by all members of your organization, and extra keys or users don’t add allowance.
Include your API key in the Authorization header of every request:
Authorization: Bearer dnsai_live_YOUR_API_KEY_HERE
Get up and running in minutes. The examples below cover the most common operations. All responses use a consistent JSON envelope with data, meta, and errors fields.
# Store your key in an environment variable — never hardcode it
export DNSAI_API_KEY="dnsai_live_YOUR_KEY_HERE"
curl -H "Authorization: Bearer $DNSAI_API_KEY" \
https://app.dnsai.com/api/v1/domains/
import os, requests
API_KEY = os.environ["DNSAI_API_KEY"]
headers = {"Authorization": f"Bearer {API_KEY}"}
# List your domains (default: 250 per request, max: 5000)
response = requests.get(
"https://app.dnsai.com/api/v1/domains/",
headers=headers,
params={"limit": 1000}, # fetch up to 1000
)
result = response.json()
print(f"Got {len(result['data'])} of {result['meta']['total_count']} domains")
for domain in result["data"]:
print(domain["domain"], domain["email_gateway"])
const API_KEY = "dnsai_live_YOUR_KEY";
const response = await fetch("https://app.dnsai.com/api/v1/domains/?limit=1000", {
headers: { "Authorization": `Bearer ${API_KEY}` },
});
const { data, meta } = await response.json();
console.log(`Got ${data.length} of ${meta.total_count} domains`);
data.forEach(d => console.log(d.domain, d.email_gateway));
For large datasets, follow meta.next to iterate through all pages. Quotas count API calls, not rows — fetching 5,000 rows counts as 1 call.
GET /api/v1/domains/ returns the entire active domain corpus, not just the domains you added, so a fetch-all loop can page through millions of rows and exhaust your daily call quota. Narrow the result with the ?search=, ?email_gateway= and ?dmarc_status= filters, or use an export, rather than paging the whole corpus.
url = "https://app.dnsai.com/api/v1/domains/?limit=5000"
all_domains = []
while url:
response = requests.get(url, headers=headers)
data = response.json()
all_domains.extend(data["data"])
url = data["meta"].get("next") # None when no more pages
print(f"Fetched {len(all_domains)} of {data['meta']['total_count']} domains")
curl -H "Authorization: Bearer $DNSAI_API_KEY" \
https://app.dnsai.com/api/v1/domains/123/
curl -X POST \
-H "Authorization: Bearer $DNSAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com", "example.org"]}' \
https://app.dnsai.com/api/v1/domains/
curl -H "Authorization: Bearer $DNSAI_API_KEY" \
https://app.dnsai.com/api/v1/dashboard/summary/
curl -H "Authorization: Bearer $DNSAI_API_KEY" \
"https://app.dnsai.com/api/v1/domains/?email_gateway=Google+Workspace&limit=5000"
Use AI tools like Claude, ChatGPT, or GitHub Copilot to build your DNSai integration faster. Download the reference files below and give them to your assistant for context-aware code generation. To let an assistant use your data directly instead of writing code, connect it to the MCP server.
Machine-readable description of every API-key endpoint, always current. Import it into Postman, Copilot Studio, client generators or any function-calling setup.
Download OpenAPI SpecComprehensive markdown document with complete field tables, code examples, and integration patterns. Paste it into any AI chat to get expert help.
Download Markdown ReferenceComplete, runnable Python client — authentication, pagination, rate limiting, the filtered change feed, subdomains, email security, Deep Scan reports and bulk lookups.
Download Python ClientAll endpoints are relative to https://app.dnsai.com/api/v1/
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/ | API root with links to all available resources |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/domains/ | List domains with scan summary. Supports ?search=, ?email_gateway=, ?dmarc_status=, ?folder_id=, ?web_technology=, ?site_type= filters. Paginated with ?limit= and ?offset= (default 250, max 5000). |
| POST | /api/v1/domains/ | Add domains for monitoring. Body: {"domains": ["example.com", ...]}. Max 500 per request. Validates RFC 1035, rejects IPs and URLs. |
| GET | /api/v1/domains/{id}/ | Full domain detail with latest scan result, change history, and WHOIS data. |
| GET | /api/v1/domains/{id}/scan-results/ | Latest scan result for a domain (DNS records, email gateway, SPF, DKIM, DMARC, geo). |
| GET | /api/v1/domains/{id}/changes/ | DNS change history for a domain, newest first. Paginated with ?limit= and ?offset=; filter with ?field=mx,spf, ?since=, ?until=. |
| GET | /api/v1/domains/{id}/whois/ | WHOIS registration data for a domain. |
| GET | /api/v1/domains/{id}/website/ | Website technologies for a domain (latest homepage scan). 404 until the domain has been website-scanned (from the app or POST …/website/scan/). |
| POST | /api/v1/domains/lookup/ | Compact profiles for up to 100 domain names in one call. Body: {"domains": ["example.com", ...]}. |
| GET | /api/v1/domains/{id}/email-security/ | Parsed DMARC tags and analysis, DKIM selectors, SPF analysis and email gateway. |
| GET | /api/v1/domains/{id}/subdomains/ | Subdomains from Certificate Transparency, each resolved in DNS. Filters: ?resolving=1, ?search=; paginated. |
| POST | /api/v1/domains/{id}/annotations/ | Star or unstar, add or remove tags, or move to a folder. Body: {"favorite": true, "add_tags": [...], "remove_tags": [...], "folder_id": 12} (folder_id 0 = none). |
| GET | /api/v1/changes/ | Every DNS change across your workspace, oldest first. Poll with ?since_id= (the last change id you processed) and store next_since_id; first poll ?since= ISO 8601 (default: last 24 hours). ?limit= up to 1,000 (default 250); has_more means call again. Accepts every filter in DNS Changes; ?order=desc&cursor= browses newest first. |
| GET | /api/v1/changes/stats/ | Change counts for the last ?days= 1–90 (default 7): by field, by day, and which email gateways gained or lost domains. |
These start work right away and count against your plan’s on-demand limits. They are refused while read-only mode is on.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/domains/{id}/rescan/ | Re-scan DNS now (counts against daily on-demand scans). |
| POST | /api/v1/domains/{id}/website/scan/ | Queue a website technology scan (hourly plan limit). |
| POST | /api/v1/domains/{id}/subdomains/lookup/ | Queue a fresh Certificate Transparency lookup (one per domain per hour). |
| POST | /api/v1/domains/{id}/whois/refresh/ | Live WHOIS lookup (hourly plan limit). |
A Deep Scan maps a domain and all its subdomains: DNS, email security, website technologies and findings. Starting one spends credits, so it takes two calls.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/domains/{id}/org-scan/estimate/ | Step 1: subdomain count, credit cost and a confirm_token valid for 10 minutes. Nothing is charged. |
| POST | /api/v1/domains/{id}/org-scan/start/ | Step 2: start the scan. Body: {"confirm_token": "osc_…"}. Each token works once. |
| GET | /api/v1/org-scans/ | Your Deep Scans, newest first. Filter with ?domain=. |
| GET | /api/v1/org-scans/{job_id}/ | Status and progress of a Deep Scan. |
| GET | /api/v1/org-scans/{job_id}/report/ | The report: aggregate counts, intelligence and findings. |
| GET | /api/v1/org-scans/{job_id}/subdomains/ | Per-subdomain rows. Filters: ?status=, ?search=; paginated. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/folders/ | Your folders with domain counts. |
| GET | /api/v1/tags/ | Your tags with domain counts. |
| GET | /api/v1/technologies/ | Most common technologies across your domains. ?kind=, ?limit=. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/dashboard/summary/ | Aggregated stats: email gateway distribution, DMARC/SPF status, scan freshness, domains needing attention. |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/exports/ | List export jobs. Paginated with ?limit= and ?offset=. |
| GET | /api/v1/exports/{job_id}/download/ | Download a completed export as CSV. |
| POST | /api/v1/exports/ | Trigger a CSV export as a background job. columns may include the DNS columns and the web_* website columns (see Website Fields). Body: {"folder_id": null, "columns": null, "filters": {"gateway": "...", "dmarc_policy": "...", "status": "...", "search": "..."}} (all optional). For DNS changes instead of domains, send {"mode": "changes", "since_date": "2026-10-01"} (up to one year back). Returns {"task_id", "status": "queued"}; poll GET /api/v1/exports/. |
These endpoints use your signed-in session, not an API key, so a leaked key can’t create more keys or change settings. Most people use Settings → API Keys instead.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/keys/ | List your API keys (key prefix only, never the full key). Key management uses your signed-in session token, not an API key. |
| POST | /api/v1/keys/ | Create a new API key. Body: {"name": "Key Name", "expires_in_days": 90, "allowed_ips": ["203.0.113.0/24"]} (expiry 1–365 days and IP allowlist are optional). Returns the full key once. |
| DELETE | /api/v1/keys/{id}/ | Revoke an API key. The key immediately stops working. |
| GET POST | /api/v1/webhooks/ | List or add change webhooks; …/{id}/test/ sends a test delivery and …/{id}/rotate-secret/ issues a new signing secret. |
| GET PATCH | /api/v1/access-policy/ | Read or set read-only mode for your API keys and AI assistants (organization admins can set it for everyone). |
All responses are wrapped in a consistent JSON envelope:
{
"status": "ok",
"data": [ ... ],
"meta": {
"total_count": 1234,
"limit": 250,
"offset": 0,
"next": "https://app.dnsai.com/api/v1/domains/?limit=250&offset=250"
},
"errors": []
}
meta.next and meta.previous are present only when another page exists: previous is absent on the first page and next on the last, rather than being sent as null.
For errors:
{
"status": "error",
"data": null,
"meta": {},
"errors": [
{
"code": "authentication_required",
"detail": "Authentication credentials were not provided."
}
]
}
List endpoints use limit/offset pagination designed for programmatic access. The default limit is 250, maximum 5,000 results per request.
| Parameter | Default | Max | Description |
|---|---|---|---|
limit | 250 | 5,000 | Number of results to return |
offset | 0 | — | Starting position in the result set |
The response meta object includes total_count so you know how many results exist. Follow meta.next to iterate, or calculate offsets directly.
# Pull up to 5000 domains per request — no UI pagination limits
url = "https://app.dnsai.com/api/v1/domains/?limit=5000"
all_domains = []
while url:
response = requests.get(url, headers=headers)
data = response.json()
all_domains.extend(data["data"])
url = data["meta"].get("next")
print(f"Fetched {len(all_domains)} of {data['meta']['total_count']} domains")
# Jump directly to any position in the result set
response = requests.get(
"https://app.dnsai.com/api/v1/domains/",
headers=headers,
params={"limit": 1000, "offset": 2000} # rows 2001–3000
)
data = response.json()
print(f"Got {len(data['data'])} domains (offset 2000 of {data['meta']['total_count']})")
DNSai tracks six fields on every domain: mx, email_gateway, dmarc_policy_p, spf, ns and txt. These filters work on /api/v1/changes/, /api/v1/changes/stats/ and the MCP list_changes tool:
| Parameter | Meaning |
|---|---|
field | One or more tracked fields, comma-separated |
domain / search | Exact domain names (comma-separated) / part of a domain name |
folder_id, tag, favorite=1 | Only domains in that folder, with that tag, or starred |
gateway | Domains whose current email gateway is this, e.g. Microsoft 365 |
gateway_from / gateway_to | Email-gateway switches away from / to a provider |
vendor | Changes that added or removed a record belonging to a vendor, e.g. Salesforce (newest-first mode) |
value | Text inside the old or new record |
since / until | ISO 8601 date or datetime bounds |
value searches (and, on whole-database plans, vendor and gateway searches) cover up to 31 days at a time; without since they use the last 7 days. Add a domain, folder_id or tag filter to search further back.
curl -H "Authorization: Bearer $DNSAI_API_KEY" \
"https://app.dnsai.com/api/v1/changes/?order=desc&gateway_to=Mimecast&since=2026-10-01"
Add up to three HTTPS endpoints under Settings → API Keys → DNS Change Webhooks. About every 15 minutes DNSai POSTs new changes on your domains (optionally only some fields), oldest first, in batches of up to 200:
{"event": "dns.changes", "webhook_id": "…", "count": 2, "has_more": false,
"next_since_id": 123456, "changes": [ { "id": 123455, "domain": "example.com",
"field_name": "email_gateway", "summary": "Email gateway: Proofpoint → Microsoft 365", … } ]}
Each request carries X-DNSai-Signature: t=<unix time>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your signing secret. Reject anything older than five minutes or with a signature that doesn’t match:
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Answer with any 2xx within 10 seconds. Redirects are not followed. Ten failed deliveries in a row pause the webhook and leave a notice in your notification bell; resume it from Settings once the endpoint is fixed. Endpoints must be public HTTPS on port 443 or 8443.
Limits belong to your subscription. Every member of your organization and every key draws from the same per-minute limit and daily quota, and every call counts from the first one. Extra keys or users don’t add allowance. (Organizations set to per-seat limits split the plan’s allowance evenly across included seats.) The MCP server has its own allowance, also shared by the organization; ask your assistant to run whoami to see it.
| Plan | Rate Limit | Daily Quota |
|---|---|---|
| Enterprise | 100 requests/min | 10,000 calls/day |
| Enterprise Max | 500 requests/min | 50,000 calls/day |
| Enterprise Max Premium | 2,000 requests/min | 200,000 calls/day |
Every API response includes these headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for your plan |
X-RateLimit-Remaining | Requests remaining in the current minute window |
X-RateLimit-Reset | Unix timestamp when the rate limit window resets |
X-API-Quota-Limit | Maximum API calls per day for your plan |
X-API-Quota-Used | API calls used today by everyone sharing the pool |
X-API-Quota-Scope | organization when the limits are shared by your organization, user otherwise |
When you exceed a rate limit or quota, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.
import time
def api_request(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
return response
raise Exception("Max retries exceeded")
The API uses standard HTTP status codes and returns errors in the envelope format.
| Status | Meaning | Common Causes |
|---|---|---|
400 | Bad Request | Invalid request body, validation errors |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Plan does not include API access |
404 | Not Found | Domain not in your account, invalid endpoint |
429 | Too Many Requests | Rate limit or daily quota exceeded |
500 | Server Error | Internal server error (contact support) |
{
"status": "error",
"data": null,
"meta": {},
"errors": [
{
"code": "rate_limit_exceeded",
"detail": "Rate limit exceeded (100 requests per minute). Please retry after 45 seconds."
}
]
}
Authorization: Bearer dnsai_live_...Retry-After header..gitignore or .env files# .env file (never commit this)
DNSAI_API_KEY=dnsai_live_YOUR_KEY_HERE
# Python usage
import os
API_KEY = os.environ["DNSAI_API_KEY"]
Every field available from the DNSai API, organized by endpoint. Use this reference when building integrations to know exactly what data is available.
Returned by GET /api/v1/domains/ for each domain in the data array.
| Field | Type | Description |
|---|---|---|
id | integer | Domain ID in your account (use this for detail/changes/whois lookups) |
domain | string | The domain name (e.g., example.com) |
email_gateway | string | null | Detected email gateway vendor (e.g., Google Workspace, Microsoft 365, Mimecast) |
dmarc_policy_p | string | null | DMARC policy: reject, quarantine, none, or Not Found (no DMARC record); null if never scanned |
spf | string | null | SPF record content |
scanned_at | datetime | null | When the domain was last scanned (ISO 8601) |
added_at | datetime | When you added this domain to your account |
folder_id | integer | null | Folder ID if organized into a folder |
is_favorite | boolean | Whether the domain is favorited |
tags | array of strings | User-assigned tags |
Returned by GET /api/v1/domains/{id}/ — includes all list fields plus:
| Field | Type | Description |
|---|---|---|
tld | string | Top-level domain (e.g., com, org, co.uk) |
status | string | Domain status: active, inactive, or removed |
first_seen | datetime | When the domain first appeared in DNSai |
last_scanned | datetime | null | Last scan timestamp |
scan_count | integer | Total number of scans performed |
notes | string | null | Your notes about this domain |
custom_label | string | null | Your custom label for this domain |
scan_result | object | null | Full scan result (see Scan Result Fields below) |
change_history | array | Recent DNS changes (see Change History Fields below) |
whois | object | null | WHOIS data (see WHOIS Fields below) |
website | object | null | Website technologies (see Website Fields below); null until scanned |
Returned by GET /api/v1/domains/{id}/scan-results/ and nested as scan_result in the domain detail response. This is the complete DNS intelligence dataset for a domain.
| Field | Type | Description |
|---|---|---|
scanned_at | datetime | When this scan was performed |
mx | string | null | MX (mail exchange) records |
spf | string | null | SPF record content (v=spf1 ...) |
dmarc | string | null | Full DMARC record (v=DMARC1; p=...) |
dmarc_policy_p | string | null | DMARC policy value: reject, quarantine, none, or Not Found (no DMARC record) |
dmarc_rua | string | null | DMARC aggregate report email address |
dmarc_ruf | string | null | DMARC forensic report email address |
txt | string | null | TXT records (verification tokens, etc.) |
a_records | string | null | A records (IPv4 addresses) |
aaaa_records | string | null | AAAA records (IPv6 addresses) |
ns | string | null | Nameserver records |
soa | string | null | SOA (start of authority) record |
bimi | string | null | BIMI (Brand Indicators for Message Identification) record |
srv | string | null | SRV (service) records |
ptr | string | null | PTR (reverse DNS) records |
| Field | Type | Description |
|---|---|---|
email_gateway | string | null | Detected email gateway vendor (e.g., Google Workspace, Microsoft 365, Mimecast, Proofpoint) |
tech_in_dns | string | null | Other technologies detected via DNS records (e.g., Salesforce, HubSpot, Zendesk) |
Country, region, city, coordinates and ASN fields (mx_server_*, a_server_*, WHOIS ip_geolocation) are resolved from the IP address. This product includes IP2Location LITE data available from https://lite.ip2location.com.
| Field | Type | Description |
|---|---|---|
mx_server_ipv4 | string | null | IPv4 address of primary MX server |
mx_server_ipv6 | string | null | IPv6 address of primary MX server |
mx_server_country | string | null | Country where MX server is hosted |
mx_server_region | string | null | Region/state of MX server |
mx_server_city | string | null | City of MX server |
mx_server_lat | decimal | null | Latitude of MX server |
mx_server_lon | decimal | null | Longitude of MX server |
mx_server_asn | string | null | ASN number of MX server hosting provider |
mx_server_asn_name | string | null | ASN name / hosting provider of MX server (e.g., Google LLC, Microsoft Corporation) |
| Field | Type | Description |
|---|---|---|
a_server_country | string | null | Country where web server is hosted |
a_server_region | string | null | Region/state of web server |
a_server_city | string | null | City of web server |
a_server_lat | decimal | null | Latitude of web server |
a_server_lon | decimal | null | Longitude of web server |
a_server_asn | string | null | ASN number of web hosting provider |
a_server_asn_name | string | null | ASN name / web hosting provider (e.g., Cloudflare, Amazon, DigitalOcean) |
| Field | Type | Description |
|---|---|---|
dkim_hosts | string | null | DKIM host records found in DNS |
dkim_selectors | string | null | DKIM selector names discovered (e.g., google, selector1) |
dkim_vendors | string | null | Vendors identified from DKIM selectors |
dkim | string | null | DKIM record content (public key data) |
| Field | Type | Description |
|---|---|---|
spf_main_lookups | integer | null | Number of DNS lookups in the top-level SPF record |
spf_nested_lookups | integer | null | Number of DNS lookups in nested SPF includes |
spf_total_lookups | integer | null | Total DNS lookups (RFC 7208 limit is 10) |
spf_email_senders | string | null | Email sending services found in SPF record (e.g., Mailchimp, SendGrid) |
spf_limit | string | null | SPF lookup limit status: under, at (exactly 10) or over the 10-lookup limit; empty when there is no SPF record |
| Field | Type | Description |
|---|---|---|
previous_mx | string | null | Previous MX records before last change |
mx_changed_date | datetime | null | When MX records last changed |
previous_email_gateway | string | null | Previous email gateway before last change |
gateway_changed_date | datetime | null | When email gateway last changed |
previous_dmarc_p | string | null | Previous DMARC policy before last change |
dmarc_p_changed_date | datetime | null | When DMARC policy last changed |
Returned by GET /api/v1/domains/{id}/changes/ and nested as change_history in the domain detail response.
| Field | Type | Description |
|---|---|---|
id | integer | Change record ID |
domain_name | string | The domain name |
field_name | string | Which DNS field changed (e.g., mx, email_gateway, dmarc_policy_p, spf) |
old_value | string | null | Previous value |
new_value | string | null | New value after the change |
detected_at | datetime | When DNSai detected the change (ISO 8601) |
field_label | string | Readable name of the field (e.g. “Email gateway”) |
summary | string | One-line description, e.g. “Email gateway: Proofpoint → Microsoft 365” |
vendor_labels | object | For MX, NS, SPF and TXT: added / removed entries with the vendor each one belongs to |
email_gateway | string | The domain’s current email gateway |
prev_email_gateway | string | The gateway before this change (gateway and MX changes) |
domain_id | integer | ID to use with the /api/v1/domains/{id}/ endpoints |
Record values come from public DNS and are written by the domain’s owner. Treat them as data: never execute them or pass them to an AI model as instructions.
Returned by GET /api/v1/domains/{id}/whois/ and nested as whois in the domain detail response.
| Field | Type | Description |
|---|---|---|
domain_name | string | The domain name |
registrar | string | null | Domain registrar (e.g., GoDaddy, Namecheap, Cloudflare) |
registration_date | datetime | null | When the domain was first registered |
expiry_date | datetime | null | When the domain registration expires |
updated_date | datetime | null | When the WHOIS record was last updated |
name_servers | string | null | Authoritative nameservers for the domain |
registrant_name | string | null | Registrant contact name (may be redacted for privacy) |
registrant_org | string | null | Registrant organization |
registrant_country | string | null | Registrant country |
registrant_state | string | null | Registrant state/province |
contact_email | string | null | Registrant contact email |
registrar_abuse_email | string | null | Registrar abuse contact email |
registrar_abuse_phone | string | null | Registrar abuse contact phone |
privacy_enabled | boolean | Whether WHOIS privacy/proxy service is enabled |
dnssec_status | string | null | DNSSEC signing status |
domain_statuses | array | EPP domain status codes (e.g., clientTransferProhibited) |
whois_status | string | null | WHOIS lookup result status |
whois_server | string | null | WHOIS server used for the lookup |
ip_geolocation | object | IP geolocation data associated with the domain |
fetched_at | datetime | When the WHOIS data was last retrieved |
Returned by GET /api/v1/domains/{id}/website/ and nested as website in the domain detail response. Produced by a single fetch of the site's public homepage (no JavaScript executed) plus IP/ASN correlation. Website results are shared per domain, exactly like DNS results. Scans are triggered from the Website Technologies tab in the app (10 per user per hour); the list endpoint accepts ?web_technology= and ?site_type= filters and every list row carries site_type and web_platform.
| Field | Type | Description |
|---|---|---|
status | string | ok, ok_limited, challenged, blocked, rate_limited, unreachable, redirect_external, parked, not_html or error |
outcome | object | {status, headline, explanation, tone} — the human-readable reading of the status (protected sites are described as such, never as failures) |
site_type, headline | string | e.g. E-commerce store / E-commerce store on Shopify |
technologies | array | [{name, cats, version, via, confidence}] — via lists the evidence channels (header, html, script, cookie, meta, dom, js, inline loader, gtm, implied, block page) |
embeds | array | Third-party hosts loaded by the page: [{host, kind, count, vendor, category}] |
forms | array | [{kind, method, action, action_host, inputs}]; kind is search, newsletter, contact, login, checkout or other |
headers | object | Security-header summary: server, x_powered_by, hsts, csp, x_frame_options, x_content_type_options, referrer_policy, permissions_policy |
hosting, hosting_summary | array, object | Per A/AAAA record: ip, asn, asn_name, country, city, known_host, kind (platform/hosting/cdn), role; summary has platform_host, provider, cdn, origin_hidden |
waf_vendor, http_status, fetched_url, title, generator, server | various | Protection product observed, final HTTP status and URL, page title, generator meta, server header |
social_links, jsonld_types, sitemaps | arrays | Social profiles found on the page, JSON-LD types, sitemap URLs from robots.txt |
scanned_at, source, scanner_version | datetime, string, string | When and how the row was produced (manual, deep = organization deep scan, bulk) |
Returned by GET /api/v1/dashboard/summary/ — aggregated statistics across your domain portfolio.
| Field | Type | Description |
|---|---|---|
total_domains | integer | Total domains in your account |
email_gateway_distribution | array | List of {gateway_name, count, percentage} objects |
dmarc_distribution | object | Object with reject, quarantine, none, missing keys, each containing {count, percentage} |
spf_status | object | {valid, over_limit, missing} — counts of SPF record status |
scan_freshness | object | {scanned_last_24h, scanned_last_7d, percentage_24h, percentage_7d} |
recent_changes | array | Last 20 DNS changes across your portfolio (last 7 days) |
needs_attention_count | integer | Domains with DMARC or SPF issues |
Returned by GET /api/v1/exports/ and POST /api/v1/exports/.
| Field | Type | Description |
|---|---|---|
id | integer | Export job ID |
export_type | string | Export format: csv, pdf, zip, or diff |
status | string | Job status: queued, processing, completed, or failed |
task_id | string | Unique task identifier for tracking |
file_url | string | null | Download path, /api/v1/exports/{id}/download/ (available when status is completed). Send the same Authorization header as every other call. |
row_count | integer | Number of rows in the export |
file_size | integer | File size in bytes |
parameters | object | Export parameters (columns, filters, folder_id) |
created_at | datetime | When the export was requested |
completed_at | datetime | null | When the export finished |
Returned by GET /api/v1/keys/ and POST /api/v1/keys/.
| Field | Type | Description |
|---|---|---|
id | integer | API key ID |
key_prefix | string | First 12 characters of the key (for identification) |
name | string | Your name for this key (e.g., “Production Server”) |
created_at | datetime | When the key was created |
last_used_at | datetime | null | When the key was last used for an API call |
is_active | boolean | Whether the key is active |
expires_at | datetime | null | Optional expiration date |
allowed_ips | array | null | IP addresses or CIDR ranges the key may be used from (empty = anywhere) |