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.
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/quotareturns your plan, its limits, andmonitored_hosts_usedagainstmonitored_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 withtargetamongpostmaster,abuse,contact,supportandadmin.
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"])
| Answer | Meaning |
|---|---|
{"status": "verified"} | Done |
400 | The 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:
| State | Meaning |
|---|---|
ok | Published and correct |
todo | Missing: record holds what to publish |
wrong | Present but incorrect: found shows what is there, record the fix |
inherited | Covered by the parent domain |
pending | Published, waiting for the first data |
unknown, not_applicable | Could 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"]}')
| Field | What it is |
|---|---|
overall | Score out of 100 |
crisis | true when the domain is on a critical blocklist. Deal with that before anything else |
categories | Three families, auth, reputation, infra, each with its score, max and its items (SPF, DKIM, DMARC, blocklists, TLS, DNSSEC…), each item pass, warn or fail |
actions | Every item below its maximum, sorted by gain, the points it would bring back |
sparkline | The 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:
| Severity | Examples of event_type |
|---|---|
critical | rbl_listed, rbl_critical_listed, esp_spam_spike |
warning | dmarc_fail_spike, new_sending_source, dkim_selector_removed, senderscore_drop, esp_bounce_spike, spf_validity_change |
info | dmarc_policy_change, dmarc_pct_change, dkim_selector_added |
positive | rbl_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
| Endpoint | What you find there |
|---|---|
GET /domains/{id}/monitoring | Every 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-items | The 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-advisor | The 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}/snapshots | One snapshot per day of everything known about the domain, for your own trend analysis |
GET /domains/{id}/gpt-status | Google 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.
PATCH /domains/{id}/monitoringpauses or resumes monitoring for the whole domain, freeing its hosts from the quota.DELETE /domains/{id}removes the domain and its whole history, immediately and for good.
Limits and errors
| Situation | Answer |
|---|---|
| Too many calls on an endpoint | 429, with a Retry-After header |
verify before the TXT record is visible | 400 |
Domain id unknown on the account | 404 |
| Write call with a plain member’s key | 403 |
| Missing or invalid key | 401 |
| Plan without API access | 403 |
Related
- API reference, Domains: every field, and a console to try the calls
- API: track your DMARC data
- API: test your emails before sending