Have an AI agent send you a weekly deliverability report
A weekly deliverability report takes three reads: email_stats for sends, hard and soft bounces, complaints, suppressions and unsubscribes, email_domains_list for each domain's status, and email_domains_diagnose for DNS findings with their fixes. A key with emails:read, domains:read and emails:send covers it on any plan. Run it from a GitHub Actions schedule: the CLI reads, a model writes the summary, and the CLI emails it to the team.
Unitpost ·
A deliverability report answers two questions: did last week's mail land, and is anything about to stop it. Unitpost answers each with one read, available as an MCP tool, a CLI command and a REST endpoint. The agent does the part people skip: turning numbers into a few sentences someone acts on.
What should the report read?
email_stats(GET /v1/email/stats,unitpost email stats,emails:read). Totals fordays, or afromandtoperiod:sent,delivered,bouncedsplit intobounced_permanent(hard) andbounced_transient(soft),complained,failed,suppressed(sends that skipped a suppressed recipient) andunsubscribed. Delivery, bounce and complaint rates are against sent; open and click rates count only mail sent with that tracking on.email_domains_list(GET /v1/email/domains,domains:read). Each domain'sstatus:pending,verifying,verified,degradedorfailed. Adegradeddomain failed a re-check: it keeps sending through a 24-hour grace, then stops.email_domains_diagnose(GET /v1/email/domains/{id}/diagnose,domains:read). It asks every authoritative nameserver about every record the domain needs and returnsfindings, most serious first, each with acodesuch asrecord_missingordmarc_missing, aseverityofblocking,warningorinfo, and thefix. It shares the DNS rate limit of verify, so call it once per domain per run.sms_statsif you also text (sms:read): delivered, failed, suppressed and segments billed. No opens or clicks.
A hard bounce or a complaint already suppresses that address, so the report isn't a cleanup list. It tells you where bad addresses come from and whether a domain is drifting.
Which key does the agent need?
emails:read, domains:read and emails:send. The Sending only preset, which every plan can create, has all three, plus campaigns:send and reads such as contacts, segments and templates. On a paid plan, create a custom key with just the three, and sms:read if you want SMS numbers.
Leave out domains:write. The agent can see a broken record but can't verify, change or delete a domain.
How do I ask an MCP client for it?
For a one-off report, connect your client to https://mcp.unitpost.com/mcp?toolsets=sending,domains, which lists email_stats, email_send and the email_domains_* tools instead of the full default set. The list only saves the model's context; the credential's scopes decide what can run. Then ask:
Read email_stats for the last 7 days and the 7 days before. Run
email_domains_diagnose once on each of my domains. Email a plain-text
summary to team@yourdomain.com from reports@yourdomain.com: what changed,
bounces, complaints, and each blocking or warning finding with its fix.That works with a person watching. On a schedule, split the job.
How do I run it every Monday?
This GitHub Actions workflow reads with the CLI, has Claude Code write the summary with no tools at all, and sends with the CLI. Each CLI command is the same API operation the MCP tool calls.
name: Weekly deliverability report
on:
schedule:
- cron: "17 8 * * 1"
timezone: "America/New_York"
workflow_dispatch:
permissions:
contents: read
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v7
with:
node-version: "22"
- run: npm install -g unitpost-cli @anthropic-ai/claude-code
- name: Read stats and domain health
env:
UNITPOST_API_KEY: ${{ secrets.UNITPOST_REPORT_KEY }}
run: |
unitpost email stats --days 7 > this-week.json
unitpost email stats --from "$(date -u -d '14 days ago' +%FT%TZ)" \
--to "$(date -u -d '7 days ago' +%FT%TZ)" > last-week.json
unitpost email domains list --limit 100 > domains.json
for id in $(jq -r '.data[].id' domains.json); do
unitpost email domains diagnose "$id"
done | jq -s '.' > dns.json
- name: Write the summary
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PROMPT: >-
Write a plain-text weekly deliverability report for the team
from this Unitpost JSON. No Markdown. Say what changed since
last week first. Use only numbers that appear in the JSON and
don't guess at causes. List each domain that isn't verified,
and each blocking or warning finding with its fix.
run: |
jq -s '{this_week: .[0], last_week: .[1], domains: .[2], dns: .[3]}' \
this-week.json last-week.json domains.json dns.json |
claude --bare -p "$PROMPT" --tools "" > report.txt
- name: Email the team
env:
UNITPOST_API_KEY: ${{ secrets.UNITPOST_REPORT_KEY }}
run: |
unitpost email send \
--from "Deliverability <reports@yourdomain.com>" \
--to team@yourdomain.com \
--subject "Deliverability report, week of $(date -u +%F)" \
--text-file report.txt \
--idempotency-key "deliverability-$(date -u +%G-W%V)"What each part is doing:
- The schedule. Per GitHub's workflow syntax,
cronis UTC unless you add an IANAtimezone. Scheduled runs can be delayed at busy times such as the top of the hour, hence minute 17, and a public repository's schedules turn off after 60 days without activity.workflow_dispatchlets you run it by hand. - Keys per step. The Unitpost key is only in the CLI steps, the Anthropic key only in the summary step. With
--bareand--tools "", Claude Code in print mode loads no MCP servers and has no tools: it reads the JSON on stdin and writes text. The model never holds a Unitpost credential and can't change the recipients. - JSON without
--json. When stdout isn't a terminal, the CLI prints exactly one JSON document. - The idempotency key names the ISO week. A re-run within 24 hours returns the first send, and if it wrote a different summary, the key answers
409and nothing goes out. - The From address must be on a domain verified in the key's workspace, or the send answers
403 domain_not_verified.
What makes the summary worth reading?
Change, not totals. Hard bounces jumping since last week sends someone to check the signup form; a delivered total alone sends nobody anywhere. That is why the workflow reads the week before too.
Keep each fix as Unitpost wrote it. It is the concrete next step for that finding, and a paraphrase can drop the detail that matters.
Tell the model not to explain. It has counts, not causes, and a guessed cause sends people the wrong way.
Is a weekly report enough?
Not for a broken domain. degraded lasts 24 hours before sends stop, and a weekly report can be six days late. Subscribe a webhook to domain.updated, which fires on every status change and carries previous_status, and to email.complained if complaints need a same-day look. The report is for trends; the webhook is the alarm.
How do I check the DNS myself?
email_domains_diagnose reads the records Unitpost expects from your own nameservers. To see what any receiver sees, use the email domain checker, which grades SPF, DKIM, DMARC and MX and links each lost point to its fix, and the DMARC checker, which reads your policy and where reports go. Both run in your browser with no sign-up, for any domain.