# DNSai REST API — Complete Reference

> **Purpose**: This document is a comprehensive reference for the DNSai REST API.
> Feed it to an AI assistant (Claude, ChatGPT, Copilot, etc.) to get help building
> integrations, writing API client code, mapping fields, and troubleshooting.

---

## Overview

The DNSai REST API provides programmatic access to DNS intelligence data. It covers:

- **Domain monitoring** — list, search, and add domains
- **DNS scan results** — MX, SPF, DKIM, DMARC, A/AAAA/NS/SOA/TXT/BIMI/SRV/PTR records
- **Email gateway detection** — identify Google Workspace, Microsoft 365, Mimecast, Proofpoint, etc.
- **SPF analysis** — lookup counts, nested includes, sending services, RFC 7208 compliance
- **MX/A server geolocation** — country, region, city, lat/lon, ASN for mail and web servers
- **WHOIS data** — registrar, dates, contacts, DNSSEC, privacy status
- **DNS change history** — track when MX, gateway, DMARC, SPF, and other fields change
- **Dashboard analytics** — portfolio-wide gateway distribution, DMARC compliance, SPF status
- **CSV exports** — trigger and download bulk exports
- **API key management** — create, list, and revoke keys

**Base URL**: `https://app.dnsai.com/api/v1`

---

## Authentication

All requests require a Bearer token in the `Authorization` header:

```
Authorization: Bearer dnsai_live_YOUR_API_KEY
```

- API keys start with the prefix `dnsai_live_`
- Create keys at: https://app.dnsai.com/settings/?tab=apikeys
- Maximum 3 active keys at a time
- Keys are shown only once at creation — store securely

---

## Response Format

Every response uses a consistent JSON envelope:

```json
{
  "status": "ok",
  "data": [ ... ],
  "meta": {
    "total_count": 1234,
    "limit": 250,
    "offset": 0,
    "next": "https://app.dnsai.com/api/v1/domains/?limit=250&offset=250",
    "previous": null
  },
  "errors": []
}
```

- `status` — `"ok"` or `"error"`
- `data` — the response payload (object, array, or null)
- `meta` — pagination metadata for list endpoints
- `errors` — array of `{code, detail}` objects

---

## Pagination

List endpoints use **limit/offset** pagination:

| Parameter | Default | Max   | Description                      |
|-----------|---------|-------|----------------------------------|
| `limit`   | 250     | 5,000 | Number of results to return      |
| `offset`  | 0       | —     | Starting position in result set  |

**Key facts**:
- Quotas count API calls, not rows. Fetching 5,000 rows = 1 API call.
- The `meta.next` field provides the full URL for the next page.
- Use `meta.total_count` to know the total result set size.

### Fetch all domains example (Python)

```python
import requests

API_KEY = "dnsai_live_YOUR_KEY"
headers = {"Authorization": f"Bearer {API_KEY}"}
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)} domains")
```

---

## Rate Limits & Quotas

| Plan                   | Price     | Rate Limit        | Daily Quota         |
|------------------------|-----------|-------------------|---------------------|
| Enterprise             | $99/mo    | 100 requests/min  | 10,000 calls/day    |
| Enterprise Max         | $2,500/mo | 500 requests/min  | 50,000 calls/day    |
| Enterprise Max Premium | $5,000/mo | 2,000 requests/min| 200,000 calls/day   |

### Rate limit headers (included in every response)

| Header                 | Description                                      |
|------------------------|--------------------------------------------------|
| `X-RateLimit-Limit`    | Max requests per minute for your plan             |
| `X-RateLimit-Remaining`| Requests remaining in current minute window       |
| `X-RateLimit-Reset`    | Unix timestamp when window resets                 |
| `X-API-Quota-Limit`    | Max API calls per day                             |
| `X-API-Quota-Used`     | API calls used today                              |

When rate limited, you get a `429` response with a `Retry-After` header (seconds to wait).

---

## Error Codes

| Status | Meaning            | Common Causes                              |
|--------|--------------------|--------------------------------------------|
| 400    | Bad Request        | Invalid request body, validation errors    |
| 401    | Unauthorized       | Missing or invalid API key                 |
| 403    | Forbidden          | Plan doesn't include API access            |
| 404    | Not Found          | Domain not in your account, bad endpoint   |
| 429    | Too Many Requests  | Rate limit or daily quota exceeded         |
| 500    | Server Error       | Internal error — contact support@dnsai.com |

---

## Endpoints

### GET /api/v1/ — API Root

Returns API version, user info, and all available endpoint paths.

### GET /api/v1/domains/ — List Domains

Returns your domains with scan summary data. Supports filtering and pagination.

**Query Parameters**:
- `search` (string) — filter by domain name (case-insensitive contains)
- `email_gateway` (string) — filter by gateway vendor (case-insensitive exact)
- `dmarc_status` (string) — filter by DMARC policy: `reject`, `quarantine`, `none`, `missing`
- `folder_id` (integer) — filter by folder
- `limit` (integer, default 250, max 5000) — results per page
- `offset` (integer, default 0) — starting position

**Response fields** (each item in `data` array):

| Field            | Type           | Description                                        |
|------------------|----------------|----------------------------------------------------|
| `id`             | integer        | Domain ID (use for detail/changes/whois lookups)   |
| `domain`         | string         | Domain name (e.g., `example.com`)                  |
| `email_gateway`  | string\|null   | Detected email gateway vendor                      |
| `dmarc_policy_p` | string\|null   | DMARC policy: reject, quarantine, none             |
| `spf`            | string\|null   | SPF record content                                 |
| `scanned_at`     | datetime\|null | Last scan timestamp (ISO 8601)                     |
| `added_at`       | datetime       | When added to your account                         |
| `folder_id`      | integer\|null  | Folder ID if organized                             |
| `is_favorite`    | boolean        | Whether domain is favorited                        |
| `tags`           | array[string]  | User-assigned tags                                 |

---

### POST /api/v1/domains/ — Add Domains

Add domains for monitoring. Max 500 per request.

**Request body**:
```json
{
  "domains": ["example.com", "example.org", "example.net"]
}
```

- Validates RFC 1035 format
- Rejects IP addresses and URLs
- Deduplicates within the request
- Automatically queues scans for newly added domains

**Response** (201 Created):

| Field              | Type    | Description                        |
|--------------------|---------|------------------------------------|
| `domains_submitted`| integer | Domains in the request             |
| `domains_added`    | integer | Newly added domains                |
| `domains_existing` | integer | Already in your account            |
| `scans_queued`     | integer | Scans queued for new domains       |

---

### GET /api/v1/domains/{id}/ — Domain Detail

Full domain detail including scan result, change history, and WHOIS.

**Response fields**:

| Field            | Type           | Description                              |
|------------------|----------------|------------------------------------------|
| `id`             | integer        | Domain ID                                |
| `domain`         | string         | Domain name                              |
| `tld`            | string         | Top-level domain (com, org, co.uk)       |
| `status`         | string         | active, inactive, or removed             |
| `first_seen`     | datetime       | When domain first appeared in DNSai      |
| `last_scanned`   | datetime\|null | Last scan timestamp                      |
| `scan_count`     | integer        | Total scans performed                    |
| `added_at`       | datetime       | When added to your account               |
| `folder_id`      | integer\|null  | Folder ID                                |
| `is_favorite`    | boolean        | Whether favorited                        |
| `tags`           | array[string]  | User-assigned tags                       |
| `notes`          | string\|null   | Your notes                               |
| `custom_label`   | string\|null   | Your custom label                        |
| `scan_result`    | object\|null   | Full scan result (see below)             |
| `change_history` | array          | Recent DNS changes (last 100)            |
| `whois`          | object\|null   | WHOIS registration data (see below)      |

---

### GET /api/v1/domains/{id}/scan-results/ — Scan Results

The complete DNS intelligence dataset for a domain. Also nested as `scan_result` in the domain detail response.

#### Core DNS Records

| 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: reject, quarantine, none   |
| `dmarc_rua`      | string\|null   | DMARC aggregate report email             |
| `dmarc_ruf`      | string\|null   | DMARC forensic report email              |
| `txt`            | string\|null   | TXT records                              |
| `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 record                              |
| `srv`            | string\|null   | SRV (service) records                    |
| `ptr`            | string\|null   | PTR (reverse DNS) records                |

#### Email Detection

| 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 (e.g., Salesforce, HubSpot, Zendesk) |

#### MX Server Geolocation

| 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 hosting provider     |
| `mx_server_asn_name`| string\|null   | ASN name / hosting provider name      |

#### A Record Server Geolocation

| 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       |

#### DKIM

| Field             | Type           | Description                            |
|-------------------|----------------|----------------------------------------|
| `dkim_hosts`      | string\|null   | DKIM host records found                |
| `dkim_selectors`  | string\|null   | DKIM selector names (e.g., google, selector1) |
| `dkim_vendors`    | string\|null   | Vendors identified from DKIM selectors |
| `dkim`            | string\|null   | DKIM record content (public key)       |

#### SPF Analysis

| Field               | Type           | Description                          |
|----------------------|----------------|--------------------------------------|
| `spf_main_lookups`  | integer\|null  | DNS lookups in top-level SPF record  |
| `spf_nested_lookups`| integer\|null  | DNS lookups in nested SPF includes   |
| `spf_total_lookups` | integer\|null  | Total DNS lookups (RFC 7208 limit: 10) |
| `spf_email_senders` | string\|null   | Sending services found in SPF (e.g., Mailchimp, SendGrid) |
| `spf_limit`         | string\|null   | SPF lookup limit status              |

#### Change Tracking (embedded in scan result)

| Field                    | Type           | Description                        |
|--------------------------|----------------|------------------------------------|
| `previous_mx`            | string\|null   | Previous MX records                |
| `mx_changed_date`        | datetime\|null | When MX last changed               |
| `previous_email_gateway` | string\|null   | Previous email gateway             |
| `gateway_changed_date`   | datetime\|null | When gateway last changed          |
| `previous_dmarc_p`       | string\|null   | Previous DMARC policy              |
| `dmarc_p_changed_date`   | datetime\|null | When DMARC policy last changed     |

---

### GET /api/v1/domains/{id}/changes/ — Change History

DNS change history for a domain. Paginated with `limit`/`offset`.

| Field         | Type           | Description                                    |
|---------------|----------------|------------------------------------------------|
| `id`          | integer        | Change record ID                               |
| `domain_name` | string         | The domain name                                |
| `field_name`  | string         | Which field changed (mx, email_gateway, dmarc_policy_p, spf, etc.) |
| `old_value`   | string\|null   | Previous value                                 |
| `new_value`   | string\|null   | New value                                      |
| `detected_at` | datetime       | When the change was detected (ISO 8601)        |

---

### GET /api/v1/domains/{id}/whois/ — WHOIS Data

WHOIS registration data. Also nested as `whois` in the domain detail response.

| Field                  | Type           | Description                         |
|------------------------|----------------|-------------------------------------|
| `domain_name`          | string         | The domain name                     |
| `registrar`            | string\|null   | Domain registrar                    |
| `registration_date`    | datetime\|null | First registered date               |
| `expiry_date`          | datetime\|null | Registration expiry                 |
| `updated_date`         | datetime\|null | Last WHOIS update                   |
| `name_servers`         | string\|null   | Authoritative nameservers           |
| `registrant_name`      | string\|null   | Registrant name (may be redacted)   |
| `registrant_org`       | string\|null   | Registrant organization             |
| `registrant_country`   | string\|null   | Registrant country                  |
| `registrant_state`     | string\|null   | Registrant state/province           |
| `contact_email`        | string\|null   | Contact email                       |
| `registrar_abuse_email`| string\|null   | Registrar abuse email               |
| `registrar_abuse_phone`| string\|null   | Registrar abuse phone               |
| `privacy_enabled`      | boolean        | WHOIS privacy enabled               |
| `dnssec_status`        | string\|null   | DNSSEC status                       |
| `domain_statuses`      | array          | EPP status codes                    |
| `whois_status`         | string\|null   | WHOIS lookup status                 |
| `whois_server`         | string\|null   | WHOIS server used                   |
| `ip_geolocation`       | object         | IP geolocation data                 |
| `fetched_at`           | datetime       | When WHOIS data was retrieved       |

---

### GET /api/v1/dashboard/summary/ — Dashboard Summary

Aggregated statistics across your domain portfolio.

| Field                        | Type    | Description                                   |
|------------------------------|---------|-----------------------------------------------|
| `total_domains`              | integer | Total domains in your account                 |
| `email_gateway_distribution` | array   | `[{gateway_name, count, percentage}, ...]`    |
| `dmarc_distribution`         | object  | `{reject: {count, percentage}, quarantine: {...}, none: {...}, missing: {...}}` |
| `spf_status`                 | object  | `{valid, over_limit, missing}` — count values |
| `scan_freshness`             | object  | `{scanned_last_24h, scanned_last_7d, percentage_24h, percentage_7d}` |
| `recent_changes`             | array   | Last 20 DNS changes (7-day window)            |
| `needs_attention_count`      | integer | Domains with DMARC or SPF issues              |

---

### GET /api/v1/exports/ — List Exports

List your export jobs (paginated).

| Field          | Type           | Description                          |
|----------------|----------------|--------------------------------------|
| `id`           | integer        | Export job ID                        |
| `export_type`  | string         | csv, pdf, zip, or diff               |
| `status`       | string         | queued, processing, completed, failed|
| `task_id`      | string         | Unique task identifier               |
| `file_url`     | string\|null   | Download URL when completed          |
| `row_count`    | integer        | Rows in the export                   |
| `file_size`    | integer        | File size in bytes                   |
| `parameters`   | object         | Export parameters                    |
| `created_at`   | datetime       | When requested                       |
| `completed_at` | datetime\|null | When finished                        |

### POST /api/v1/exports/ — Trigger Export

Trigger a CSV export as a background job (202 Accepted).

**Request body**:
```json
{
  "folder_id": null,
  "columns": null,
  "filters": {"gateway": "Google Workspace", "dmarc_policy": "none"}
}
```

### GET /api/v1/keys/ — List API Keys

| Field         | Type           | Description                    |
|---------------|----------------|--------------------------------|
| `id`          | integer        | API key ID                     |
| `key_prefix`  | string         | First 12 chars of the key      |
| `name`        | string         | Your name for this key         |
| `created_at`  | datetime       | When created                   |
| `last_used_at`| datetime\|null | Last API call timestamp        |
| `is_active`   | boolean        | Whether active                 |
| `expires_at`  | datetime\|null | Expiration date                |

### POST /api/v1/keys/ — Create API Key

**Body**: `{"name": "Production Server"}`
Returns the full key once (201 Created).

### DELETE /api/v1/keys/{id}/ — Revoke API Key

Immediately revokes the key (204 No Content).

---

## Code Examples

### Python — Complete Pagination

```python
import os
import time
import requests

API_KEY = os.environ["DNSAI_API_KEY"]
BASE_URL = "https://app.dnsai.com/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}

def fetch_all(endpoint, params=None):
    """Fetch all results from a paginated endpoint."""
    url = f"{BASE_URL}{endpoint}"
    if params is None:
        params = {}
    params.setdefault("limit", 5000)

    all_results = []
    request_url = url + "?" + "&".join(f"{k}={v}" for k, v in params.items())

    while request_url:
        response = requests.get(request_url, headers=headers)

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue

        response.raise_for_status()
        data = response.json()
        all_results.extend(data["data"])
        request_url = data["meta"].get("next")

    return all_results

# Fetch all domains
domains = fetch_all("/domains/")
print(f"Total domains: {len(domains)}")

# Fetch domains filtered by gateway
google_domains = fetch_all("/domains/", {"email_gateway": "Google Workspace"})

# Get full detail for each domain
for domain in domains[:5]:
    detail = requests.get(
        f"{BASE_URL}/domains/{domain['id']}/",
        headers=headers,
    ).json()["data"]

    scan = detail.get("scan_result") or {}
    whois = detail.get("whois") or {}

    print(f"\n{detail['domain']}:")
    print(f"  Gateway: {scan.get('email_gateway', 'N/A')}")
    print(f"  MX: {scan.get('mx', 'N/A')}")
    print(f"  SPF: {scan.get('spf', 'N/A')}")
    print(f"  DMARC: {scan.get('dmarc_policy_p', 'N/A')}")
    print(f"  Registrar: {whois.get('registrar', 'N/A')}")
    print(f"  Expires: {whois.get('expiry_date', 'N/A')}")
```

### JavaScript / Node.js

```javascript
const API_KEY = process.env.DNSAI_API_KEY;
const BASE_URL = "https://app.dnsai.com/api/v1";
const headers = { "Authorization": `Bearer ${API_KEY}` };

async function fetchAll(endpoint, params = {}) {
  params.limit = params.limit || 5000;
  const query = new URLSearchParams(params).toString();
  let url = `${BASE_URL}${endpoint}?${query}`;
  const results = [];

  while (url) {
    const response = await fetch(url, { headers });

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }

    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    results.push(...data.data);
    url = data.meta.next;
  }

  return results;
}

// Usage
const domains = await fetchAll("/domains/");
console.log(`Total: ${domains.length} domains`);

for (const d of domains.slice(0, 5)) {
  const detail = await fetch(`${BASE_URL}/domains/${d.id}/`, { headers });
  const { data } = await detail.json();
  console.log(`${data.domain}: ${data.scan_result?.email_gateway || "N/A"}`);
}
```

### curl

```bash
# Set your API key
export DNSAI_KEY="dnsai_live_YOUR_KEY"

# List domains (first 250)
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/" | jq '.data[] | .domain'

# List with pagination (5000 at a time)
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/?limit=5000" | jq '.meta.total_count'

# Filter by gateway
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/?email_gateway=Google+Workspace" | jq

# Domain detail
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/123/" | jq '.data.scan_result'

# Scan results
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/123/scan-results/" | jq

# Change history
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/123/changes/" | jq

# WHOIS data
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/domains/123/whois/" | jq

# Dashboard summary
curl -s -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/dashboard/summary/" | jq

# Add domains
curl -s -X POST \
  -H "Authorization: Bearer $DNSAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["example.com", "example.org"]}' \
  "https://app.dnsai.com/api/v1/domains/" | jq

# Trigger CSV export
curl -s -X POST \
  -H "Authorization: Bearer $DNSAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": {"gateway": "Microsoft 365"}}' \
  "https://app.dnsai.com/api/v1/exports/" | jq

# Check rate limits (inspect response headers)
curl -s -I -H "Authorization: Bearer $DNSAI_KEY" \
  "https://app.dnsai.com/api/v1/" | grep -i "x-rate\|x-api"
```

---

## Security Best Practices

1. **Store keys in environment variables** — never hardcode in source files
2. **Use a secrets manager** (AWS Secrets Manager, HashiCorp Vault) in production
3. **Never commit keys to version control** — add `.env` to `.gitignore`
4. **Rotate keys every 90 days** — create new before revoking old (max 3 active)
5. **Always use HTTPS** — the API does not accept HTTP connections
6. **Monitor usage** — check `X-API-Quota-Used` header and Settings > API Keys
7. **Handle rate limits gracefully** — implement retry with `Retry-After` header

---

## Common Integration Patterns

### Bulk Domain Enrichment
1. Fetch all domains: `GET /domains/?limit=5000`
2. For each domain, get full detail: `GET /domains/{id}/`
3. Extract `scan_result` and `whois` fields for CRM/database enrichment

### Change Monitoring
1. Periodically call `GET /dashboard/summary/`
2. Check `recent_changes` for new DNS changes
3. For specific domains: `GET /domains/{id}/changes/`

### DMARC Compliance Reporting
1. Call `GET /dashboard/summary/`
2. Read `dmarc_distribution` for reject/quarantine/none/missing counts
3. Filter non-compliant: `GET /domains/?dmarc_status=none` or `?dmarc_status=missing`

### Email Gateway Intelligence
1. Call `GET /dashboard/summary/`
2. Read `email_gateway_distribution` for vendor breakdown
3. Filter by vendor: `GET /domains/?email_gateway=Microsoft+365`

### Export for Offline Analysis
1. Trigger: `POST /exports/` with optional filters
2. Poll: `GET /exports/` until status = `completed`
3. Download via `file_url`

---

*DNSai API v1 — Documentation: https://app.dnsai.com/api-setup/*
*Support: support@dnsai.com*
