What you will build

Once a domain is verified, Sender Audit watches it continuously: SPF, DKIM, DMARC, MX, TLS, DNSSEC, blocklists, and the changes in all of them. The API lets you drive that from your own tools, for example to:

  • onboard domains automatically when a customer or a brand is created in your system;
  • hand the DNS work to whoever runs the zone, with the exact records to publish;
  • pull a health score per domain into a dashboard, and alert on blocklisting or a broken SPF.

This guide follows the life of a domain: add it, verify it, complete its setup, then read its health from the summary down to the raw records.

Endpoint reference This guide shows which calls to chain and what to read in each response. Every field is documented in the API reference, generated from the API itself.

Before you start

  • An API key created from the dashboard, under Account, Developer, on a plan that includes API access.
  • Access to the domain’s DNS, or to one of its administrative mailboxes, to prove you control it.
  • Room in your quota. GET /domains/quota returns your plan, its limits, and monitored_hosts_used against monitored_hosts_limit.
export SA_KEY="sa_live_your_key"
export SA_API="https://senderaudit.com/api/v1"
import os, requests

API = "https://senderaudit.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SA_KEY']}"

Domain endpoints identify a domain by its id, a UUID returned when you add it, not by its name. GET /domains lists the account’s domains with their id, is_verified and setup progress (setup_done out of setup_total).

Step 1: add the domain

POST /domains/initiate adds the domain and starts its verification. Two methods:

  • dns: you publish a TXT record. The simplest to automate.
  • email: a confirmation link goes to an administrative mailbox of the domain, chosen with target among postmaster, abuse, contact, support and admin.
curl -s -X POST "$SA_API/domains/initiate" \
  -H "Authorization: Bearer $SA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com", "method": "dns"}'
domain = session.post(f"{API}/domains/initiate", json={"domain": "example.com", "method": "dns"}).json()
domain_id = domain["id"]
{
  "id": "6f1c2b9e-3d4a-4e8f-9a51-0c7d2e8b4f13",
  "domain": "example.com",
  "verification_method": "dns",
  "verification_token": "Zk3v9Q…",
  "is_verified": false
}

A subdomain of a zone the account has already verified needs no proof of its own: it comes back verified straight away. Calling initiate again on the same domain issues a new token and cancels the previous one.

Step 2: verify it

With the DNS method, publish a TXT record on the domain itself (not on a subdomain) with the value senderaudit_verify: followed by the token:

example.com.  TXT  "senderaudit_verify:Zk3v9Q…"

Then call POST /domains/{id}/verify:

curl -s -X POST "$SA_API/domains/$DOMAIN_ID/verify" -H "Authorization: Bearer $SA_KEY"
r = session.post(f"{API}/domains/{domain_id}/verify")
if r.status_code == 400:
    print("Record not visible yet, retry in a few minutes")
else:
    print(r.json()["status"])
AnswerMeaning
{"status": "verified"}Done
400The record is not visible yet. DNS propagation can take a few minutes: retry, at most 10 times a minute
{"status": "conflict"}The domain is already verified on another account. POST /domains/{id}/confirm-transfer moves it to yours; reserved to admin and owner members

With the email method, verification happens when someone clicks the link received in the mailbox: verify answers 400 in that case. Poll GET /domains until is_verified turns true.

Once verified, start the first monitoring pass with POST /domains/{id}/monitoring/scan. It answers queued, and the records appear within a few minutes. After that, monitoring runs on its own: every 12 hours for DNS, every hour for blocklists.

Step 3: publish the records (DMARC, TLS-RPT, MTA-STS)

GET /domains/{id}/setup tells you what the domain still has to publish, with the exact record for each step.

setup = session.get(f"{API}/domains/{domain_id}/setup").json()
print(f'{setup["summary"]["done"]}/{setup["summary"]["total"]} done')
for step in setup["steps"]:
    if step["state"] in ("todo", "wrong") and step["record"]:
        rec = step["record"]
        print(step["key"], rec["type"], rec["name"], rec["value"])

Each step has a key (ownership, dmarc, tlsrpt, mta_sts) and a state:

StateMeaning
okPublished and correct
todoMissing: record holds what to publish
wrongPresent but incorrect: found shows what is there, record the fix
inheritedCovered by the parent domain
pendingPublished, waiting for the first data
unknown, not_applicableCould not be checked, or does not apply

The proposed DMARC and TLS-RPT records complete the existing ones with your collection addresses rather than replacing them. For MTA-STS, policy_file gives the URL and the content of the policy file to host.

After publishing, POST /domains/{id}/setup/check returns the same answer with fresh DNS lookups.

When someone else runs the DNS, POST /domains/{id}/setup/shares creates a public link, valid 14 days, that shows them the records to publish and their live state, without an account. Reserved to admin and owner members; DELETE .../setup/shares/{share_id} revokes it.

Step 4: read the domain’s health

GET /domains/{id}/health-card is the summary: one score and the list of what to fix, sorted by what it brings.

curl -s "$SA_API/domains/$DOMAIN_ID/health-card" -H "Authorization: Bearer $SA_KEY" \
  | jq '{overall, crisis, actions: [.actions[] | {label, status, gain}]}'
card = session.get(f"{API}/domains/{domain_id}/health-card").json()
print(card["overall"], "/ 100", "CRISIS" if card["crisis"] else "")
for action in card["actions"][:3]:
    print(f'+{action["gain"]:g}  {action["label"]}: {action["summary"]}')
FieldWhat it is
overallScore out of 100
crisistrue when the domain is on a critical blocklist. Deal with that before anything else
categoriesThree families, auth, reputation, infra, each with its score, max and its items (SPF, DKIM, DMARC, blocklists, TLS, DNSSEC…), each item pass, warn or fail
actionsEvery item below its maximum, sorted by gain, the points it would bring back
sparklineThe overall score over the last 30 measures

actions is the to-do list: its first entries are the fixes worth the most.

Alerting on what changed

GET /domains/{id}/events?days=7 returns the notable changes, one per day and type, with a severity:

SeverityExamples of event_type
criticalrbl_listed, rbl_critical_listed, esp_spam_spike
warningdmarc_fail_spike, new_sending_source, dkim_selector_removed, senderscore_drop, esp_bounce_spike, spf_validity_change
infodmarc_policy_change, dmarc_pct_change, dkim_selector_added
positiverbl_delisted, senderscore_recovery, an SPF that becomes valid again
for e in session.get(f"{API}/domains/{domain_id}/events", params={"days": 1}).json():
    if e["severity"] == "critical":
        print("ALERT", e["event_date"], e["event_type"], e["detail"])

A daily job that reads yesterday’s events of every domain and forwards the critical ones is the simplest monitoring you can plug in.

The score over time

GET /domains/{id}/health-history returns the score over the last 90 days, one point per measure, with each component (spf, dkim, dmarc, rbl, mx, tls, ssl, dnssec) alongside overall. Useful to date a regression.

Going further: the detail

EndpointWhat you find there
GET /domains/{id}/monitoringEvery DNS record watched (records), and their changes (timeline) with first_seen_at, last_seen_at and removed_at. building: true while the first pass is running
GET /domains/{id}/monitored-itemsThe hosts watched under the domain (the domain itself and the subdomains seen as From or envelope sender), each with how it was discovered. They are what the quota counts, and each one can be paused
GET /domains/{id}/spf-advisorThe SPF record compared with real DMARC traffic: lookups used out of 10, mechanisms nobody sends through, and an optimized_record when one is possible. Needs some DMARC history (has_sufficient_data)
GET /domains/{id}/snapshotsOne snapshot per day of everything known about the domain, for your own trend analysis
GET /domains/{id}/gpt-statusGoogle Postmaster Tools metrics, once Postmaster is connected

To force a fresh DNS pass outside the 12-hour cycle, call POST /domains/{id}/monitoring/scan?manual=true. It depends on your plan (403 with upgrade_required otherwise). Every call to this endpoint, manual or not, is limited to one per hour and per domain: the next one answers 429 with retry_after in seconds.

Pausing and removing

These calls are reserved to admin and owner members: a key created by a plain member gets 403.

Limits and errors

SituationAnswer
Too many calls on an endpoint429, with a Retry-After header
verify before the TXT record is visible400
Domain id unknown on the account404
Write call with a plain member’s key403
Missing or invalid key401
Plan without API access403