---
title: Analytics
description: Query daily send and engagement rollups by day, tag, and recipient domain, plus Gmail spam-rate reputation for your sending domains.
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

Mailfully exposes four read-only analytics endpoints:

| Endpoint | Returns |
|---|---|
| [`GET /v1/analytics/daily`](/api-reference/analytics/daily) | Per-day totals |
| [`GET /v1/analytics/by-tag`](/api-reference/analytics/by-tag) | Per-day counters per tag value |
| [`GET /v1/analytics/by-domain`](/api-reference/analytics/by-domain) | Per-day counters per recipient domain |
| [`GET /v1/analytics/external-reputation`](/api-reference/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

<Tabs>
<Tab title="curl">

```bash
curl "https://api.mailfully.com/v1/analytics/daily?from=2026-06-08&to=2026-07-08" \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

</Tab>
<Tab title="Node SDK">

```typescript
import { Mailfully } from "@mailfully/node";

const mailfully = new Mailfully({ apiKey: process.env.MAILFULLY_API_KEY ?? "" });

const { data, error } = await mailfully.analytics.daily({
  from: "2026-06-08",
  to: "2026-07-08",
});
```

</Tab>
</Tabs>

```json
{
  "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`:

<Tabs>
<Tab title="curl">

```bash
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"
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.analytics.byTag({
  tag: "order-shipped",
  from: "2026-07-01",
  to: "2026-07-08",
});
```

</Tab>
</Tabs>

```json
{
  "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 }
  ]
}
```

<Warning>
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.
</Warning>

## 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`:

<Tabs>
<Tab title="curl">

```bash
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"
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.analytics.byDomain({
  domain: "example.com",
  from: "2026-07-01",
  to: "2026-07-08",
});
```

</Tab>
</Tabs>

```json
{
  "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).

<Tabs>
<Tab title="curl">

```bash
curl https://api.mailfully.com/v1/analytics/external-reputation \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.analytics.externalReputation();
```

</Tab>
</Tabs>

```json
{
  "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

| 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`).
