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.
Endpoint reference This guide shows which calls to chain and what to read in each response. Every field is documented in the API reference, under Integrations and ESP Stats, 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 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 keyBy webhook forwarding
HowSender Audit reads your platform’s APIYour platform’s webhooks are relayed to Sender Audit
What you getDaily totals, and the sender identities configured on the platformEvery event: daily totals and the breakdown per mailbox provider, IP and campaign, with bounce reasons
PlatformsBrevo, Mailchimp, Mailjet, Mandrill, SendGrid, Amazon SES, Postmark, SweegoBrevo, 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:

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%}')
FieldWhat it is
sent, deliveredAccepted by the platform, accepted by the receiving server
hard_bounces, soft_bouncesPermanent and temporary failures
blocked, deferredRefused by the receiver, or delayed
spam_reportsComplaints from recipients
unique_opens, unique_clicksMessages opened or clicked at least once: the base for rates
opens, clicks, unsubscribesRaw counts
opens_mpp, opens_proxied, opens_nhi, clicks_nhiOpens 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_byOne row per
mbpDestination mailbox provider (Gmail, Microsoft, Yahoo…)
ipSending IP. Delivery columns only: engagement events do not carry the IP
campaignCampaign
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

Limits and errors

SituationAnswer
Platform key refused by the platform400
More than 4 Postmaster collections an hour429
Too many calls on an endpoint429, with a Retry-After header
Missing or invalid key401
Plan without API access403