Skip to main content

Monitoring Gmail Postmaster Tools for Spam Rate Regressions

Pull Gmail's user-reported spam rate through the Postmaster Tools API, alert on absolute and relative thresholds, and attribute a spike to the stream that caused it.

Gmail publishes the one number that tells you directly how recipients feel about your mail: the share of delivered messages that were reported as spam. This guide covers pulling it through the API on a schedule, alerting on it in a way that produces action rather than noise, and attributing a spike to the specific stream that caused it.

Root Cause: The Only Direct Signal You Get

Every other deliverability metric is an inference. Bounce rate tells you about address validity, engagement tells you about content, and your own delivery log tells you only that a receiver accepted the message. The user-reported spam rate is different: it counts an explicit human action, taken by a recipient, about your specific message.

That directness makes it the highest-value input to any monitoring system, and it comes with two properties that shape how you use it. It is delayed — the data for a given day typically appears two days later — so it can never be a real-time signal. And it is domain-scoped, reported against the DKIM d= domain rather than per template or per campaign, which means the API alone will tell you that something got worse without telling you what.

Gmail's stated ceiling is 0.30%, above which the account is treated as a problem sender. That number is a limit, not a target. A healthy transactional sender sits below 0.10%, and the useful alerting threshold is therefore well under the published ceiling — by the time you cross 0.30% the reputation damage has already been done.

Spam rate bands and where to alert Below 0.05 percent is healthy, 0.10 percent warrants investigation, 0.20 percent should page, and 0.30 percent is Gmail's published limit. Alert Far Below the Published Ceiling under 0.05% — healthy 0.10% — investigate 0.20% — page 0.30% — Gmail limit By the time the published limit is crossed, the reputation band has usually already moved. The threshold that produces useful action is roughly a third of the one Gmail documents.
Gmail's 0.30% is the point at which you are already a problem; the alerting threshold belongs where there is still headroom to react.

The Exact Setup

Access requires a verified domain and a service account with the read-only Postmaster scope. Verification is a DNS TXT record on the DKIM signing domain, which is the detail most teams get wrong — verifying the organisational domain shows no data when you sign as mail.example.com.

# postmaster_pull.py — fetch the daily series and expose it to your metrics stack.
import datetime as dt

from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/postmaster.readonly"]


def daily_series(domain: str, key_file: str, days: int = 30) -> list[dict]:
    creds = service_account.Credentials.from_service_account_file(key_file, scopes=SCOPES)
    api = build("gmailpostmastertools", "v1beta1", credentials=creds)

    rows = api.domains().trafficStats().list(
        parent=f"domains/{domain}", pageSize=days,
    ).execute().get("trafficStats", [])

    series = []
    for row in rows:
        # The resource name ends in the date: domains/<domain>/trafficStats/20260718
        day = dt.datetime.strptime(row["name"].rsplit("/", 1)[-1], "%Y%m%d").date()
        series.append({
            "date": day,
            # Absent when Gmail volume was below its reporting floor for that day.
            "spam_rate": float(row.get("userReportedSpamRatio", 0.0)),
            "band": row.get("domainReputation", "UNKNOWN"),
            "spf": float(row.get("spfSuccessRatio", 1.0)),
            "dkim": float(row.get("dkimSuccessRatio", 1.0)),
            "dmarc": float(row.get("dmarcSuccessRatio", 1.0)),
        })
    return sorted(series, key=lambda r: r["date"])

The important handling detail is the missing day. Gmail omits a day entirely when volume fell below its reporting floor, and code that treats a missing value as zero will report a perfect spam rate on your quietest days — then show an apparent spike when normal volume resumes. Treat an absent day as unknown and exclude it from any average.

Alerting on Both Absolute and Relative Change

A single absolute threshold misses the most useful signal. A sender sitting at 0.02% who moves to 0.06% has tripled their complaint rate and is nowhere near any published limit; that change is worth investigating precisely because it is early.

# alerts.py — combine an absolute floor with a relative-change rule.
WARN_ABS = 0.0010     # 0.10%
PAGE_ABS = 0.0020     # 0.20%
REL_FACTOR = 2.5      # week-over-week multiple that counts as a jump
REL_MIN = 0.0003      # ignore relative jumps below this — noise at low volume


def evaluate(series: list[dict]) -> list[tuple[str, str]]:
    known = [r for r in series if r["spam_rate"] is not None]
    if not known:
        return [("warn", "no Postmaster data — check domain verification and volume")]

    latest = known[-1]
    out = []

    if latest["spam_rate"] >= PAGE_ABS:
        out.append(("page", f"spam rate {latest['spam_rate']:.3%} on {latest['date']}"))
    elif latest["spam_rate"] >= WARN_ABS:
        out.append(("warn", f"spam rate {latest['spam_rate']:.3%} on {latest['date']}"))

    # Compare against the same weekday to avoid weekly traffic-shape effects.
    prior = next((r for r in reversed(known[:-1])
                  if (latest["date"] - r["date"]).days == 7), None)
    if prior and prior["spam_rate"] > 0 and latest["spam_rate"] >= REL_MIN:
        if latest["spam_rate"] >= prior["spam_rate"] * REL_FACTOR:
            out.append(("warn",
                f"spam rate up {latest['spam_rate'] / prior['spam_rate']:.1f}x "
                f"week over week: {prior['spam_rate']:.3%}{latest['spam_rate']:.3%}"))
    return out

Comparing against the same weekday rather than yesterday matters more than it looks. Transactional volume has a weekly shape — weekday signups, weekend receipts — and a day-over-day comparison generates a false alarm every Monday.

Attributing a Spike

The API reports one number per domain, so attribution has to come from your own data. The technique that works is correlation by send volume: for the day the spike occurred, list every template and segment by volume sent to Gmail addresses, and look for something that was not there the week before.

Attributing a spike to a stream The API gives one domain-level number, so attribution comes from correlating the spike date against your own per-template send volumes. One Number In, Attribution From Your Own Data Postmaster Tools spam rate 0.21% on 18 July, domain-wide your send log, same day volume per template and segment, Gmail only the stream that is new a re-engagement batch that did not run the week before Tag every send with its template and segment at dispatch time — attribution is impossible to reconstruct afterwards. A two-day reporting delay means the log has to be queryable by date, not only by recent activity.
Attribution depends entirely on tagging sends at dispatch time; without it, a domain-level spike is unattributable after the fact.

This is why every dispatch should carry template and segment tags into the event store. The reporting delay means you will be querying two-day-old data, so the log needs to be indexed by send date rather than only by recency.

Variant: Multiple Sending Subdomains

If transactional and marketing mail send from separate subdomains — as they should — each is verified and queried independently. Pull them in the same job and alert per domain, because the whole benefit of the split is knowing which stream moved. Aggregating them back into one number in your dashboard throws away the isolation you paid for.

Pipeline Integration

Run the pull once a day, well after the T-2 data lands, and write every reading to durable storage rather than only evaluating the latest. The historical series is what makes the relative rule possible, and it is also what lets you answer whether a current problem is new or a return of something seen before.

Route the alerts to whoever owns deliverability and reputation, not to the template author. A spam-rate spike is a sending-practice event far more often than a content one, and defaulting the page to the last person who edited a template sends the investigation in the wrong direction.

Validation and Deployment Checklist

Debugging: Named Symptoms

Symptom: the reported rate is far higher than your own complaint webhook count. Cause: Gmail's number counts reports it never forwards to you, because feedback loops are not offered for Gmail. Fix: treat the two as independent signals — the webhook count covers Yahoo and Microsoft, and Gmail's rate is the only view of Gmail. Neither validates the other.

Symptom: a large spike on a single day with no matching send. Cause: almost always the reporting delay confusing the correlation — the spike is attributed to the day the messages were delivered, which can be a day after they were sent. Fix: widen the correlation window to two days either side before concluding a send was not responsible.

Symptom: the rate is zero every day. Cause: usually below the reporting floor rather than genuinely zero. Fix: check the volume figure in the same response; if it is also absent, you are below the floor and the metric is unusable at your current volume.

Reporting delay across three days A message sent on day one is delivered and reported on day one or two, and the figure appears in the API on day three, which is why correlation needs a widened window. Correlate Across Days, Not Within One day 1 — you send your log records it here day 1-2 — delivered, reported recipients press the button day 3 — visible in the API attributed to the delivery day A send-day correlation misses by one day in both directions, which is enough to blame the wrong stream entirely.
The API attributes complaints to the delivery day, so send-day correlation routinely points at the wrong template.

Frequently Asked Questions

Why is there no data for our domain?

Three causes account for almost all cases: the verification TXT record is on the wrong domain, the service account has not been granted access in the Postmaster Tools UI, or your Gmail volume is below the reporting floor of roughly 100 messages a day. Check them in that order — the first is by far the most common, and it produces exactly the same empty result as the third.

Can I get spam rate per template?

Not from Gmail. The metric is reported against the signing domain and nothing finer. If a stream is important enough to measure independently, give it its own subdomain and DKIM selector, which produces genuinely separate reporting at the cost of an additional identity to warm.

Does the reported rate include messages filed as spam by Gmail's filter?

No. It counts only messages a recipient explicitly reported, which is why it is such a clean signal — automated filtering decisions are excluded. That also means a rate of zero does not mean everything reached the inbox; it means nobody pressed the button on what did.

Should we alert on the reputation band as well?

Yes, but as a lower-urgency signal. The band is a lagging aggregate, so by the time it moves the underlying cause is weeks old. It is valuable for confirming that a remediation worked — a band returning to high is the evidence that the fix landed — rather than for detecting a new problem.


← Back to Email Deliverability and Reputation