Monitoring7 min read

Weekly deliverability report on autopilot

Monday morning, 08:00: Slack ping with last week's inbox rate per domain, per provider, with a seven-day delta. Here's the cron, the shell, and the webhook payload to build it.

Daily alerts catch fires. Weekly reports tell you if your house is trending toward a fire. Both are useful; most teams only build the first. Here's how to stand up the second in an hour, wire it to cron, and have it post to Slack every Monday at 08:00 sharp.

What you get

A Slack message every Monday summarising inbox rate per sender domain, broken down by Gmail / Outlook / Yahoo, with a delta compared to the previous week. One message. Scannable in 10 seconds.

Shape of the report

The goal is a message your CMO or head of growth can parse in ten seconds over coffee. That means no tables, no attachments, no dashboards. Just a Slack message.

Weekly deliverability · Week 49 · mail.acme.io

  Gmail    82%  (-3pp vs W48)
  Outlook  71%  (+5pp vs W48)
  Yahoo    89%  (flat)
  Mail.ru  64%  (-8pp vs W48)  <-- regression

  Tests run: 7   Seeds per test: 24
  Lowest day: Thu 73%   Highest: Tue 88%

The cron entry

One line, Monday 08:00 local. Feeds stdout and stderr into a log for debugging when the script inevitably breaks the first time.

# Weekly deliverability report - every Monday 08:00
0 8 * * 1  /opt/email-monitor/weekly.sh >> /var/log/weekly-deliv.log 2>&1

The script

Two API calls: one to list tests from the last 14 days (you need the last week AND the week before for the delta), one per test to fetch the per-provider breakdown. Aggregation happens in jq.

#!/usr/bin/env bash
set -euo pipefail

API="https://check.live-direct-marketing.online/api"
KEY="${INBOX_CHECK_API_KEY}"
SLACK="${SLACK_WEBHOOK}"
DOMAIN="mail.acme.io"

# Unix timestamps for last 14 days
NOW=$(date +%s)
WEEK_AGO=$(( NOW - 7 * 86400 ))
TWO_WEEKS_AGO=$(( NOW - 14 * 86400 ))

# Pull tests from last 14 days
TESTS=$(curl -s "$API/check?domain=$DOMAIN&since=$TWO_WEEKS_AGO" \
  -H "Authorization: Bearer $KEY")

# Compute inbox rate per provider for a time window.
# Args: $1 = starting timestamp
rate_for_window() {
  echo "$TESTS" | jq --arg from "$1" --arg domain "$DOMAIN" '
    [.[] | select(.completedAt > ($from | tonumber))] as $tests
    | {
        gmail:   (([$tests[] | .providers.gmail.inbox]   | add) / ([$tests[] | .providers.gmail.total]   | add) * 100 | floor),
        outlook: (([$tests[] | .providers.outlook.inbox] | add) / ([$tests[] | .providers.outlook.total] | add) * 100 | floor),
        yahoo:   (([$tests[] | .providers.yahoo.inbox]   | add) / ([$tests[] | .providers.yahoo.total]   | add) * 100 | floor),
        mailru:  (([$tests[] | .providers.mailru.inbox]  | add) / ([$tests[] | .providers.mailru.total]  | add) * 100 | floor),
        count:   ($tests | length)
      }'
}

CURR=$(rate_for_window $WEEK_AGO)
PREV=$(rate_for_window $TWO_WEEKS_AGO)

# Build Slack message (use jq to template it)
MSG=$(jq -n --argjson curr "$CURR" --argjson prev "$PREV" --arg domain "$DOMAIN" '
  {
    text: ("Weekly deliverability - " + $domain),
    blocks: [
      { type: "header", text: { type: "plain_text", text: ("Weekly: " + $domain) } },
      { type: "section", fields: [
        { type: "mrkdwn", text: ("*Gmail*: " + ($curr.gmail | tostring) + "% (" + (($curr.gmail - $prev.gmail) | tostring) + "pp)") },
        { type: "mrkdwn", text: ("*Outlook*: " + ($curr.outlook | tostring) + "% (" + (($curr.outlook - $prev.outlook) | tostring) + "pp)") },
        { type: "mrkdwn", text: ("*Yahoo*: " + ($curr.yahoo | tostring) + "% (" + (($curr.yahoo - $prev.yahoo) | tostring) + "pp)") },
        { type: "mrkdwn", text: ("*Mail.ru*: " + ($curr.mailru | tostring) + "% (" + (($curr.mailru - $prev.mailru) | tostring) + "pp)") }
      ]},
      { type: "context", elements: [
        { type: "mrkdwn", text: ("Tests: " + ($curr.count | tostring)) }
      ]}
    ]
  }')

curl -s -X POST "$SLACK" -H "Content-Type: application/json" -d "$MSG"

Three nuances that trip people up

Define the week in UTC, not local time

If your cron is in CET and your API data is in UTC, your "last 7 days" window drifts by an hour every time DST flips. Use UTC throughout and convert only at the presentation layer.

Handle missing data, not zero

If you did not run a test on Wednesday (CI was down, seed mailboxes rotated), the script should skip that day, not count it as 0% inbox. Filter on completedAt, not on a date range with assumed daily coverage.

Rate-limit awareness

If you have 20 domains and run the weekly report sequentially, you hit the API 20 times in a burst. The free tier has a burst limit of 10 requests per minute. Sleep 6 seconds between domains or use the paid tier.

Keep the report boring

Resist the urge to include every metric. Inbox rate per provider + delta + test count is plenty. Open rates, click-through, bounce rates belong in the ESP dashboard, not in the Monday morning ping. A report that takes 30 seconds to read is a report that gets read.

Useful extensions

  • Email digest instead of Slack. Same script, different sink. Swap the curl at the end for a sendmail or an SES SendEmail API call.
  • Per-team routing. Marketing sees marketing domains, product sees transactional. One script, multiple cron entries, different Slack channels.
  • Colour coding. Slack block kit supports coloured sidebars via attachments. Red for regressions greater than 10pp, yellow for 5—10pp, green for stable or up.

FAQ

Why Monday 08:00 instead of Friday close-of-business?

Two reasons. First, placement issues from Thursday and Friday still show up in data over the weekend, and by Monday morning you have a complete picture. Second, people act on Monday morning reports. Friday afternoon reports get read on the way to the pub and forgotten.

Can I use this with a spreadsheet instead of Slack?

Yes. Append a row to a Google Sheet via the Sheets API, or write to a shared CSV on S3 and import into Looker Studio. The report shape does not change — only the sink.

What if my ESP already sends weekly reports?

ESP reports cover delivery (accepted by the MTA). Seed-based reports cover placement (landed in the inbox). Different metric, different signal. Both are useful; only the placement report tells you if your users see your mail.

How long should I keep historical weekly reports?

Keep at least 13 weeks (one quarter) so you can diff against quarter-over-quarter. A year of reports (52 weeks) takes up essentially zero storage and pays for itself the first time someone asks "when did Outlook start tanking?"
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required