The Edgewatch Domain Intelligence API offers enriched passive DNS, RDAP, and infrastructure data to support threat detection, domain attribution, and cyber investigations. Track historical DNS records, registrar metadata, and name server relationships to uncover malicious activity and map global domain infrastructure. The API returns passive DNS, RDAP / WHOIS, and infrastructure data (nameservers, mail servers, registrar metadata). It is historical / observational data — it does not perform live DNS resolution on your behalf.
Base URL
https://pdns.edgewatch.net
Interactive documentation
- Interactive docs (Swagger): https://pdns.edgewatch.net/doc/
- OpenAPI schema: https://pdns.edgewatch.net/swagger.json
All paths below are relative to the base URL. Every example uses HTTPS.
1. What you need before you start
Edgewatch issues each client a pair of credentials:
| Credential | Header name | Purpose |
|---|---|---|
| Client ID | X-Client-Id | Identifies your application |
| Client secret | X-Client-Secret | Authenticates the token request |
Treat the secret like a password. Do not commit it to source control, do not put it in client-side JavaScript, and rotate it if it leaks.
You will also need:
curl, or an HTTP library (requestsin Python,fetchin Node, etc.)- A place to store the access token and refresh token (environment variables, a secrets manager, or your app config)
If you do not have credentials yet, contact Edgewatch Support.
2. Authenticate the client (step by step)
API access uses short-lived JWT access tokens. You exchange your client ID and secret for a token, then send that token on every data request.
Step 1 — Request an access token
curl -sS -X GET "https://pdns.edgewatch.net/api/auth/token/" \
-H "Accept: application/json" \
-H "X-Client-Id: YOUR_CLIENT_ID" \
-H "X-Client-Secret: YOUR_CLIENT_SECRET"
Successful response (200):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
| Field | Lifetime (default) | Use |
|---|---|---|
token | 1 hour | Send as Authorization: Bearer <token> on data requests |
refreshToken | 7 days | Exchange for a new pair when the access token expires |
Do not send X-Client-Id / X-Client-Secret on lookup requests. Those headers are only for /api/auth/token/.
Common errors:
| Status | Body | Cause |
|---|---|---|
401 | Missing X-Client-Id or X-Client-Secret | Header missing |
401 | Invalid X-Client-Id or X-Client-Secret | Wrong credentials |
503 | Authentication service unavailable | Temporary auth outage — retry later |
Step 2 — Call a data endpoint with the Bearer token
export PDNS_TOKEN="paste-the-token-value-here"
curl -sS \
-H "Accept: application/json" \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rrset/name/example.com"
The Authorization header must look like:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
A space after Bearer is required. Do not wrap the token in quotes inside the header value.
Billed accounts also receive remaining credit on successful responses:
X-Remaining-Credits: 142.5
Step 3 — Confirm the token is valid (optional)
curl -sS \
-H "Accept: application/json" \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/auth/validate/"
Successful response includes your client profile (uid, clientId, name, expiresAt, and related fields). Use this during integration tests or after a deploy.
| Status | Meaning |
|---|---|
200 | Token is valid |
401 | Missing, invalid, or expired token |
Step 4 — Refresh before the access token expires
When /api/auth/validate/ or a data call returns 401 (Token expired), request a new pair without sending the client secret again:
curl -sS -X GET "https://pdns.edgewatch.net/api/auth/refresh/" \
-H "Accept: application/json" \
-H "X-Refresh-Token: ${PDNS_REFRESH_TOKEN}"
Successful response (200) is the same shape as Step 1: a new token and a new refreshToken. Store both. Discard the previous pair.
| Status | Body | Cause |
|---|---|---|
400 | X-Refresh-Token header is required | Header missing |
403 | Expired refresh token / Invalid or expired refresh token | Get a new pair from /api/auth/token/ with the client secret |
Recommended client logic:
- Cache
tokenandrefreshToken. - On
401from a data endpoint, call/api/auth/refresh/. - Retry the original request once with the new access token.
- If refresh fails with
403, fall back to/api/auth/token/withX-Client-Id/X-Client-Secret.
Authentication errors on data endpoints
| Status | Typical body | What to do |
|---|---|---|
401 | Missing Authorization header. | Add Authorization: Bearer … |
401 | Token expired | Refresh (Step 4) |
401 | Invalid token | Request a new token (Step 1) |
400 | client_id not found in request. | Token did not resolve to a billed client — contact Support |
403 | Insufficient credits. | Top up or wait for the next credit cycle |
3. Conventions that apply to every query
HTTPS and JSON
Send Accept: application/json. Responses are JSON unless noted (WHOIS includes a plain-text block inside a JSON field).
Domain names
- Pass names without a trailing dot:
example.com, notexample.com. - Case does not matter; the API lowercases internally.
- Leading
*is stripped.
Lookup endpoints return owner names with a trailing dot in the JSON ("rrname": "example.com."). That is normal DNS presentation.
Pagination (search-style endpoints)
These routes use page numbers:
GET /api/query/{query}/GET /api/reverse-ip-lookup/{ip}/GET /api/domains-subdomains-discovery/{domain}/
| Parameter | Default | Max | Meaning |
|---|---|---|---|
page | 1 | — | Page number |
page_size | 10 | 100 | Rows per page |
Response envelope:
{
"count": 42,
"next": "https://pdns.edgewatch.net/api/query/example.com/?page=2",
"previous": null,
"results": []
}
Limit (lookup endpoints)
Passive DNS lookup routes take limit instead of pages. Default 100.
Time filters (lookup endpoints)
Unix timestamps in UTC seconds:
| Parameter | Meaning |
|---|---|
time_first_after | First-seen ≥ this time |
time_first_before | First-seen ≤ this time |
time_last_after | Last-seen ≥ this time |
time_last_before | Last-seen ≤ this time |
For large zones, pass a time_last_after window. Unbounded queries scan more history and are slower.
Empty results
- Lookup (
/api/lookup/…): unknown name or IP →200with[](not404). - Query / reverse-IP: no matches →
404with{"error": "…"}. - RDAP / WHOIS: not found →
404.
Credits
Each authenticated call against the billed surface consumes credits. Watch X-Remaining-Credits. Plan token refresh so you do not burn credits on 401 retries.
4. Quick start: one complete session
BASE="https://pdns.edgewatch.net"
# 1. Authenticate
AUTH=$(curl -sS "${BASE}/api/auth/token/" \
-H "X-Client-Id: ${PDNS_CLIENT_ID}" \
-H "X-Client-Secret: ${PDNS_CLIENT_SECRET}")
export PDNS_TOKEN=$(printf '%s' "$AUTH" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
export PDNS_REFRESH_TOKEN=$(printf '%s' "$AUTH" | python3 -c "import sys,json; print(json.load(sys.stdin)['refreshToken'])")
# 2. Query
curl -sS \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
-H "Accept: application/json" \
"${BASE}/api/lookup/rrset/name/example.com?limit=20"
# 3. When the access token expires
REFRESHED=$(curl -sS "${BASE}/api/auth/refresh/" \
-H "X-Refresh-Token: ${PDNS_REFRESH_TOKEN}")
export PDNS_TOKEN=$(printf '%s' "$REFRESHED" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
export PDNS_REFRESH_TOKEN=$(printf '%s' "$REFRESHED" | python3 -c "import sys,json; print(json.load(sys.stdin)['refreshToken'])")
The rest of this guide assumes Authorization: Bearer ${PDNS_TOKEN} on every request.
5. Passive DNS lookup (recommended)
Base path: /api/lookup/
Each row is one distinct resource-record value, aggregated over observations — not a per-crawl event log.
| Field | Meaning |
|---|---|
rrname | Owner name (FQDN with trailing dot) |
rrtype | A, AAAA, NS, MX, TXT, CNAME, SOA, … |
bailiwick | Zone context for the record |
rdata | Array of rdata strings (one value per row) |
count | How many times this value was observed |
time_first / time_last | Unix timestamps (UTC) |
5.1 Forward lookup — records for a domain
What DNS records do we know for this name?
GET /api/lookup/rrset/name/{domain}
GET /api/lookup/rrset/name/{domain}/{rrtype}
GET /api/lookup/rrset/name/{domain}/{rrtype}/{bailiwick}
# All types
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rrset/name/example.com"
# NS only
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rrset/name/example.com/NS"
# Seen since 1 Jan 2026, max 50 rows
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rrset/name/example.com?time_last_after=1735689600&limit=50"
Example body:
[
{
"rrname": "example.com.",
"rrtype": "NS",
"bailiwick": "example.com.",
"rdata": ["a.iana-servers.net."],
"count": 12,
"time_first": 1700000000,
"time_last": 1730000000
}
]
5.2 Reverse lookup — domains on an IP
Which names pointed at this address?
GET /api/lookup/rdata/ip/{ip}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rdata/ip/93.184.216.34"
Works for IPv4 and IPv6.
5.3 Reverse lookup — who uses this hostname in NS / MX / CNAME
GET /api/lookup/rdata/name/{hostname}
GET /api/lookup/rdata/name/{hostname}/{rrtype}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rdata/name/a.iana-servers.net/NS"
5.4 Reverse lookup — exact TXT / SPF / SOA string
URL-encode the value. The path is a catch-all.
GET /api/lookup/rdata/raw/{urlencoded_value}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/lookup/rdata/raw/v=spf1%20-all"
If the value also looks like a hostname or IP, matching name/IP rows are merged into the same array.
6. Search, reverse IP, and subdomains
These older search routes return mixed record serializers and use page / page_size. Prefer /api/lookup/ for new integrations.
6.1 Combined query (domain or IP)
GET /api/query/{query}/
Optional header: dribble-filter-rrtype: A (or NS, MX, TXT, …).
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/query/example.com/?page=1&page_size=25"
curl -sS \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
-H "dribble-filter-rrtype: A" \
"https://pdns.edgewatch.net/api/query/93.184.216.34/"
Each result includes rrtype, rrname, rdata, count, first_seen, last_seen, plus Unix time_first / time_last.
6.2 Reverse IP (legacy helper)
GET /api/reverse-ip-lookup/{ip}/
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/reverse-ip-lookup/93.184.216.34/?page=1&page_size=50"
Invalid IP → 400. No rows → 404.
6.3 Subdomain discovery
GET /api/domains-subdomains-discovery/{domain}/
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains-subdomains-discovery/example.com/?page=1&page_size=50"
Example body:
{
"count": 3,
"next": null,
"previous": null,
"results": {
"name": "example.com",
"subdomains": [
"mail.example.com",
"www.example.com"
]
}
}
7. Nameservers and mail servers
7.1 Search nameservers
GET /api/nameservers/search?query={substring}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/nameservers/search?query=cloudflare"
Example:
{
"nameserver": "cloudflare",
"domains": ["example.com"],
"count": 1
}
Batch (POST JSON):
curl -sS -X POST "https://pdns.edgewatch.net/api/nameservers/search" \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"nameservers": ["ns1.example.net", "ns2.example.net"]}'
7.2 Domains on one nameserver
GET /api/nameservers/domains/{hostname}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/nameservers/domains/ns1.example.net"
7.3 Nameserver inventory size
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/nameservers/count"
{ "total_nameservers": 123456 }
7.4 Mail servers (MX)
Substring search (e.g. google, microsoft, aspmx):
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/mailservers/search?query=google"
Exact MX hostname:
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/mailservers/domains/aspmx.l.google.com"
Results are capped (currently 10,000 domain names per response).
8. WHOIS and RDAP
8.1 WHOIS-style text (domain, IP, or CIDR)
Provide exactly one of domain, ip, or cidr.
GET /api/whois/lookup?domain={name}
GET /api/whois/lookup?ip={address}
GET /api/whois/lookup?cidr={network}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/whois/lookup?domain=example.com"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/whois/lookup?ip=8.8.8.8"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/whois/lookup?cidr=8.8.8.0/24"
Example:
{
"type": "domain",
"query": "example.com",
"whois": "Domain Name: EXAMPLE.COM\nRegistrar: ...",
"source": "pdns"
}
source is pdns when the answer comes from stored registration data, or live_rdap when the API filled a gap from a live RDAP server. The whois field is a text block (newlines included), suitable for display in a WHOIS panel.
8.2 Structured RDAP summary
GET /api/rdap/lookup?domain={name}
GET /api/rdap/lookup?ip={address}
GET /api/rdap/lookup?asn={number}
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/rdap/lookup?domain=example.com"
Domain example:
{
"domain": "example.com",
"asn": 15133,
"ipv4": ["93.184.216.34"],
"ipv6": [],
"status": ["client delete prohibited"],
"registrar": { "name": "RESERVED-Internet Assigned Numbers Authority", "iana_id": 376 },
"created": "1995-08-14T04:00:00+00:00",
"updated": "2024-08-14T07:01:31.339351+00:00",
"expires": "2025-08-13T04:00:00+00:00"
}
Batch POST:
curl -sS -X POST "https://pdns.edgewatch.net/api/rdap/lookup" \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com"], "ips": ["8.8.8.8"], "asns": [15169]}'
9. Domain and TLD analytics
Send the Bearer token on these routes as well.
Registrar IANA ID
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/iana-id?domain=example.com"
{
"domain": "example.com",
"iana_id": 376,
"registrar": "RESERVED-Internet Assigned Numbers Authority"
}
Batch:
curl -sS -X POST "https://pdns.edgewatch.net/api/domains/iana-id" \
-H "Authorization: Bearer ${PDNS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com", "iana.org"]}'
Active / expired counts
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/active"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/expired"
# Include the domain list (can be large)
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/expired?list=true"
Created or updated on a calendar day
Date format: YYYY-MM-DD.
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/created?date=2026-01-15"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/updated?date=2026-01-15"
EPP status
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/status?status=clientHold"
Domains by TLD
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/domains/by-tld/com"
Responses for by-tld, created, updated, status, and expired?list=true can be large. Prefer lookup/WHOIS when you already know the name.
TLD counts and expirations
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/tlds/count"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/tlds/expirations?date=2026-01-15"
10. Metadata
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/metadata/version"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/metadata/fields"
curl -sS -H "Authorization: Bearer ${PDNS_TOKEN}" \
"https://pdns.edgewatch.net/api/metadata/rate_limit"
fields lists the shared response keys (rrname, rrtype, rdata, time_first, time_last, registrar, asn, …). Use this when generating client stubs.
11. Typical investigation workflow
- Authenticate —
/api/auth/token/ - Asset overview —
GET /api/lookup/rrset/name/{domain} - Mail path — same URL with
/MX, or/api/mailservers/search?query=… - Nameservers —
/NS, then pivot with/api/lookup/rdata/name/{ns}/NS - IP pivot — take A/AAAA rdata, then
/api/lookup/rdata/ip/{ip} - Registration —
/api/whois/lookup?domain=and/api/rdap/lookup?domain= - Subdomains —
/api/domains-subdomains-discovery/{domain}/
12. Python example
import os
import time
import requests
BASE = "https://pdns.edgewatch.net"
CLIENT_ID = os.environ["PDNS_CLIENT_ID"]
CLIENT_SECRET = os.environ["PDNS_CLIENT_SECRET"]
class PdnsClient:
def __init__(self):
self.session = requests.Session()
self.session.headers["Accept"] = "application/json"
self.token = None
self.refresh_token = None
def authenticate(self):
r = self.session.get(
f"{BASE}/api/auth/token/",
headers={
"X-Client-Id": CLIENT_ID,
"X-Client-Secret": CLIENT_SECRET,
},
timeout=30,
)
r.raise_for_status()
data = r.json()
self.token = data["token"]
self.refresh_token = data["refreshToken"]
self.session.headers["Authorization"] = f"Bearer {self.token}"
def refresh(self):
r = self.session.get(
f"{BASE}/api/auth/refresh/",
headers={"X-Refresh-Token": self.refresh_token},
timeout=30,
)
r.raise_for_status()
data = r.json()
self.token = data["token"]
self.refresh_token = data["refreshToken"]
self.session.headers["Authorization"] = f"Bearer {self.token}"
def get(self, path, **kwargs):
url = f"{BASE}{path}"
r = self.session.get(url, timeout=60, **kwargs)
if r.status_code == 401:
try:
self.refresh()
except requests.HTTPError:
self.authenticate()
r = self.session.get(url, timeout=60, **kwargs)
r.raise_for_status()
return r.json()
client = PdnsClient()
client.authenticate()
rrset = client.get("/api/lookup/rrset/name/example.com", params={"limit": 50})
whois = client.get("/api/whois/lookup", params={"domain": "example.com"})
rdap = client.get("/api/rdap/lookup", params={"domain": "example.com"})
for row in rrset:
last = time.strftime("%Y-%m-%d", time.gmtime(row["time_last"] or 0))
print(row["rrtype"], row["rdata"], last)
print(whois["whois"][:400])
print(rdap.get("registrar"))
Install the dependency with pip install requests.
13. HTTP status codes (summary)
| Code | When |
|---|---|
200 | Success (lookup may still be an empty array) |
400 | Bad input (missing query param, invalid IP/CIDR/date) |
401 | Missing, invalid, or expired credentials / token |
403 | Refresh token rejected, or insufficient API credits |
404 | No match on query / reverse-IP / WHOIS / RDAP |
429 | Rate limit (browser/explorer traffic; billed JWT clients are credit-gated) |
503 | Authentication service unavailable |
14. What this API does not do
- Live recursive DNS resolution as the primary answer (data is passive / historical).
- Per-resolution packet captures or a full change-by-change audit log. Lookup rows are aggregated RRsets.
- Returning ingest or worker-control endpoints. Those are internal.
Interactive try-it-out lives at /doc/. Paste your Bearer token into the Swagger Authorize dialog (Bearer security scheme) after you complete Step 1.