What you will build
If you send from dedicated IP addresses, their reputation is yours to watch: one blocklisting and a share of your mail goes to spam or bounces. Sender Audit checks each verified IP every hour against the blocklists, and every day for the rest (reverse DNS, SMTP banner, SenderScore). The API lets you:
- register your IPs automatically when you provision a new server or a new pool at your ESP;
- pull a reputation score per IP into your monitoring;
- alert as soon as an address is listed.
Before you start
- An API key created from the dashboard, under Account, Developer, on a plan that includes API access.
- Public IPv4 addresses with a reverse DNS (PTR): ownership is proved on the hostname the IP points to.
- Room in your quota.
GET /ips/quotareturnsmonitored_ips_usedagainstmonitored_ips_limit. The quota counts addresses: a /24 weighs 254.
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']}"
An entry can be a single IP (203.0.113.25), a CIDR block up to a /24 (203.0.113.0/24) or a range of up to 256 addresses (203.0.113.10-203.0.113.40). IP endpoints identify an entry by its id, returned when you add it; GET /ips lists them.
Step 1: preview
POST /ips/preview resolves the reverse DNS of the addresses without adding anything, and tells you where the proof can go.
curl -s -X POST "$SA_API/ips/preview" \
-H "Authorization: Bearer $SA_KEY" \
-H "Content-Type: application/json" \
-d '{"ip_value": "203.0.113.25"}'
{
"ip_value": "203.0.113.25",
"input_type": "single",
"ip_count": 1,
"ptr_hostnames": ["mail25.example.com"],
"root_domains": ["example.com"],
"dns_targets": { "ptr": ["mail25.example.com"], "root": ["example.com"] },
"email_targets": { "ptr": { "postmaster": ["postmaster@mail25.example.com"] }, "root": { "postmaster": ["postmaster@example.com"] } }
}
No PTR means no possible proof: set up the reverse DNS at your hosting provider first.
Step 2: add and verify
POST /ips/initiate adds the entry. Like domains, two methods: dns (a TXT record) or email (a link sent to postmaster, abuse, contact, support or admin, given as target).
For a block or a range, scope picks where the proof goes:
ptr(default): on every PTR hostname of the block;root: on the root domains those hostnames belong to, usually one. Much simpler for a /24.
ip = session.post(f"{API}/ips/initiate", json={
"ip_value": "203.0.113.0/24", "method": "dns", "scope": "root",
}).json()
ip_id = ip["id"]
print("Publish on:", ip["verification_hosts"])
print("TXT value:", f'senderaudit_verify:{ip["verification_token"]}')
Publish a TXT record with the value senderaudit_verify: followed by the token on each host listed in verification_hosts, then call POST /ips/{id}/verify:
curl -s -X POST "$SA_API/ips/$IP_ID/verify" -H "Authorization: Bearer $SA_KEY"
| Answer | Meaning |
|---|---|
{"status": "verified"} | Done, the first checks start right away |
{"status": "pending", "message": "3/5 host(s) verified"} | Some hosts still miss the record. Each call checks every host again, so keep the records already published and add the missing ones |
400 | No PTR to check against, or email method (verification then happens through the link) |
Step 3: read the IP’s health
GET /ips/{id}/health-card has the same shape as the domain health card: one score, categories, and actions sorted by what they bring.
card = session.get(f"{API}/ips/{ip_id}/health-card").json()
print(card["ip_value"], 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 IP is on a critical blocklist. Act now |
categories | reputation (blocklists), identity (PTR and forward-confirmed reverse DNS), senderscore, geoip, diagnostics (port 25 and SMTP banner), each with its items and their status pass, warn or fail |
actions | Every item below its maximum, sorted by gain |
ptr_map | For a block or a range, the PTR of each address |
Alerting on blocklists
GET /ips/{id}/health-history returns the last 90 days of measures, each with overall, rbl, senderscore and crisis. Comparing the last two points is enough to catch a listing or a SenderScore drop:
history = session.get(f"{API}/ips/{ip_id}/health-history").json()["history"]
if len(history) >= 2:
before, now = history[-2], history[-1]
if now["crisis"] and not before["crisis"]:
print("ALERT: critical blocklisting")
elif (now["rbl"] or 0) < (before["rbl"] or 0):
print("Blocklist score dropped:", before["rbl"], "->", now["rbl"])
Sender Audit also emails the account when one of its IPs gets listed. The API is for routing that signal into your own tools.
Pausing and removing
Reserved to admin and owner members: a key created by a plain member gets 403.
PATCH /ips/{id}/monitoringpauses or resumes an entry. Resuming checks the quota again: a block only comes back if all of its addresses fit.DELETE /ips/{id}removes the entry and its whole history, immediately and for good.
Limits and errors
| Situation | Answer |
|---|---|
| Block larger than a /24, range over 256 addresses, private or reserved address | 422 |
| Too many calls on an endpoint | 429, with a Retry-After header |
Entry 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, IPs: every field, and a console to try the calls
- API: monitor your domains
- API: analyse your sending performance