Analytics
Query daily send and engagement rollups by day, tag, and recipient domain, plus Gmail spam-rate reputation for your sending domains.
Mailfully exposes four read-only analytics endpoints:
| Endpoint | Returns |
|---|---|
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 four 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 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?from=2026-06-08&to=2026-07-08" \
-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. Days with no traffic are omitted, not zero-filled — 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:
| Plan | Retention |
|---|---|
| Free | 30 days |
| Starter | 30 days |
| Growth | 60 days |
| Scale | 90 days |
Asking for data older than your retention window does not error: you get rows starting at the floor.
By tag
Rollups are keyed by tag value, not tag name. A send tagged { "name": "category", "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 true totals, use /v1/analytics/daily, which counts each message exactly once.
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 | The recipient opened the message. |
clicked | The recipient clicked a link in the message. |
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 day bucketing 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).