# Get daily sending statistics

`GET /v1/email/stats/daily`

Returns one row of aggregate sending statistics per calendar day for the workspace: UTC days by default, or your local days when `timezone` is set. Days with no activity are included with zero counts, so the series charts without client-side gap handling. Suited to charts and trend lines; for per-message exact accounting use the message detail endpoints.

Rows use event time. For example, a complaint received on Wednesday for a message sent the prior Monday is counted in Wednesday's row.

The maximum window is 365 days; requesting a longer range returns `422`.

## Code samples

**TypeScript**

```ts
const series = await bird.email.stats.daily({ from: "2026-05-01", to: "2026-05-31" });
for (const row of series.data) console.log(row.bucket, row.delivery.delivered);
```

Examples: [TypeScript](/docs/api/reference/get-email-stats-daily.ts.md) · [Python](/docs/api/reference/get-email-stats-daily.py.md) · [Go](/docs/api/reference/get-email-stats-daily.go.md) · [PHP](/docs/api/reference/get-email-stats-daily.php.md) · [CLI](/docs/api/reference/get-email-stats-daily.cli.md) · [MCP](/docs/api/reference/get-email-stats-daily.mcp.md) · [cURL](/docs/api/reference/get-email-stats-daily.curl.md)

## Example response `200`

```json
{
  "period": {
    "from": "2026-05-01",
    "to": "2026-05-31",
    "grain": "day",
    "data_as_of": "2026-05-25T14:03:10Z"
  },
  "data": [
    {
      "bucket": "2026-05-25",
      "sends_accepted": 412,
      "delivery": {
        "accepted": 14820,
        "processed": 14810,
        "delivered": 14720,
        "bounced": 90,
        "bounces": {
          "hard": 12410,
          "soft": 14290,
          "admin": 410,
          "block": 920,
          "undetermined": 80,
          "hard_rate": 0.454,
          "soft_rate": 0.523,
          "admin_rate": 0.015,
          "block_rate": 0.0337,
          "undetermined_rate": 0.0029
        },
        "complained": 3,
        "deferred": 14,
        "rejected": 10,
        "oob_bounces": 2,
        "effective_delivered": 14718,
        "all_bounces": 92,
        "oob_rate": 0.00014,
        "delivery_rate": 0.9939,
        "bounce_rate": 0.0061,
        "complaint_rate": 0.0002
      },
      "engagement": {
        "opens": 5420,
        "opens_non_prefetched": 3210,
        "unique_opens": 3640,
        "unique_opens_non_prefetched": 2480,
        "clicks": 924,
        "unique_clicks": 621,
        "unsubscribes": 12,
        "open_rate": 0.1683,
        "click_rate": 0.0422,
        "unsubscribe_rate": 0.0009
      },
      "latency": {
        "processing": {
          "p50_ms": 420,
          "p95_ms": 1820,
          "p99_ms": 4920
        },
        "delivery": {
          "p50_ms": 420,
          "p95_ms": 1820,
          "p99_ms": 4920
        },
        "total": {
          "p50_ms": 420,
          "p95_ms": 1820,
          "p99_ms": 4920
        }
      }
    }
  ]
}
```

## Query parameters

- `from` (string): Start date (inclusive), `YYYY-MM-DD`. Interpreted as a calendar day in `timezone` (a UTC day when `timezone` is omitted). Defaults to 30 days before `to` when omitted.
- `to` (string): End date (inclusive), `YYYY-MM-DD`. Interpreted as a calendar day in `timezone` (a UTC day when `timezone` is omitted). Defaults to today in that timezone when omitted. Window may not exceed 365 days.
- `timezone` (string): IANA timezone identifier used to group statistics, for example `Asia/Kathmandu`. The default is UTC. Day and hour boundaries, including the default window when `from` and `to` are omitted, follow this timezone. When this parameter is set, pass `from` and `to` as calendar days or `Z` instants instead of timestamps with explicit UTC offsets.
- `category` (string): Restrict the statistics to a category or a comma-separated union of categories: `transactional` or `marketing`. Mutually exclusive with the other dimension filters; only one may be set per request.
- `sending_domain` (string): Restrict the statistics to a sending domain or a comma-separated union of sending domains (the part of the From address after @). Mutually exclusive with the other dimension filters; only one may be set per request.
- `tag` (string): Restrict the statistics to a tag. Use `name` to match any value of a tag, or `name:value` for a specific pair (for example `campaign:spring_launch`). To combine values of one tag name, separate complete pairs with commas, such as `campaign:spring,campaign:summer`. Different tag names cannot be combined. Mutually exclusive with the other dimension filters; only one may be set per request.
- `sending_ip` (string): Restrict the statistics to a sending IP or a comma-separated union of IPs. Mutually exclusive with the other dimension filters; only one may be set per request. A sending IP is assigned only after a message reaches delivery, so this filter reports delivery-side metrics only. Accepted, processed, rejected, complaint, and engagement counts are `0`, and processing latency is `null`. Complaint, open, and click rates are `0` when deliveries exist and `null` otherwise.
- `recipient_domain` (string): Restrict the statistics to a recipient mailbox domain or a comma-separated union of domains (the part of the recipient address after the `@`, for example `gmail.com`). Mutually exclusive with the other dimension filters; only one may be set per request.
- `template` (string): Restricts the statistics to a template or a comma-separated union of templates, identified by ID (`emt_…`) or name. This parameter is mutually exclusive with other dimension filters.

## Response body

- `period` (object, required): The window and bucket grain the response covers, echoed from the request, plus the freshness boundary the data is current to.
- `period.from` (string, required): Inclusive start of the window. A calendar day (YYYY-MM-DD, in the requested `timezone`) on the day grain. On the hour grain, an RFC 3339 UTC instant marking the start of the first hour bucket, which falls on a local hour boundary when `timezone` is set.
- `period.to` (string, required): Inclusive end of the window. A calendar day (YYYY-MM-DD, in the requested `timezone`) on the day grain. On the hour grain, an RFC 3339 UTC instant marking the start of the last hour bucket, which falls on a local hour boundary when `timezone` is set.
- `period.grain` (string, required)

  The bucket grain of the series, either `day` or `hour`.

  Possible values: `day`, `hour`
- `period.data_as_of` (nullable string): The instant the statistics in this response are current to: events recorded up to roughly this time are reflected, while more recent events may not be yet. Statistics are served from a rolling aggregation that refreshes every few seconds, so a response reflects data from up to a few seconds ago. Use this field to label data freshness rather than assuming the numbers are to-the-second. Null when the freshness boundary is not being reported.
- `data` (array of object, required): One row per bucket (day or hour, per the grain) in the period, in chronological order. Buckets with no activity are included with zero counts.
- `data.bucket` (string, required): The day (YYYY-MM-DD, in the requested `timezone`) or hour this point covers, matching the period's grain. An hour bucket is an RFC 3339 UTC instant marking the start of the hour. It falls on a local hour boundary when `timezone` is set, which is on the UTC hour only for whole-hour offsets.
- `data.sends_accepted` (integer, required): Distinct email messages accepted in this bucket, counted at the message level (one per accepted send regardless of how many recipients it addresses). Every other metric in `delivery` and `engagement` is recipient-level or event-level.
- `data.delivery` (object, required)
- `data.delivery.accepted` (integer): Distinct recipients accepted for delivery after suppression filtering. Reported on time buckets and the period summary. Breakdown rows leave it out, because their rollups do not have it.
- `data.delivery.processed` (integer, required): Distinct recipients whose message was processed and handed off for delivery.
- `data.delivery.delivered` (integer, required): Distinct recipients whose message the receiving mail server accepted.
- `data.delivery.bounced` (integer, required): Distinct recipients whose delivery failed. This is approximately the sum of the five `bounces.*` sub-counts (hard, soft, admin, block, undetermined). The two totals are worked out independently, so they can differ slightly.
- `data.delivery.bounces` (object, required)
- `data.delivery.bounces.hard` (integer, required): Distinct recipients with a permanent delivery failure (invalid address or non-existent domain).
- `data.delivery.bounces.soft` (integer, required): Distinct recipients with a transient delivery failure (mailbox full or server temporarily unavailable).
- `data.delivery.bounces.admin` (integer, required): Distinct recipients refused by a policy at the receiving end, such as relaying denied or a blocklisted domain.
- `data.delivery.bounces.block` (integer, required): Distinct recipients bounced because the receiving mail server blocked the sending IP for reputation reasons.
- `data.delivery.bounces.undetermined` (integer, required): Distinct recipients bounced where the receiving server's response did not allow precise classification.
- `data.delivery.bounces.hard_rate` (nullable number, required): Fraction of bounced recipients that hard bounced, computed as `hard / bounced`. Null when `bounced` is zero.
- `data.delivery.bounces.soft_rate` (nullable number, required): Fraction of bounced recipients that soft bounced, computed as `soft / bounced`. Null when `bounced` is zero.
- `data.delivery.bounces.admin_rate` (nullable number, required): Fraction of bounced recipients that admin bounced, computed as `admin / bounced`. Null when `bounced` is zero.
- `data.delivery.bounces.block_rate` (nullable number, required): Fraction of bounced recipients that block bounced, computed as `block / bounced`. Null when `bounced` is zero.
- `data.delivery.bounces.undetermined_rate` (nullable number, required): Fraction of bounced recipients with undetermined classification, computed as `undetermined / bounced`. Null when `bounced` is zero.
- `data.delivery.complained` (integer, required): Distinct recipients who reported the message as spam via a feedback loop.
- `data.delivery.deferred` (integer, required): Distinct recipients whose delivery the receiving server temporarily delayed and is still being retried.
- `data.delivery.rejected` (integer, required): Distinct recipients rejected before any delivery attempt. Includes recipients on the workspace suppression list, transmissions that could not be completed, message-generation failures, and recipients refused by sending policy. The per-recipient `rejection_reason` field on `GET /v1/email/messages/{message_id}/recipients` surfaces the specific cause.
- `data.delivery.oob_bounces` (integer, required): Out-of-band bounce events: distinct failure notifications received after the receiving server had initially confirmed delivery. The count represents deduplicated events rather than unique recipients.
- `data.delivery.effective_delivered` (integer, required): Recipients who remain delivered after all bounce signals resolve, computed as `delivered - oob_bounces`. Use this as the base for engagement-rate denominators. Clamped to 0 when `oob_bounces` exceeds `delivered`.
- `data.delivery.all_bounces` (integer, required): Total recipients in this scope who did not receive the message, computed as `bounced + oob_bounces`.
- `data.delivery.oob_rate` (nullable number, required): Share of this scope's delivery attempts that resulted in an out-of-band bounce, computed as `oob_bounces / (delivered + bounced)`. Null when there were no attempts.
- `data.delivery.delivery_rate` (nullable number, required): Share of this scope's delivery attempts that remained delivered after all bounce signals, computed as `effective_delivered / (delivered + bounced)`. Null when there were no attempts.
- `data.delivery.bounce_rate` (nullable number, required): Share of this scope's delivery attempts that ultimately failed (inband or out-of-band), computed as `all_bounces / (delivered + bounced)`. Because `oob_bounces` counts events rather than recipients, `all_bounces` can exceed the attempt count. The rate is clamped to 1. Null when there were no attempts.
- `data.delivery.complaint_rate` (nullable number, required): Spam complaints in this scope relative to effectively delivered recipients, computed as `complained / effective_delivered`. Complaints are attributed by event time, so a scope can record more of them than it effectively delivered, pushing the rate above 1. Null when `effective_delivered` is zero.
- `data.engagement` (object, required)
- `data.engagement.opens` (integer, required): Distinct open events, counting repeat opens from the same recipient and opens auto-fetched by inbox privacy features (such as Apple Mail Privacy Protection and the Gmail image proxy).
- `data.engagement.opens_non_prefetched` (integer, required): Distinct open events excluding those auto-fetched by inbox privacy features. Same event-counting semantics as `opens` (repeat opens from the same recipient count separately), with prefetched opens removed.
- `data.engagement.unique_opens` (integer, required): Distinct recipients who opened at least once, including opens auto-fetched by inbox privacy features.
- `data.engagement.unique_opens_non_prefetched` (integer, required): Distinct recipients who opened at least once, excluding opens auto-fetched by inbox privacy features. This is the numerator used for open rate, so iOS-heavy audiences (Apple Mail Privacy Protection and similar) do not inflate it.
- `data.engagement.clicks` (integer, required): Distinct click events, counting repeat clicks from the same recipient.
- `data.engagement.unique_clicks` (integer, required): Distinct recipients who clicked at least once.
- `data.engagement.unsubscribes` (integer, required): Distinct unsubscribe events, recorded via the list-unsubscribe header or the footer link.
- `data.engagement.open_rate` (nullable number, required): Distinct non-prefetched openers relative to effectively delivered recipients in the same scope, computed as `unique_opens_non_prefetched / delivery.effective_delivered`; on rows without an `effective_delivered` field (the mailbox-provider breakdowns) the denominator equals `delivery.delivered`. The numerator excludes opens auto-fetched by inbox privacy features. Opens are attributed by event time, so engagement earned by earlier deliveries can push the rate above 1. Null when the denominator is zero.
- `data.engagement.click_rate` (nullable number, required): Distinct clickers relative to effectively delivered recipients in the same scope, computed as `unique_clicks / delivery.effective_delivered` (`delivery.delivered` on rows without an `effective_delivered` field). Clicks are attributed by event time, so engagement earned by earlier deliveries can push the rate above 1. Null when the denominator is zero.
- `data.engagement.unsubscribe_rate` (nullable number, required): Unsubscribe events relative to effectively delivered recipients in the same scope, computed as `unsubscribes / delivery.effective_delivered` (`delivery.delivered` on rows without an `effective_delivered` field). Unsubscribes are attributed by event time, so the rate can exceed 1. Null when the denominator is zero.
- `data.latency` (object, required)
- `data.latency.processing` (object): Approximate p50, p95, and p99 latency percentiles in milliseconds for one latency family over the bucket. All three are null when no qualifying event contributed a measurement.
- `data.latency.processing.p50_ms` (nullable integer, required): Median (50th percentile) latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.processing.p95_ms` (nullable integer, required): 95th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.processing.p99_ms` (nullable integer, required): 99th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.delivery` (object): Approximate p50, p95, and p99 latency percentiles in milliseconds for one latency family over the bucket. All three are null when no qualifying event contributed a measurement.
- `data.latency.delivery.p50_ms` (nullable integer, required): Median (50th percentile) latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.delivery.p95_ms` (nullable integer, required): 95th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.delivery.p99_ms` (nullable integer, required): 99th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.total` (object): Approximate p50, p95, and p99 latency percentiles in milliseconds for one latency family over the bucket. All three are null when no qualifying event contributed a measurement.
- `data.latency.total.p50_ms` (nullable integer, required): Median (50th percentile) latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.total.p95_ms` (nullable integer, required): 95th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.
- `data.latency.total.p99_ms` (nullable integer, required): 99th percentile latency in milliseconds. Null when no qualifying event contributed a measurement.

## Related resources

- [What is click-through rate for email and SMS?](/explained/engagement/what-is-click-through-rate) (answer)
- [Email analytics](/email-api/features/analytics) (product)
- [Email metrics](/docs/guides/email/tracking-and-metrics) (docs)

[Get an implementation brief](/learn/workspace?topic=email-metrics)
