1. Home
  2. Knowledge Base
  3. Public REST API Services
  4. Domain Intelligence API – Passive DNS

Domain Intelligence API – Passive DNS

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

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:

CredentialHeader namePurpose
Client IDX-Client-IdIdentifies your application
Client secretX-Client-SecretAuthenticates 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 (requests in Python, fetch in 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..."
}
FieldLifetime (default)Use
token1 hourSend as Authorization: Bearer <token> on data requests
refreshToken7 daysExchange 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:

StatusBodyCause
401Missing X-Client-Id or X-Client-SecretHeader missing
401Invalid X-Client-Id or X-Client-SecretWrong credentials
503Authentication service unavailableTemporary 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.

StatusMeaning
200Token is valid
401Missing, 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.

StatusBodyCause
400X-Refresh-Token header is requiredHeader missing
403Expired refresh token / Invalid or expired refresh tokenGet a new pair from /api/auth/token/ with the client secret

Recommended client logic:

  1. Cache token and refreshToken.
  2. On 401 from a data endpoint, call /api/auth/refresh/.
  3. Retry the original request once with the new access token.
  4. If refresh fails with 403, fall back to /api/auth/token/ with X-Client-Id / X-Client-Secret.

Authentication errors on data endpoints

StatusTypical bodyWhat to do
401Missing Authorization header.Add Authorization: Bearer …
401Token expiredRefresh (Step 4)
401Invalid tokenRequest a new token (Step 1)
400client_id not found in request.Token did not resolve to a billed client — contact Support
403Insufficient 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, not example.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}/
ParameterDefaultMaxMeaning
page1—Page number
page_size10100Rows 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:

ParameterMeaning
time_first_afterFirst-seen ≥ this time
time_first_beforeFirst-seen ≤ this time
time_last_afterLast-seen ≥ this time
time_last_beforeLast-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 → 200 with [] (not 404).
  • Query / reverse-IP: no matches → 404 with {"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.


Base path: /api/lookup/

Each row is one distinct resource-record value, aggregated over observations — not a per-crawl event log.

FieldMeaning
rrnameOwner name (FQDN with trailing dot)
rrtypeA, AAAA, NS, MX, TXT, CNAME, SOA, …
bailiwickZone context for the record
rdataArray of rdata strings (one value per row)
countHow many times this value was observed
time_first / time_lastUnix 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

  1. Authenticate — /api/auth/token/
  2. Asset overview — GET /api/lookup/rrset/name/{domain}
  3. Mail path — same URL with /MX, or /api/mailservers/search?query=…
  4. Nameservers — /NS, then pivot with /api/lookup/rdata/name/{ns}/NS
  5. IP pivot — take A/AAAA rdata, then /api/lookup/rdata/ip/{ip}
  6. Registration — /api/whois/lookup?domain= and /api/rdap/lookup?domain=
  7. 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)

CodeWhen
200Success (lookup may still be an empty array)
400Bad input (missing query param, invalid IP/CIDR/date)
401Missing, invalid, or expired credentials / token
403Refresh token rejected, or insufficient API credits
404No match on query / reverse-IP / WHOIS / RDAP
429Rate limit (browser/explorer traffic; billed JWT clients are credit-gated)
503Authentication 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.


Was this article helpful?

Related Articles