What you will build
DMARC tells you who sends in your name. Your sending platform (ESP) and Google Postmaster Tools tell you what happens next: how much is delivered, bounced, blocked, reported as spam, opened. Sender Audit collects both and serves them in one place, with opens from privacy proxies and bots counted apart from human ones. The API lets you:
- feed a deliverability dashboard with one series per platform and per domain;
- alert on a rise in bounces or complaints at one mailbox provider;
- explain a bad day: which provider blocked, for which reason, from which IP, on which campaign.
Before you start
- An API key created from the dashboard, under Account, Developer, on a plan that includes API access.
- Access to your sending platform: an API key for it, or the right to configure its webhooks.
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']}"
Step 1: connect your sending platform
There are two ways in, and they can be combined for the same platform.
| By API key | By webhook forwarding | |
|---|---|---|
| How | Sender Audit reads your platform’s API | Your platform’s webhooks are relayed to Sender Audit |
| What you get | Daily totals, and the sender identities configured on the platform | Every event: daily totals and the breakdown per mailbox provider, IP and campaign, with bounce reasons |
| Platforms | Brevo, Mailchimp, Mailjet, Mandrill, SendGrid, Amazon SES, Postmark, Sweego | Brevo, Mailjet, and any platform through the native format |
By API key
GET /integrations/esp/catalog lists the supported platforms with what each one needs. Most take a single API key; Mailjet and Amazon SES take several fields, sent as a JSON string.
curl -s -X POST "$SA_API/integrations/esp/connect" \
-H "Authorization: Bearer $SA_KEY" \
-H "Content-Type: application/json" \
-d '{"provider": "brevo", "credential": "xkeysib-..."}'
import json
session.post(f"{API}/integrations/esp/connect", json={
"provider": "mailjet",
"credential": json.dumps({"api_key": "...", "secret_key": "..."}),
})
The key is checked against the platform before being stored, encrypted. Give it read-only rights when the platform allows it. Data then syncs on its own; POST /integrations/esp/{provider}/sync forces a sync now.
GET /integrations/esp/{provider}/identities returns the sender identities found on the platform, each with what the platform says (esp_dkim_enabled, esp_spf_enabled) next to what DNS actually shows (dns_spf_valid, dns_dkim_found, dns_dmarc_policy). A domain the platform believes signed but with no DKIM key in DNS shows up here. account_domain_id is the id of the matching monitored domain, null when you do not monitor it.
By webhook forwarding
POST /integrations/esp/forward/connect returns the URLs to relay your platform’s webhooks to, unchanged. Brevo gets one URL per channel, to match its separate transactional and marketing webhooks; Mailjet and the native format get a single URL, keyed all, since the channel travels in each event.
curl -s -X POST "$SA_API/integrations/esp/forward/connect" \
-H "Authorization: Bearer $SA_KEY" \
-H "Content-Type: application/json" \
-d '{"provider": "brevo", "channels": ["transactional", "marketing"]}'
{
"status": "connected",
"provider": "brevo",
"source": "client_forward",
"channels": ["transactional", "marketing"],
"urls": {
"transactional": "https://senderaudit.com/api/v1/webhooks/esp/...",
"marketing": "https://senderaudit.com/api/v1/webhooks/esp/..."
}
}
provider is brevo, mailjet, or senderaudit for the native format, used when your platform is not supported or when you send from your own servers. The URLs carry their own secret: treat them like a key.
Checking the connection
GET /integrations/esp lists the connected platforms with is_active, last_used_at and the active channels. GET /integrations/esp/quota returns the events consumed over the current period against your plan’s allowance.
Step 2: connect Google Postmaster Tools
Postmaster Tools is Google’s own view of your domains: spam rate as reported by Gmail users, authentication success, encryption, compliance with Gmail’s bulk sender rules.
Connecting it goes through Google’s consent screen, which needs a browser: do it once from the dashboard, under Integrations. After that, everything goes through the API:
GET /integrations/google-postmaster/domainslists the domains verified in your Postmaster account, whether each one is linked here, and your quota;POST /integrations/google-postmaster/link-domainwith{"gpt_domain": "example.com"}links one, and adds it to your monitored domains if needed, already verified. The response carries itsdomain_id, theidthe domain routes take;POST /integrations/google-postmaster/collectforces a collection now, up to 4 times an hour.
The figures are then read per domain with GET /domains/{id}/gpt-status: the latest day and 30 days of history, with spam_rate, spf_success_ratio, dkim_success_ratio, dmarc_success_ratio and compliance. Google publishes a day with a delay of one to two days, and only for domains with enough Gmail volume.
Step 3: the key figures
GET /esp/stats/providers returns the platform, channel and domain combinations that have data: the values to pass as filters everywhere else.
GET /esp/stats/daily returns one row per day, platform, channel and domain, between date_from and date_to (YYYY-MM-DD).
curl -s "$SA_API/esp/stats/daily?provider=brevo&date_from=2026-10-01&date_to=2026-10-14" \
-H "Authorization: Bearer $SA_KEY"
rows = session.get(f"{API}/esp/stats/daily", params={
"provider": "brevo", "date_from": "2026-10-01", "date_to": "2026-10-14",
}).json()["rows"]
for r in rows:
if not r["sent"]:
continue
bounce = (r["hard_bounces"] + r["soft_bounces"]) / r["sent"]
spam = r["spam_reports"] / r["delivered"] if r["delivered"] else 0
open_rate = r["unique_opens"] / r["delivered"] if r["delivered"] else 0
print(f'{r["date"]} {r["channel_type"]:13} {r["domain"]:20} bounce {bounce:.2%} spam {spam:.3%} open {open_rate:.1%}')
| Field | What it is |
|---|---|
sent, delivered | Accepted by the platform, accepted by the receiving server |
hard_bounces, soft_bounces | Permanent and temporary failures |
blocked, deferred | Refused by the receiver, or delayed |
spam_reports | Complaints from recipients |
unique_opens, unique_clicks | Messages opened or clicked at least once: the base for rates |
opens, clicks, unsubscribes | Raw counts |
opens_mpp, opens_proxied, opens_nhi, clicks_nhi | Opens from Apple Mail Privacy Protection, from image proxies, and opens and clicks from bots and security scanners, classified by Sender Audit from the user agent and the IP |
Human opens are opens - opens_mpp - opens_nhi. Use them, rather than raw opens, to compare engagement between periods: Apple’s proxy opens every email it receives, and link scanners click every link.
Gmail’s bulk sender rules put the complaint rate limit at 0.3 %, with 0.1 % as the target: a daily check on spam_reports / delivered per domain is the alert to set first.
Going further: the detail
These views need webhook forwarding: daily totals from an API key do not carry the mailbox provider, the IP or the campaign. Without webhook data, they answer with empty rows and "source": "none".
One day, broken down
GET /esp/stats/breakdown returns one day grouped by a single dimension, with group_by:
group_by | One row per |
|---|---|
mbp | Destination mailbox provider (Gmail, Microsoft, Yahoo…) |
ip | Sending IP. Delivery columns only: engagement events do not carry the IP |
campaign | Campaign |
day = session.get(f"{API}/esp/stats/breakdown", params={
"provider": "brevo", "channel": "marketing", "date": "2026-10-14", "group_by": "mbp",
}).json()
for r in day["rows"]:
print(r["key"], r["sent"], r["blocked"], r["spam_reports"])
available_columns lists the columns that make sense for the grouping. GET /esp/stats/detail returns the finest grain, one row per campaign, mailbox provider and IP together.
A trend for one provider
GET /esp/stats/timeseries returns the daily series of one value of a dimension, for example group_by=mbp&value=microsoft, to see when a provider started blocking.
Why it bounced
GET /esp/stats/bounce-reasons returns, for one day, the three most frequent bounce and block reasons per mailbox provider, with their count, the SMTP code and an example message. label_key identifies reasons Sender Audit knows (a blocklisting, a reputation block, an unknown user); template carries the provider’s text otherwise.
Disconnecting
DELETE /integrations/esp/{provider}disconnects a platform;?source=client_forwardtargets the webhook connection when both exist. Collected data is kept.DELETE /integrations/google-postmasterdisconnects Postmaster Tools.
Limits and errors
| Situation | Answer |
|---|---|
| Platform key refused by the platform | 400 |
| More than 4 Postmaster collections an hour | 429 |
| Too many calls on an endpoint | 429, with a Retry-After header |
| Missing or invalid key | 401 |
| Plan without API access | 403 |
Related
- API reference: Integrations and ESP Stats
- API: monitor your sending IPs
- API: track your DMARC data