# Sending analytics

Delivery, bounce, complaint, and click rates over any window up to 400 days, grouped by day, week, or month, broken down by domain, tag, or template.

One endpoint answers what you sent, what arrived, what bounced, what people complained about, and what they clicked, over a range you choose, grouped how you want it, split by the dimension you care about. It is the same data the dashboard draws, and it is a GET.

## Four breakdowns, and they are the four you would ask for

By sending domain, so a subdomain having a bad week is visible before it drags the apex down with it. By tag, which is how you separate password resets from receipts without separating them into different accounts. By mail class, transactional against marketing. By template. A breakdown names the top 20 and folds the rest into Other rather than returning a row for every tag value you have ever sent.

## There is no open rate here, deliberately

You get opened, opens, and opens_tracked. You do not get an open rate, because Apple Mail Privacy Protection fetches the pixel for a large and unknowable share of recipients whether or not anybody read the message. An open count is a floor, and the endpoint says so in its own response. Publishing a percentage computed on top of that number would be inventing precision, and the number people are most likely to make a decision on is the one we are least willing to make up.

## A rate with no denominator is null, not zero

A day you sent nothing has a bounce_rate of null. Zero would be a claim that nothing bounced, which is a different statement from not having sent anything, and it is the kind of difference that makes a chart lie about a quiet weekend.

## Counted off the delivery log, not off event rows

Every figure comes from counters on the messages themselves. Nothing counts rows in the raw open and click event tables, because those are prunable under retention and a total that shrinks when old events are cleaned up is a total nobody can reconcile.

## Bot opens are recognised and do not move the counters

A fetch of the tracking pixel with no user agent, with a known machine agent, or arriving within five seconds of the send is classified as a prefetch rather than a person. It is still stored, so the record is complete, but it does not move a counter and it does not start an automation. A welcome sequence that branches on "did they open it" should not branch on a scanner.

## The API

| Endpoint | What it does |
| --- | --- |
| `GET /v1/analytics?from=&to=` | Totals and a series. Up to 400 days, with `to` inclusive. |
| `&group_by=day|week|month` | The bucket size for the series. |
| `&breakdown=domain|tag|mail_class|template` | Split the range. With tag, also pass tag_key. |
| `&domain=&mail_class=&template_id=&tag=key:value` | Filters, applied before the grouping. |
| `GET /v1/analytics/tags?days=` | Which tag keys you have actually been sending, and how much of each. |

## Bounce rate by domain, last 30 days

One call. The response carries totals, a weekly series, and a row per sending domain.

```bash
curl -sS -G https://emails.sh/v1/analytics \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  --data-urlencode "from=2026-07-01" \
  --data-urlencode "to=2026-07-31" \
  --data-urlencode "group_by=week" \
  --data-urlencode "breakdown=domain" \
  --data-urlencode "mail_class=transactional"

# Each row carries sent, delivered, bounced, complained, delivered_rate,
# bounce_rate, complaint_rate, clicks_tracked, clicked, clicks, click_rate,
# opens_tracked, opened, and opens. Rates are null where nothing was sent.
```

## What this does not do

### No open rate, and that is not an oversight

See above. If your reporting requires one, compute opened divided by delivered yourself and know what you are computing.

### No per-link click breakdown in the API

The dashboard shows which links in a message were clicked. The analytics endpoint returns click totals and a rate, not a table of URLs.

### No geography, device, or client breakdown

The four breakdowns above are the four there are. Nothing here tells you which mail client somebody read in or which country they were in.

### Open and click webhooks do not exist

Delivery, bounce, complaint, inbound, and filtered events fire webhooks. Opens and clicks do not: they feed the engagement record that segments read and they can start an automation, but nothing posts to your endpoint when somebody opens a message.

### Tracking is a workspace default for transactional mail

Open and click tracking are off unless you switch them on, and for POST /v1/emails the switch is the workspace default rather than a field on the request. Broadcasts take track_opens and track_clicks per broadcast.

## Questions

### How far back can I query?

Up to 400 days in one range, with the default being the last 30. How much history exists depends on your plan's retention.

### How do tags work?

Tags are a map of key to value you pass on a send. Analytics breaks down by one key at a time, so pass tag_key with breakdown=tag, and use GET /v1/analytics/tags to see which keys you have actually been using.

### Does marketing mail appear in the same analytics as transactional?

Yes, in one endpoint, separable with mail_class. That is the point: the bounce rate that matters for your domain's reputation is the one across everything you send from it.

### Why is my click rate lower than my provider used to report?

Two likely reasons. Click tracking is off by default here, so untracked mail contributes sends without contributing clicks. And a click on an unsubscribe link is not counted, because those links are excluded from rewriting.

## Related

- [Deliverability](https://emails.sh/features/deliverability.md)
- [Broadcasts](https://emails.sh/features/broadcasts.md)
- [Dedicated IPs](https://emails.sh/features/dedicated-ips.md)

Docs: https://emails.sh/docs.md