Skip to content

Powered by Grav CMS + Helios Theme

Reports

Newsletters → Reports provides seven reports for reviewing newsletter performance. Choose a report and date range to see headline figures, a table and, where useful, a time-series chart. Each figure includes its definition. This page is for the store owner reading the numbers after a campaign.

Every report supports CSV export and has a shareable URL. Reports are read-only.

Pick a report and a range

The report pills across the top choose one of the seven. The row under them picks the dates: last 7 days, 30 days, 90 days, this year, all time, or between two dates you type.

List growth gains a Grouped by picker for day, week or month. A report that can be narrowed gains one select: a list, a campaign or an automation. Refresh throws away the cached answer and reads the store again. Export CSV downloads the table as it appears.

Everything you press is written to the address bar, so a screen you have built is a URL you can bookmark or send: #/section/newsletters/reports/list-growth?from=2026-06-01&to=2026-09-05&bucket=week&list=2.

How to read the figures

  • A rate comes with the counts it is made of. Open rate 41.7% sits in a row that also carries Sent 192 and Opened 79, so you can check the arithmetic by eye.
  • Null is not zero. A rate with nothing to divide by is a dash, not 0%. "Nobody opened it" and "nobody has been sent to yet" are different answers.
  • Dates are UTC. A campaign's date here and its date on the campaign screen are the same day. Send times is the one exception and reads the store's own timezone.

There is no money on any of these reports. What a campaign earned is a question for the base plugin's Analytics screen, which knows about refunds.

Campaign performance

reports/campaigns is one row per campaign that started sending inside the window, in sending order, with a totals row at the foot. The chart is open and click rate over the campaigns. A subject test says which subject won beside its name.

Figure What it means
Campaigns How many campaigns started sending inside the range. A campaign that started before it and finished inside it is not here: a campaign belongs to the day it went out.
Sent How many messages those campaigns handed to the transport altogether.
Accepted How many the transport reported as delivered. A store whose transport reports nothing has zeroes here for ever.
Opened How many recipients loaded the tracking pixel at least once. Somebody who reads with images off never counts, so this is a floor rather than a total.
Clicked How many recipients followed at least one link. Counted once per person however many links they pressed.
Bounced How many of the campaign's messages did not arrive: a permanent bounce, or a message the transport would not put on the wire. A soft bounce is not in here, because a full mailbox may yet accept the message.
Left How many people unsubscribed from this campaign, by whichever door: the link at the foot, the one-click header, or the preference centre.
Complained How many recipients pressed their mail app's spam button. Only a transport that reports complaints can fill this in, and only some receivers report them at all.
Open rate Opened as a share of everybody the campaign reached: the largest of sent, accepted, opened and clicked, so a campaign halfway through its list does not report a rate that falls as it goes.
Click rate Clicked as a share of the same denominator.
Bounce rate Bounced as a share of sent.
Unsubscribe rate People who left as a share of sent. A rate that jumps on one campaign is about that campaign. A rate that climbs slowly is about how often the store writes.
Complaint rate Complaints as a share of sent. Gmail asks bulk senders to stay under 0.1% and treats 0.3% as the line, so the third decimal is the one that matters.

The totals row divides the totals. It does not average the campaigns' rates.

List growth

reports/list-growth is per day, week or month: who joined, how they arrived, who confirmed, who left and who was suppressed. The chart is joined against left. List narrows it to one list, and then "joined" means joined that list.

Figure What it means
Joined How many people were added to the mailing list inside the range, whichever way they arrived. Somebody who joined, left and joined again is one person, counted on the day they first arrived.
Confirmed How many people pressed the button in a confirmation email. They need not have joined in the same period: somebody who signs up on a Friday and confirms on the Monday is counted in each period on the day it happened. With double opt-in off this stays at zero.
Left How many people unsubscribed from everything. Leaving one list of several is not here: they are still on the mailing list.
Suppressed How many addresses were added to the never-mail-again list, from a hard bounce, a complaint, or somebody adding one by hand.
Net Joined minus left. Suppressions are counted beside it and not subtracted, because a suppression usually follows an unsubscribe or a bounce and taking both off would count the same person twice.

There is one column per way somebody can arrive (signup form, at checkout, their account, imported, API, added by the store, an automation) and they add up to Joined. Months are calendar months. Weeks are seven days from the start of the window rather than Mondays, so the first bar is never a stub.

Engagement

reports/engagement tells you who to win back and who to stop mailing.

Figure What it means
Read something in 30 days How many subscribers opened or clicked at least one message in the thirty days up to the end of the range. Counted per person, not per message.
In 60 days The same over sixty days. It includes everybody in the thirty-day figure, so the two are not added together.
In 90 days The same over ninety days, and it includes the sixty-day figure for the same reason.
Never opened anything Subscribers who have never opened or clicked any message this store has sent them. Somebody who has never been sent anything is in here too.
Dormant Subscribers who were written to in the last ninety days and opened none of it.

Somebody the store has not written to is not dormant. This is the same rule the segment engine's "has not opened in N days" condition follows.

Under the figures is a breakdown by list and by tag: how many people are on each today, how many messages went to them in the range, and their own open and click rates. Somebody on two lists is counted under each, so these rows do not add up to the figures above.

reports/links lists the most pressed links across the campaigns in the window, most pressed first. Campaign narrows it to one.

Column What it means
Link The address as it was written in the campaign, before any tracking or link tagging was added to it.
Campaign The campaign the link was in. The same address in two campaigns is two rows.
People How many different recipients pressed this link.
Clicks Every press, including the same person twice.
Of opens The people who pressed this link as a share of the people who opened that campaign. Somebody who never opened the mail never had the chance to press anything.

Bounces and complaints

reports/bounces is the report to open when a provider's dashboard says your reputation slipped. It has two tables: by the company receiving the mail, and by the company sending it.

Figure What it means
Sent How many messages left this store inside the range, campaigns and automation emails together.
Bounced How many of them bounced at all, soft bounces included. This is larger than the Bounced column on the campaign report, which counts only the ones that stopped a message arriving.
Bounce rate Bounced as a share of sent.
Complaints How many recipients pressed the spam button on a message sent inside the range.
Complaint rate Complaints as a share of sent.
Column What it means
Hard Bounces the provider called permanent: the mailbox does not exist. One of these suppresses the address on its own.
Soft Bounces the provider called temporary, or would not classify. Three inside thirty days are treated as a hard one.
Refused Messages the sending provider would not put on the wire at all: a quota, a virus scan, a header it would not write. Nothing was said about the person, so nobody is suppressed for one.
Bounced Every bounce on a send row, whether or not the provider ever said which kind it was. It is never less than the three columns before it added together.

The three-way split comes from the events log. A store whose transport reports nothing has no split at all, and the plain Bounced column is the one that is always true.

A domain with fewer than ten messages is gathered into "Everywhere else" rather than given a row, so one bounce out of two does not read as a fifty per cent bounce rate. The columns still add up.

Automations

reports/automations is per flow: who went in, who is currently enrolled, who finished, who left early and why, and how the emails did. Automation cuts it by the steps of one flow instead, and then the three columns about people go.

Figure What it means
Enrolled How many people entered an automation inside the range. Somebody who entered the same one twice, on a flow that allows re-entry, is two.
Walking one now How many people are inside an automation at this moment. A count of now rather than of the range.
Finished How many people reached the end of an automation inside the range.
Emails sent How many automation emails went out inside the range. They are never listed with the campaigns.
Open rate Opens as a share of those emails.

Why they left names each reason with its count: a purchase, an unsubscribe, a bounce or complaint, no longer matching the automation's segment, or somebody taking them out by hand. Finishing is not one of them. That is the Finished column.

Send times

reports/send-times is opens by hour of the day and day of the week, as a grid shaded from quietest to busiest. Every number is in the page, and hovering a cell shows its count.

  • It is when people read, not when you sent. A campaign that went out on Tuesday morning and was read on Wednesday evening is a Wednesday evening.
  • It is only as good as open tracking. Anybody reading with images off is not in it, and neither is anybody on a store with open tracking switched off.

Times are in the store's own clock: reports.timezone in the base plugin, falling back to Grav's system.timezone and then UTC. This is the same clock an automation's "Tuesday at nine" is worked out in.

Export a report to CSV

  1. Build the report you want on the screen.
  2. Click Export CSV.

The file is the table on the screen, with the report's own headings in the report's own order. Rates are written as percentages with one decimal, with the counts each rate is made of in their own columns beside it. Dates are ISO-8601 in UTC. The file is named newsletter-<report>-<from>-to-<to>.csv.

The five-minute cache

Answers are cached for five minutes, keyed on everything that changes the answer: the report, both ends of the range, the bucket and whatever it was narrowed by. Every answer says whether it came from the cache and when it was worked out. Click Refresh to throw away the cached answer for exactly this question and read the store again.

The routes

Both routes are GET and both take kahunacart.newsletters.view. Nothing here writes anything about the store, so a reader who cannot send mail can still read every report.

Route What it answers
GET /kahunacart/newsletters/reports/{type} One report: the range, the figures, the chart, the columns, the rows, and a definition per figure and per column.
GET /kahunacart/newsletters/reports/{type}.csv The same table as a file.

{type} is one of campaigns, list-growth, engagement, links, bounces, automations or send-times.

Parameter What it does
from, to The window, as YYYY-MM-DD. Whole days at both ends, so a range of one day is one whole day. An epoch is accepted too.
preset 7d, 30d, 90d, year, all or custom, expanded into the same from and to you could have written. Explicit dates win over a preset. Left off, the window is the last 30 days.
bucket day, week or month, for List growth. Left off, it is chosen from the length of the window: days up to a quarter, weeks up to about thirteen months, months beyond that.
list, campaign, flow Narrow the report that offers it.
refresh 1 throws away the cached answer for exactly this question and reads the store again.

In the JSON every rate is a fraction between 0 and 1, and is null rather than zero where there was nothing to divide by. Times are UTC epoch seconds, except on the send-time report, which reads the store's own timezone and names it.

The MCP tool

One tool, get_newsletters_report, takes a type parameter and the same window parameters the route takes. Its description carries the definitions, so a model quoting a figure quotes what it means alongside it. The CSV route has no tool, because the answer is a document to read rather than a payload.

The tool is listed with the rest on Run the store from an AI assistant.