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 for days, or a from and to period: sent, delivered, bounced split into bounced_permanent (hard) and bounced_transient (soft), complained, failed, suppressed (sends that skipped a suppressed recipient) and unsubscribed. 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's status: pending, verifying, verified, degraded or failed. A degraded domain 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 returns findings, most serious first, each with a code such as record_missing or dmarc_missing, a severity of blocking, warning or info, and the fix. It shares the DNS rate limit of verify, so call it once per domain per run.
  • sms_stats if 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:

cURL
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.

cURL
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, cron is UTC unless you add an IANA timezone. 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_dispatch lets 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 --bare and --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 409 and 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.

Frequently asked questions

What should a weekly email deliverability report include?

Sent and delivered counts, hard and soft bounces, complaints, suppressed sends and unsubscribes, each compared with the week before. Then every sending domain's status and each blocking or warning DNS finding with its fix. Open rates are the weakest line: they count only mail sent with open tracking on.

Can the agent fix the DNS problems it finds?

Not with this key, and it shouldn't. email_domains_diagnose is read-only, and the fix is a record at your DNS provider, which Unitpost can't edit. Once someone changes the record, verify the domain from the dashboard or with email_domains_verify, which needs domains:write.

Can I run the report without GitHub Actions?

Yes. The steps are shell commands, so cron on a server, or any scheduler that can run the unitpost CLI with the key in its environment, does the same reads and the same send.

Free to start

Start sending for free

$0/ mo

  • 5,000 emails / mo — no credit card
  • Your domain with DKIM, SPF & DMARC
  • REST API, SMTP & official SDKs
  • Visual editor + code templates
  • Open & click tracking + signed webhooks
  • 10,000 contacts · automatic suppression