Analytics
Query daily send and engagement rollups by day, tag, and recipient domain, plus Gmail spam-rate reputation for your sending domains.
Mailfully exposes five read-only analytics endpoints:
| Endpoint | Returns |
|---|---|
GET /v1/analytics/engagement | Raw daily open/click events by automation signal |
GET /v1/analytics/daily | Per-day totals |
GET /v1/analytics/by-tag | Per-day counters per tag value |
GET /v1/analytics/by-domain | Per-day counters per recipient domain |
GET /v1/analytics/external-reputation | Gmail spam-rate signal per sending domain |
All five require the read:analytics scope. It can be minted onto an API key. A key created with only send gets 403 insufficient_scope. The three rollup endpoints and engagement events are environment-scoped, so test and live counts never mix; external reputation is org-wide.
Daily rollups
curl "https://api.mailfully.com/v1/analytics/daily" \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx"{
"data": [
{ "day": "2026-07-06", "sent": 412, "delivered": 402, "bounced": 6, "complained": 1, "opened": 118, "clicked": 37 },
{ "day": "2026-07-07", "sent": 389, "delivered": 381, "bounced": 4, "complained": 0, "opened": 102, "clicked": 29 }
]
}
Each row is one UTC day with six counters: sent, delivered, bounced, complained, opened, clicked. Counters count recipients. A send to three addresses adds 3 to sent and up to 3 to delivered; opened and clicked are capped per message at its addressed recipient count. They estimate recipients rather than unique people; rows from before the cap was introduced retain their original counts. Days with no traffic are omitted rather than zero-filled, so fill gaps client-side if you chart the data.
Date windows and retention
from and to take YYYY-MM-DD calendar dates and are both inclusive. When omitted, to defaults to the current UTC day and from to 30 days before it. A malformed or impossible date (2026-13-40) returns 422 validation_error with param naming the field.
from is clamped up to your plan's retention floor on every request; to is never clamped. Asking for data older than your retention window does not error: you get rows starting at the floor. Retention for each plan is in Quotas and plan limits.
Raw engagement events
Use GET /v1/analytics/engagement when you need every retained open and click event, including repeated pixel loads and link requests. These raw counts are separate from the recipient-capped rollup counters and the dashboard's capped engagement rates.
const { data, error } = await mailfully.analytics.engagement({
from: "2026-10-01",
to: "2026-10-17",
});
if (error) {
console.error(error.message); // Unavailable is not zero activity.
} else {
for (const row of data.data) {
console.log(row.day, row.opens.total, row.opens.total - row.opens.likely);
}
}
{
"data": [
{
"day": "2026-10-17",
"opens": { "total": 8, "likely": 3, "unlikely": 2, "unknown": 3 },
"clicks": { "total": 4, "likely": 1, "unlikely": 1, "unknown": 2 }
}
]
}
For each kind, total = likely + unlikely + unknown. The classification uses a reported automation signal, never a user-agent guess. Missing, malformed, and historical metadata is unknown. Excluding likely automation means total - likely, so unknown events remain included. unlikely and unknown can still be automated; none of these counts establishes a person opened a message or that it reached an Inbox.
Rows use UTC event dates and omit days with no events. from and to are inclusive YYYY-MM-DD calendar dates. Omitted to defaults to today's UTC date; omitted from defaults independently to 30 days before today. Supplying only an older to can therefore make the default window reversed.
This endpoint returns 422 validation_error for malformed or impossible dates, a future to, from after to, or more than 93 inclusive days. Window validation happens before retention clamping. Events must also meet the plan's exact retention timestamp, so the first retained day may be partial. An entirely expired valid window returns { "data": [] }. A failed or timed-out read returns an error; display unavailable rather than substituting zeros. These stricter window rules apply to engagement events, not the existing rollup endpoints.
By tag
Rollups are keyed by tag value, not tag name. A send tagged { "name": "event", "value": "order-shipped" } counts under order-shipped:
curl "https://api.mailfully.com/v1/analytics/by-tag?tag=order-shipped&from=2026-07-01&to=2026-07-08" \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx"{
"data": [
{ "tag": "order-shipped", "day": "2026-07-06", "sent": 152, "delivered": 149, "bounced": 2, "complained": 0, "opened": 47, "clicked": 16 },
{ "tag": "order-shipped", "day": "2026-07-07", "sent": 141, "delivered": 139, "bounced": 1, "complained": 0, "opened": 43, "clicked": 12 }
]
}
A message with N distinct tag values is counted under each of the N tags, so summing across tags over-counts multi-tag sends. For totals, use /v1/analytics/daily.
By recipient domain
The domain axis is where your mail went: the recipient's domain, taken from the first to recipient that survives suppression filtering, not your sending domain. A send to ada@example.com from orders@mail.acme.com rolls up under example.com:
curl "https://api.mailfully.com/v1/analytics/by-domain?domain=example.com&from=2026-07-01&to=2026-07-08" \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx"{
"data": [
{ "domain": "example.com", "day": "2026-07-06", "sent": 214, "delivered": 209, "bounced": 3, "complained": 1, "opened": 64, "clicked": 21 },
{ "domain": "example.com", "day": "2026-07-07", "sent": 198, "delivered": 195, "bounced": 2, "complained": 0, "opened": 57, "clicked": 18 }
]
}
Use it to see how individual mailbox providers treat your mail. A bounce spike isolated to one domain points at that provider, not your content. Messages without a resolvable recipient domain roll up under the sentinel row "unknown", so domain sums always equal the daily totals.
External reputation
This endpoint reports how Gmail sees your sending domains: the latest Gmail Postmaster user-reported spam-rate observation per verified sending domain. It takes no parameters and is org-scoped (test and live share it).
curl https://api.mailfully.com/v1/analytics/external-reputation \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx"{
"data": {
"domains": [
{ "domain": "mail.acme.com", "gmailSpamRate": 0.0007, "observedAt": "2026-07-06T09:00:00.000Z" }
],
"worstGmailSpamRate": 0.0007
}
}
Unlike the rest of the API, this payload uses camelCase field names:
gmailSpamRate: the spam rate as a fraction.0.0007means 0.07% of delivered mail was reported as spam.nullwhen the observation carries no numeric rate.observedAt: when Mailfully last polled Gmail Postmaster for the signal. The provider's data itself lags a day or two behind real traffic.worstGmailSpamRate: the maximum rate across your domains, ornullwhen no domain has a numeric rate.nullmeans "no signal", not 0%.
Domains with no Gmail signal at all (below Google's volume threshold, or not enrolled in Postmaster) do not appear in domains.
How metrics are computed
| Counter | Incremented when |
|---|---|
sent | The message was handed off for delivery. |
delivered | The recipient's mail server accepted the message. |
bounced | The message permanently bounced. Transient and undetermined bounces are never counted. |
complained | The recipient marked the message as spam. |
opened | A tracking pixel loaded, subject to the per-message recipient cap. |
clicked | A tracked link was requested, subject to the per-message recipient cap. |
Rejections, rendering failures, and delivery delays increment no counter. opened and clicked are engagement counters and never participate in bounce-rate math.
Also worth knowing:
- UTC days, by event time. Each event lands on the UTC day of its own timestamp, not the day Mailfully processed it. A delivery at 23:59 UTC and its open at 00:01 UTC land on different days.
- Counters only. The API never computes rates. Divide client-side (for example, bounce rate =
bounced / sent). - The dashboard's meters warn early. The bounce meter shows Watch at 2% and Critical at 4%. The complaint meter shows Watch at 0.05% and Critical at 0.07%. Critical is the threshold in the sending standards, so clean your list when a meter reaches Watch. The meters cover the date range you have selected; the policy uses a rolling 7-day window.
