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:

EndpointReturns
GET /v1/analytics/dailyPer-day totals
GET /v1/analytics/by-tagPer-day counters per tag value
GET /v1/analytics/by-domainPer-day counters per recipient domain
GET /v1/analytics/external-reputationGmail 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:

PlanRetention
Free30 days
Starter30 days
Growth60 days
Scale90 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.0007 means 0.07% of delivered mail was reported as spam. null when 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, or null when no domain has a numeric rate. null means "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

CounterIncremented when
sentThe message was handed off for delivery.
deliveredThe recipient's mail server accepted the message.
bouncedThe message permanently bounced. Transient and undetermined bounces are never counted.
complainedThe recipient marked the message as spam.
openedThe recipient opened the message.
clickedThe 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).