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.
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>&1The 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.
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
curlat the end for asendmailor an SESSendEmailAPI 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.